Postfix и Dovecot. Установка и настройка. Часть 1 из 2

05.01.2025

Теги: LinuxКлиентКонфигурацияНастройкаПочтаСервер

Почтовая служба

Типичная почтовая служба состоит из следующих основных компонентов

  • Почтовый сервер, или агент пересылки сообщений (Mail Transport Agent, MTA). Этот компонент ответственен за перемещение электронной почты между почтовыми серверами, этим занимаются, например, Sendmail и Postfix.
  • Агент доставки электронной почты (Mail Delivery Agent, MDA). Этот компонент отвечает за распределение полученных сообщений по локальным почтовым ящикам пользователей. Например, это Maildrop и Procmail. MDA иногда называют LDA (Local Delivery Agent).
  • Агент отправки электронной почты (Message Submission Agent, MSA). Этот компонент отвечает за получение сообщений от MUA и передачу этих сообщений MTA. Многие MTA также выполняют функцию MSA, но существуют специально разработанные MSA без полной функциональности MTA.
  • Агент доступа к электронной почте (Mail Access Agent, MAA). Этот компонент взаимодействует с почтовыми клиентами пользователей MUA по протоколу POP3 и/или IMAP. Например, это Cyrus или Dovecot для работы с POP3/IMAP.
  • Почтовый клиент, который ещё называют почтовым агентом (Mail User Agent, MUA). Именно с ним взаимодействует пользователь, например это Thunderbird или MS Outlook. Они позволяют пользователю читать почту и писать электронные письма.

Почтовый сервер (Mail Transport Agent, MTA) может выступать в роли сервера и в роли клиента. Когда почтовый сервер получает письма — он выступает как SMTP-сервер. Когда почтовый сервер отправляет письма — он выступает как SMTP-клиент. Поэтому в составе Postfix есть SMTP-клиент smtp и SMTP-сервер smtpd.

Отправка почты, протоколы SMTP и ESMTP

Протокол SMTP (Simple Mail Transfer Protocol) определяет правила пересылки почты между компьютерами, при этом, он не регламентирует правила хранения или визуализации сообщений. Протокол используется как для пересылки сообщений между двумя почтовыми серверами, так и для отправления сообщения от почтового клиента почтовому серверу.

Протокол SMTP — это старый и очень простой протокол, в котором отсутствуют многие возможности, без которых работа сегодня немыслима, поэтому сегодня практически везде используется его расширенная версия ESMTP (Extended Simple Mail Transfer Protocol), но очень часто название протокола, даже в технической литературе, продолжает указываться как SMTP.

Протокол SMTP использует для работы порт 25/TCP и изменить это значение нельзя, точнее, мы можем это сделать, но после этого нас не смогут найти другие почтовые сервера, которые решат доставить нам почту. Также это единственный порт, который обязательно должен быть доступен из внешней сети.

Современные коммуникации нельзя представить без шифрования и SMTP не исключение. Первоначально развитие многих протоколов шло по пути создания защищенной при помощи SSL/TLS версии и назначения для нее отдельного порта. Поэтому появился защищённый протокол SMTPS (SMTP over SSL), который работает на 465-ом порту.

Однако такой подход не принес желаемого результата и для протокола SMTP было разработано расширение STARTTLS. Почтовые серверы начинают сессию как обычную SMTP, а потом договариваются о шифровании и согласовывают алгоритмы. Всегда используется самый стойкий алгоритм из доступных, если же договориться не получилось, то обмен почтой происходит в открытом виде.

Все это взаимодействие происходит через один-единственный порт — 25, как для зашифрованной, так и незашифрованной почты.

В настоящий момент принято соглашение, что порт 25 используется только для ретрансляции почты, т.е. взаимодействия между серверами, клиентам использовать этот порт не следует. Для взаимодействия с клиентами была создана новая служба MSA (Message submission agent) — агент отправки почты, который обычно является частью MTA, но использует собственный порт 587 TCP.

Более того, вынесение обслуживания клиентов в отдельную службу позволяет более тонко настроить безопасность, например, запретив незащищенные подключения, что не позволит пользователю настроить почтовый клиент неправильно. При разделении функций MTA и MSA наилучшей практикой будет открытие 25 порта только во внешнюю сеть, а 587 — во внутреннюю.

Протокол SMTPS на 465-ом порту в настоящее время не используется почтовыми серверами для взаимодействия друг с другом. Но зато этот протокол часто использует служба MSA (Message submission agent), которая взаимодействует с почтовыми клиентами MUA (Mail User Agent). В этом случае служба MSA часто называется submissions (последняя s означает SSL).

Получение почты, протоколы POP3 и IMAP

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

IMAP — протокол, используемый для доступа к почтовым сообщениям, хранящимся на сервере электронной почты. Протокол позволяет пользователям получать доступ к сообщениям электронной почты непосредственно на сервере. Когда клиент запрашивает доступ к электронному сообщению, сервер отправляет копию сообщения клиенту. Когда пользователь удаляет или перемещает сообщение на одном устройстве, сервер обновляет статус сообщения, чтобы все другие устройства, имеющие доступ к той же учетной записи — отразили эти изменения.

Протоколы имеют свои защищенные версии — POP3S, который использует порт 995/TCP и IMAPS, который использует порт 993/TCP. И есть расширение STARTTLS, которое позволяет создать зашифрованное соединение прямо поверх обычного TCP-соединения — по аналогии с протоколом SMTP.

Существуют различные реализации IMAP и POP3, самый популярный — сервер Dovecot, который позволяет работать с обоими протоколами. В системах, основанных на Debian, функционал IMAP и POP3 предоставляются в двух разных пакетах — dovecot-imapd и dovecot-pop3d.

Порты для почтового сервера

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

Протокол SMTP

  • 25/TCP SMTP (стандартный, поддержка TLS/SSL с помощью STARTTLS)
  • 587/TCP submission (порт для почтовых агентов MUA для отправки сообщений)
  • 465/TCP SMTPS (нужна предварительная установка TLS/SSL соединения)

Протокол POP3

  • 110/TCP POP3 (стандартный, поддержка TLS/SSL с помощью STARTTLS)
  • 995/TCP POP3S (нужна предварительная установка TLS/SSL соединения)

Протокол IMAP

  • 143/TCP IMAP (стандартный, поддержка TLS/SSL с помощью STARTTLS)
  • 993/TCP IMAPS (нужна предварительная установка TLS/SSL соединения)

ДНС-записи для почтового сервера

Во-первых, добавим A-запись, которая свяжет ip-адрес почтового сервера 222.222.222.222 с доменным именем mail.example.com. Во-вторых, добавим MX-запись, которая сообщит отправителю, что письмо с адресом получателя feedback@example.com нужно отправлять серверу mail.example.com. В-третьих, добавим SPF-запись, которая сообщит получателям список наших доверенных серверов. Получатели должны принимать только письма от серверов, указанных в A-записях и MX-записях.

Name Type TTL Value
example.com A 3600 111.111.111.111
www.example.com A 3600 111.111.111.111
mail.example.com A 3600 222.222.222.222
example.com MX 10 3600 mail.example.com
example.com TXT 3600 v=spf1 a mx -all

SPF (Sender Policy Framework) позволяет владельцу домена указать, какие почтовые серверы имеют право отправлять почту от имени этого домена. Строка spf1 a mx -all указывает версию протокола SPF, разрешает отправку с ip-адреса, на который указывает A-запись домена, разрешает отправку с ip-адресов, указанных в MX-записях домена, строго запрещает отправку со всех прочих ip-адресов.

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

DMARC — техническая спецификация, созданная для борьбы с подделкой адресов почты. DMARC стандартизирует, как почтовые серверы-получатели должны обрабатывать письма, которые не прошли проверку подлинности SPF и/или DKIM, а также предоставляет механизм отчетности для доменов-отправителей. Владелец домена публикует DMARC-политику в виде TXT-записи в DNS. Эта запись сообщает принимающим серверам, что домен использует DMARC, и какие действия следует предпринять, если письмо не проходит проверку.

Установка SMTP сервера Postfix

Здесь все просто — нужно установить пакет postfix без файла конфигурации

# apt install postfix

Минимальная конфигурация Postfix

Теперь создадим файл конфигурации — в нем будет минимум опций, которые нужны для настройки безопасной конфигурации. Нужно задать свои значения опций myhostname и myorigin — чтобы указать почтовый домен, например example.com. Кроме того, на этапе настройки желательно задать значение опции mynetworks — чтобы почтовый сервер принимал почту только от одного ip-адреса. С этого ip-адреса (у меня это 123.123.123.123) будем отправлять почтовые сообщения для тестирования конфигурации.

# nano /etc.postfix/main.cf
# Какие настройки по умолчанию использовать. У разных версий Postfix разные
# настройки по умолчанию — они изменяются от версии к версии.
compatibility_level = 3.6

# Установка значения yes (вместо значения по умолчению no) приводит к тому,
# что ответы с кодами 5xx преобразуются в 4xx. Соответственно, почтовые
# серверы будут пытаться доставить сообщения на наш почтовый сервер позже. За
# это время можно проверить логи и убедиться, правильно ли работают правила.
soft_bounce = yes

# Местоположение очередей и корневой каталог демонов Postfix, которые
# запускаются в среде с измененным корневым каталогом (chroot).
queue_directory = /var/spool/postfix

# Местоположение всех утилит Postfix — postdrop, postmap, postqueue и другие.
command_directory = /usr/sbin

# Местоположение всех демонов Postfix, перечисленных в файле конфигурации
# master.cf. Эта директория должна принадлежать пользователю root.
daemon_directory = /usr/lib/postfix/sbin

# Местоположение, куда Postfix будет записывать данные в процессе работы,
# например кэши. Эта директория должна принадлежать $mail_owner, см.ниже.
data_directory = /var/lib/postfix

# Задает владельца очередей и большинства процессов демона Postfix. Нужно
# задать аккаунт в системе, который больше нигде не используется и которому
# не принадлежат другие файлы или процессы. Нельзя использовать nobody или 
# daemon.
mail_owner = postfix

# Используется для указания имени хоста почтового сервера. Типичные примеры
# имён хостов почтовых серверов — smtp.example.com или mail.example.com.
myhostname = mail.example.com

# Позволяет указать почтовый домен, обслуживанием которого занимается сервер,
# например — example.com.
mydomain = example.com

# Позволяет указать доменное имя, используемое в почтовых сообщениях,
# отправленных с сервера. Когда пользователи отправляют почту без указания
# имени домена в адресах конверта (SMTP ENVELOPE) или заголовках (MAIL
# HEADER), эта опция определяет, какое имя домена должно быть добавлено.
# По умолчанию используется значение опции $myhostname.
myorigin = $mydomain

# Задает сетевой интерфейс, который будет прослушиваться Postfix. По умолчанию
# прослушиваются все сетевые интерфейсы на сервере (значение all).
inet_interfaces = $myhostname, localhost

# Содержит список доменов, которые сервер будет считать конечными пунктами
# назначения для входящей почты. Другими словами — сервер не будет такие
# письма пересылать дальше, а будет оставлять у себя. Значение по
# умолчанию — $myhostname, localhost.$mydomain, localhost
mydestination = $mydomain, localhost

# Определяет локальных получателей, которые определены в файле паролей Linux
# и картах псевдонимов. Письма другим получателям будут отклоняться (reject).
# Сообщения предназначено локальному получателю, если доменная часть адреса
# получателя совпадает с $mydestination или $inet_interfaces + была найдена
# запись в /etc/passwd или в картах $alias_maps.
local_recipient_maps = proxy:unix:passwd.byname $alias_maps

# Код ответа сервера, если локальный получатель не найден, см. предыдущую
# опцию конфигурации. Значение по умолчанию 550, но для начальном этапе
# настройки есть смысл установить значение 450 (попробуйте снова позже).
unknown_local_recipient_reject_code = 450

# Позволяет указать, с каких ip-адресов можно отправлять почту через этот
# сервер. Обычно разрешают отправлять только из своей локальной сети.
mynetworks = 127.0.0.0/8, 123.123.123.123/32

# Опция указывает на необходимость приема почты для этих доменов несмотря на
# то, что этот сервер не является местом их конечного назначения. Самое
# безопасное значение опции — пустое.
relay_domains =

# Задает список карт псевдонимов, используемых агентом доставки почты (MDA).
# Карта позволяет перенаправить почту для каких-то получателей (даже не
# существующих) другому получателю или получателям.
alias_maps = hash:/etc/aliases

# Задает список карт псевдонимов, которые создаются командой newaliases. Это
# отдельная опция конфигурации, поскольку alias_maps может указывать на карты,
# которые не обязательно находятся под контролем Postfix.
alias_database = hash:/etc/aliases

# Вся почта будет попадать в директорию /var/mail, где имеется файл для
# каждого пользователя. Если в конце добавить слэш — то сообщения будут
# сохраняться в виде отдельных файлов внутри /var/mail/username.
mail_spool_directory = /var/mail/

# Задает имя файла для хранения сообщений в домашних директориях пользователей.
# Можно задать значение опции в виде Maildir/ — в этом случае сообщения будут
# сохраняться в виде отдельных файлов в директории /home/username/Maildir.
#home_mailbox = Postbox
#home_mailbox = Postdir/

# Использовать для локальной доставки почты указанного агента доставки почты.
#mailbox_command = /usr/bin/procmail

# Позволяет задать ответ, который возвращает сервер при подключении клиентов.
# Лучше всего установить значение, чтобы оно не указывало — какой почтовый
# сервер используется. В начале должно быть $myhostname — это требование RFC.
smtpd_banner = $myhostname ESMTP $mail_name

# Задает путь к утилите sendmail, которая входит в поставку Postfix и
# предоставляет совместимый с Sendmail интерфейс для приложений, способных
# только вызывать /usr/sbin/sendmail.
sendmail_path = /usr/sbin/sendmail

# Задает путь к команде newaliases, которая входит в поставку Postfix и
# предназначенной для создания карты алиасов. Команда newaliases является
# символической ссылкой на Postfix утилиту sendmail.
newaliases_path = /usr/bin/newaliases

# Задает путь к команде mailq, которая входит в поставку Postfix и
# предназначена для просмотра очередей. Команда mailq является символической
# ссылкой на Postfix утилиту sendmail.
mailq_path = /usr/bin/mailq

# Задает имя группы, которая используется для выполнения команд отправки почты
# и управления очередями. Это должна быть группа, которая не используется
# другими учетными записями, даже учетной записью Postfix.
setgid_group = postdrop

# Задает версию ip-протокола, которую будет использовать сервер при
# установлении соединений. По умолчанию имеет значение all.
inet_protocols = ipv4

# Добавлять важные заголовки, если их нет — например Date или Message-ID
always_add_missing_headers = yes

# Максимальное время для установки соединения с удалённым SMTP-сервером
smtp_connect_timeout = 20s
# Mакс.время ожидания ответа от удалённого SMTP-сервера на команду HELO/EHLO
smtp_helo_timeout = 10s

# ОГРАНИЧЕНИЯ НА ПРИЕМ ПОЧТЫ ЭТИМ ПОЧТОВЫМ СЕРВЕРОМ

smtpd_helo_required = yes
strict_rfc821_envelopes = yes
disable_vrfy_command = yes

# Не принимать почту на адреса, для которых у нас нет почтовых ящиков.
# Вместо этой опции можно использовать правило reject_unlisted_recipient
# в триггере smtpd_recipient_restrictions. В любом случае нужно создать
# список почтовых адресов получателей, которые нужно принимать.
#smtpd_reject_unlisted_recipient = yes

# Ограничения на этапе установке подключения
smtpd_client_restrictions =
    # Разрешить клиентов, прошедших аутентификацию по RFC 4954
    permit_sasl_authenticated
    # Разрешить клиентов из доверенных сетей (опция $mynetworks)
    permit_mynetworks
    # Отклонять клиентов, у которых доменное имя из PTR-записи
    # не разрешается в тот же ip-адрес по A-записи
    reject_unknown_client_hostname

# Ограничения на этапе команды HELO/EHLO
smtpd_helo_restrictions =
    # Отклонять клиентов, использующих неправильный синтаксис домена
    reject_invalid_helo_hostname
    # Отклонять клиентов, указывающих не полное доменное имя FQDN
    reject_non_fqdn_helo_hostname
    # Разрешить клиентов, прошедших аутентификацию по RFC 4954
    permit_sasl_authenticated
    # Разрешить клиентов из доверенных сетей (опция $mynetworks)
    permit_mynetworks
    # Отклонять клиентов, ДНС-имя которых из команды HELO/EHLO не
    # имеет A- или MX-записи или ip-адрес не является правильным
    reject_unknown_helo_hostname

# Ограничения на этапе команды MAIL FROM
smtpd_sender_restrictions =
    # Отклонять почту от отправителей с неполным доменным именем
    reject_non_fqdn_sender
    # Отклонять почту от отправителей из несуществующих доменов
    reject_unknown_sender_domain
    # Разрешить клиентов, прошедших аутентификацию по RFC 4954
    permit_sasl_authenticated
    # Разрешить клиентов из доверенных сетей (опция $mynetworks)
    permit_mynetworks
    # Запросить сервер, обслуживающий указанный адрес отправителя,
    # на предмет существования на нём пользователя с этим адресом
    reject_unverified_sender

# Ограничения на этапе команды RCPT TO (relay)
smtpd_relay_restrictions =
    # Разрешить клиентов, прошедших аутентификацию по RFC 4954
    permit_sasl_authenticated
    # Разрешить клиентов из доверенных сетей (опция $mynetworks)
    permit_mynetworks
    # Отклонять почту, если домена получателя нет в $mydestination,
    # $virtual_alias_domains, $virtual_mailbox_domains, $relay_domains
    reject_unauth_destination
    # Отклонять почту для получателя, если наш сервер не является
    # конечной точкой, а домен получателя не имеет A- и MX-записей
    reject_unknown_recipient_domain

# Ограничения на этапе команды RCPT TO (spam)
smtpd_recipient_restrictions =
    # Отклонять почту для получателей с неполным доменным именем
    reject_non_fqdn_recipient
    # Всегда принимать почту для учетных записей postmaster и abuse
    check_recipient_access hash:/etc/postfix/permit_recipients
    # Разрешить клиентов, прошедших аутентификацию по RFC 4954
    permit_sasl_authenticated
    # Разрешить клиентов из доверенных сетей (опция $mynetworks)
    permit_mynetworks
    # Отклонять почту на адреса, для которых нет почтовых ящиков
    reject_unlisted_recipient

# Ограничения на команду ETRN от почтовых серверов
smtpd_etrn_restrictions = reject

# Задержка в 1 сек между отправкой писем. Наш сервер
# не сможет отправить больше 60 писем за минуту.
smtp_transport_rate_delay = 1s

# ОГРАНИЧЕНИЯ НА ПРИЁМ СООБЩЕНИЙ ОТ SMTP-КЛИЕНТОВ

# Максимальное количество получателей сообщения, то
# есть количество записей RCPT: (по умолчанию 1000)
smtpd_recipient_limit = 100

# Период времени, на протяжении которого действуют
# ограничения для клиента, перечисленые ниже
anvil_rate_time_unit = 60s

# клиент может отправить 30 сообщений за 60 секунд
smtpd_client_message_rate_limit = 30
# не больше 30 получателей сообщений за 60 секунд
smtpd_client_recipient_rate_limit = 30
# количество одновременно разрешенных подключений
smtpd_client_connection_count_limit = 10
# количество разрешенных подключений за 60 секунд
smtpd_client_connection_rate_limit = 30

# Клиенты, на которых не действуют ограничения. По
# умолчанию $mynetworks, но у нас исключений нет
smtpd_client_event_limit_exceptions =

# Максимальное кол-во попыток аутентификации в минуту
# с одного ip-адреса (для защиты от подбора паролей)
smtpd_client_auth_rate_limit = 1

# ОГРАНИЧЕНИЯ НА РАЗМЕР ЯЩИКА И РАЗМЕР СООБЩЕНИЯ

# ограничение на размер почтового ящика 500 Мбайт
mailbox_size_limit = 524288000
virtual_mailbox_limit = 524288000
# макс. размер входящего и исходящего сообщения 15 Мб;
# реальный размер сообщения будет около 10 Мб, потому
# что кодирование бинарных данных увеличивает размер
message_size_limit = 15728640

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

По умолчанию вся почта для пользователя username сохраняется в файл /var/mail/username. Но мы в настройках Postfix указали сохранять почту в виде отдельных файлов в директории /var/mail/username. Давайте создадим эти директории для каждого пользователя системы. Возможно, есть способ при создании пользователя автоматически создавать директорию, но в файле конфигурации команды useradd есть только настройка для создания файла /var/mail/username.

# mkdir /var/mail/evgeniy
# chown evgeniy /var/mail/evgeniy
# mkdir /var/mail/sergey
# chown sergey /var/mail/sergey
Наиболее распространены два формата почтовых ящиков — mbox (все сообщения хранятся в одном файле и отделены пустой строкой) и maildir (каждое сообщение хранится в отдельном файле).

Чтобы соответствовать требованиям RFC — почтовый сервер должен всегда принимать сообщения для пользователей abuse и postmaster. В нашем файле конфигурации есть правило check_recipient_access — но мы еще не создали карту доступа для этих пользователей.

# nano /etc/postfix/permit_recipients
# карта доступа, которая разрешает принимать почту для abuse и
# postmaster, чтобы почтовый сервер отвечал требованиям RFC
postmaster@example.com    PERMIT
abuse@example.com         PERMIT
# postmap hash:/etc/postfix/permit_recipients

В результате будет создана индексированная карта /etc/postfix/permit_recipients.db, но в файле конфигурации мы ссылаемся на эту карту как hash:/etc/postfix/permit_recipients — с префиксом hash и без расширения .db.

Это все хорошо, но у нас на сервере нет пользователей abuse и postmaster — это значит, что почту для них должен получать кто-то другой. Скорее всего, это будет администратор сервера — который заодно будет получать почту для пользователя root.

# nano /etc/aliases
# почта для root, abuse и postmaster будет направлена
# в почтовый ящик администратора сервера evgeniy
root:          evgeniy
abuse:         evgeniy
postmaster:    evgeniy
# postalias hash:/etc/aliases # создание индексированной карты
Поскольку мы добавили abuse и postmaster в файл /etc/aliases — нет необходимости в использовании check_recipient_access. Единственное ограничение, которое могло бы отклонить сообщение для abuse и postmaster — это reject_unlisted_recipient. Но это ограничение не сработает, потому что при проверке карты поиска /etc/aliases.db будут найдены записи abuse и postmaster.

Теперь все готово, запускаем почтовый сервер

# systemctl start postfix.service
Правила permit_mynetworks и permit_sasl_authenticated потенциально опасные, поскольку позволяют отправлять почту «своим» SMTP-клиентам. Если была утечка пароля или компьютер пользователя заражен вирусом — возможна рассылка спама с нашего почтового сервера. Эти два правила лучше удалить из конфигурации после запуска демонов submission(s) — «свои» почтовые клиенты должны отправлять почту через них. Правила permit_mynetworks и permit_sasl_authenticated должны действовать только для демонов submission(s), но не для демона smtpd на 25-ом порту.

Отправка тестового сообщения

Отправлять будем с ip-адресов 123.123.123.123 (доверенная сеть) и 101.101.101.101 (чужая сеть). Получателем одного сообщения будет evgeniy@example.com, получателем другого сообщения будет somebody@mail.ru.

Правило reject_unauth_destination предписывает отбрасывать сообщение, если наш сервер не является местом назначения. Правила permit_sasl_authenticated и permit_mynetworks, расположенные выше — разрешают принимать такие сообщения. Эти два правила срабатывают только для «своих» клиентов, которые находятся в доверенной сети или прошли аутентификацию.

Отправим сообщение с ip-адреса 123.123.123.123 «своему» получателю evgeniy@example.com — и посмотрим, что ответит сервер

$ telnet mail.example.com 25
Trying 222.222.222.222...
Connected to mail.example.com.
Escape character is '^]'.
220 mail.example.com ESMTP Postfix
HELO [123.123.123.123]
250 mail.example.com
MAIL FROM: <sergey@example.com>
250 2.1.0 Ok
RCPT TO: <evgeniy@example.com>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Hello, Evgeniy!
.
250 2.0.0 Ok: queued as 4497C20078 # сообщение принято и поставлено в очередь
QUIT
221 2.0.0 Bye

Отправим сообщение с ip-адреса 123.123.123.123 «чужому» получателю somebody@mail.ru — и посмотрим, что ответит сервер

$ telnet mail.example.com 25
Trying 222.222.222.222...
Connected to mail.example.com.
Escape character is '^]'.
220 mail.example.com ESMTP Postfix
HELO [123.123.123.123]
250 mail.example.com
MAIL FROM: <sergey@example.com>
250 2.1.0 Ok
RCPT TO: <somebody@mail.ru>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Hello, Somebody!
.
250 2.0.0 Ok: queued as 2AB80213C8 # сообщение принято и поставлено в очередь
QUIT
221 2.0.0 Bye

Отправим сообщение с ip-адреса 101.101.101.101 «своему» получателю evgeniy@example.com — и посмотрим, что ответит сервер

$ telnet mail.example.com 25
Trying 222.222.222.222...
Connected to mail.example.com.
Escape character is '^]'.
220 mail.example.com ESMTP Postfix
HELO [101.101.101.101]
250 mail.example.com
MAIL FROM: <sergey@example.com>
250 2.1.0 Ok
RCPT TO: <evgeniy@example.com>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Hello, Evgeniy!
.
250 2.0.0 Ok: queued as 4497C20078 # сообщение принято и поставлено в очередь
QUIT
221 2.0.0 Bye

Отправим сообщение с ip-адреса 101.101.101.101 «чужому» получателю somebody@mail.ru — и посмотрим, что ответит сервер

$ telnet mail.example.com 25
Trying 222.222.222.222...
Connected to mail.example.com.
Escape character is '^]'.
220 mail.example.com ESMTP Postfix
HELO [101.101.101.101]
250 mail.example.com
MAIL FROM: <sergey@example.com>
250 2.1.0 Ok
RCPT TO: <somebody@mail.ru>
554 5.7.1 <somebody@mail.ru>: Relay access denied
QUIT
221 2.0.0 Bye

Давайте временно разрешим пересылку почты для домена mail.ru — для этого изменим значение опции relay_domains

# nano /etc/postfix/main.cf
# Опция указывает на необходимость приема почты для этих доменов, несмотря
# на то, что этот сервер не является местом их конечного назначения. Самое
# безопасное значение опции — пустое.
relay_domains = mail.ru
# systemctl reload postfix.service  # перечитать файлы конфигурации

Еще раз пробуем отправить сообщение «чужому» получателю somebody@mail.ru — теперь наш сервер принял сообщение

$ telnet mail.example.com 25
Trying 222.222.222.222...
Connected to mail.example.com.
Escape character is '^]'.
220 mail.example.com ESMTP Postfix
HELO [101.101.101.101]
250 mail.example.com
MAIL FROM: <sergey@example.com>
250 2.1.0 Ok
RCPT TO: <somebody@mail.ru>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Hello, Somebody!
.
250 2.0.0 Ok: queued as D3DA0213BE # сообщение принято и поставлено в очередь
QUIT
221 2.0.0 Bye

Обязательно возвращаем в исходное состояние файл конфигурации Postfix — потому что сейчас настройки небезопасные.

# nano /etc/postfix/main.cf
# Опция указывает на необходимость приема почты для этих доменов, несмотря
# на то, что этот сервер не является местом их конечного назначения. Самое
# безопасное значение опции — пустое.
relay_domains =
# systemctl reload postfix.service  # перечитать файлы конфигурации

Утилита управления postconf

Утилита предназначена для просмотра и изменения настроек сервера Postfix.

# postconf -p # значения всех опций настройки почтового сервера
# postconf -d опция_настройки # значение опции настройки по умолчанию
# postconf -n опция_настройки # значение опции настройки в файле конфигурации
# postconf -x опция_настройки # значение опции после подстановки переменных
# postconf -e опция=значение # установка значения в файле конфигурации

Примеры использования утилиты

# postconf -d mydestination
mydestination = $myhostname, localhost.$mydomain, localhost
# postconf -n mydestination
mydestination = $mydomain, localhost
# postconf -x mydestination
mydestination = example.com, localhost
# postconf -p | grep ^my
mydestination = $mydomain, localhost
mydomain = example.com
myhostname = mail.example.com
mynetworks = 127.0.0.0/8, 123.123.123.123/32
mynetworks_style = ${{$compatibility_level} <level {2} ? {subnet} : {host}}
myorigin = $mydomain
# postconf -px | grep ^my
mydestination = example.com, localhost
mydomain = example.com
myhostname = mail.example.com
mynetworks = 127.0.0.0/8, 123.123.123.123/32
mynetworks_style = host
myorigin = example.com

Утилита sendmail

Утилита sendmail входит в поставку Postfix и предоставляет совместимый с Sendmail интерфейс для приложений, способных только вызывать /usr/sbin/sendmail.

# sendmail [опции] адрес_получателя

После указания адреса получателя можно вводить текст письма. Для завершения ввода текста предназначена комбинация клавиш Ctrl+D в начале новой строки. Опция -c — адрес копии, опция -c — адрес скрытой копии, опция -F — имя отправителя, опция -f — адрес отправителя, опция -v — подробный режим, опция -d — режим отладки, опция -t — получить заголовки из тела сообщения.

# sendmail -F evgeniy -f evgeniy@example.com sergey@example.com
Hello, Sergey!
Ctrl+D

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

# sendmail -F evgeniy -f evgeniy@example.com sergey@example.com
Hello, Sergey!
.

Пример автоматического получения заголовков сообщения из тела сообщения при использовании опции -t

# sendmail -t << EOF
From: evgeniy@example.com
To: sergey@example.com
Subject: Hello from Evgeniy

Hello, Sergey!
EOF

Посмотрим на это сообщение в почтовом ящике пользователя sergey — все заголовки были добавлены

# cat /var/mail/sergey/new/1745653143.Vfd01I212d9M63609.2283199-mail
Return-Path: <root@example.com>
X-Original-To: sergey@example.com
Delivered-To: sergey@example.com
Received: by mail.example.com (Postfix, from userid 0)
    id 0D9A8212DF; Sat, 26 Apr 2025 10:39:03 +0300 (MSK)
To: sergey@example.com
From: evgeniy@example.com
Subject: Hello from Evgeniy
Message-Id: <20250426073903.0D9A8212DF@mail.example.com>
Date: Sat, 26 Apr 2025 10:39:03 +0300 (MSK)

Hello, Sergey!

Коды ответов Postfix

Коды ответов определены в стандарте SMTP (RFC 5321), каждый код состоит из трёх цифр. Кроме того, существуют расширенные коды статуса (RFC 3463), которые идут после основного кода.

250 2.1.0 Ok
│   │ │ │ └─ Текстовое описание для человека
│   │ │ └─── Детальный код (третья цифра)
│   │ └───── Категория (вторая цифра)
│   └─────── Класс (первая цифра, дублирует основной код)
└─────────── Основной код SMTP (RFC 5321)

Примеры кодов ответа Postfix

# Успешные операции
220 mail.example.com ESMTP Postfix          # Приветствие
250 2.1.0 Ok                                # Команда выполнена
221 2.0.0 Bye                               # Закрытие соединения

# Промежуточные
354 End data with <CR><LF>.<CR><LF>         # Готов принять данные

# Временные ошибки
421 4.3.0 Server too busy                   # Сервер перегружен
450 4.1.1 Mailbox unavailable               # Ящик временно недоступен
451 4.3.0 Internal error                    # Внутренняя ошибка
452 4.3.1 Insufficient storage              # Недостаточно места

# Постоянные ошибки
500 5.5.1 Command unrecognized              # Неизвестная команда
550 5.1.1 User unknown                      # Пользователь не найден
552 5.2.3 Message size exceeds limit        # Превышен размер
554 5.7.1 Relay access denied               # Релей запрещен

Директива конфигурации soft_bounce позволяет заменить все коды 5xx на 4xx — это полезно при отладке.

# Все коды 5xx заменяются на 4xx
soft_bounce = yes
# Нормальный режим (по умолчанию)
soft_bounce = no

Упрощенная логика в исходном коде Postfix

if (user_not_found) {
    if (soft_bounce == yes) {
        return "450 4.1.1 User unknown";  // временная
    } else {
        return "550 5.1.1 User unknown";  // постоянная
    }
}

Директивы конфигурации, которые задают коды ответов

# Проблемы с хостами
unknown_client_reject_code = 450
unknown_hostname_reject_code = 450
unknown_helo_hostname_tempfail_code = 450
invalid_hostname_reject_code = 501

# Проблемы с форматом
non_fqdn_reject_code = 504

# Relay ограничение
relay_domains_reject_code = 554

# Неизвестные получатели
unknown_local_recipient_reject_code = 550
unknown_relay_recipient_reject_code = 550
unknown_virtual_alias_reject_code = 550
unknown_virtual_mailbox_reject_code = 550

# Проверка адресов
unknown_address_reject_code = 450
unknown_address_tempfail_code = 450
unverified_recipient_reject_code = 450
unverified_recipient_defer_code = 450
unverified_recipient_tempfail_code = 450
unverified_sender_reject_code = 450
unverified_sender_defer_code = 450
unverified_sender_tempfail_code = 450

# Коды smtpd_xxxxx_restrictions
reject_unknown_client_hostname_code = 450
reject_unknown_helo_hostname_code = 450
reject_unknown_sender_domain_code = 450
reject_unknown_recipient_domain_code = 450
reject_unauth_destination_code = 554
reject_unauth_pipelining_code = 554

Основные демоны Postfix

Архитектура Postfix является модульной и содержит различные демоны, каждый из которых решает свою узкую задачу. Всех демонов мы рассмотрим позже, а сейчас перечислим только основных.

  • master — главный демон, который запускает и управляет всеми остальными демонами
  • smtpd — демон SMTP-сервера, обрабатывающий входящие соединения для получения писем с другого почтового сервера
  • smtp — демон SMTP-клиента, обрабатывающий исходящие соединения для отправки писем на другой почтовый сервер
  • local — локальный агент доставки, отвечающий за доставку сообщений в почтовые ящики системных пользователей
  • virtual — локальный агент доставки, отвечающий за доставку сообщений в почтовые ящики виртуальных пользователей
  • qmgr — менеджер очереди, обрабатывает и контролирует все сообщения в почтовой очереди

Почтовые пользователи могут быть пользователями системы (есть в файле /etc/passwd), либо виртуальными пользователями. Виртуальные пользователи не имеют учетной записи, почту для них получает специальный пользователь системы, которого нужно предварительно создать.

Виртуальные домены и ящики

Более подробно рассмотрим эту тему ниже, а сейчас только в общих чертах, поскольку «виртуальные пользователи» будут упоминаться постоянно. Кроме классического варианта настройки почтового сервера, когда есть пользователи системы и их почтовые ящики — есть еще два.

Домены виртуальных псевдонимов — увеличивает количество доменов, для которых сервер является местом конечного назначения. Это позволяет создать дополнительные адреса почты на других доменах — и перенаправить сообщения для этих адресов существующим пользователям системы. Пользователю sergey можно направить письмо на любой из этих адресов — sergey@example.com, sergey@example.net, sergey@example.org.

myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
mydestination = $mydomain, localhost

virtual_alias_domains = example.net, example.org
virtual_alias_maps = hash:/etc/postfix/virtual_alias_map

Домены виртуальных почтовых ящиков — это почтовые ящики для пользователей, у которых нет локальной учетной записи (нет в файле /etc/passwd). Почтовые сообщения для таких пользователей — будут перенаправлены в почтовый ящик /var/vmail/domain/username. Виртуальных доменов может быть несколько — поэтому путь к ящику включает domain.

Давайте посмотрим, как можно изменить наш вариант настройки почтового сервера. Для этого канонический домен example.com делаем виртуальным — то есть, переносим из mydestination в virtual_mailbox_domains. Далее можно увеличивать количество доменов, которые обслуживает почтовый сервер — просто добавляя новые домены в virtual_mailbox_domains.

# Переносим example.com из mydestination в virtual_mailbox_domains. Сообщения
# для example.com попадают в ящики /var/vmail/example.com/username, сообщения
# для localhost попадают в ящики /var/mail/username.
myhostname = mail.example.com
mydomain = localhost
myorigin = localhost
mydestination = localhost

virtual_mailbox_domains = example.com
virtual_uid_maps = static:2000
virtual_gid_maps = static:2000
virtual_mailbox_base = /var/vmail
virtual_mailbox_maps = hash:/etc/postfix/virtual_mailbox_map
virtual_alias_maps = hash:/etc/postfix/virtual_alias_map

Аутентификация SMTP-клиентов (Cyrus SASL)

Под клиентами в данном случае подразумевается MUA — например, Thunderbird или MS Outlook. MUA не должны отправлять почту через 25-ый порт — мы во время настройки будем нарушать это правило.

SASL означает «Simple Authentication and Security Layer» (простой уровень аутентификации и безопасности). Сам по себе SASL — это не более чем список требований к механизмам и протоколам аутентификации, чтобы быть совместимыми с SASL, как описано в RFC 4422.

Многие путают SASL с одной конкретной реализацией SASL — библиотекой Cyrus SASL. Например, Dovecot тоже имеет собственную реализацию SASL, которая может (когда-нибудь) быть отделена от самого Dovecot, чтобы «конкурировать» с библиотекой Cyrus SASL в качестве альтернативной реализации.

Postfix не реализует сам SASL, а вместо этого использует существующие реализации — Cyrus SASL или Dovecot SASL. Сейчас рассмотрим первый вариант, а второй — когда доберемся до Dovecot. Cyrus и Dovecot — это IMAP/POP3 почтовые серверы, которые обеспечивают работу почтовых клиентов MUA с почтовыми ящиками на сервере. Проверить, что Postfix поддерживает Cyrus SASL и/или Dovecot SASL — можно с помощью команды.

# postconf -a
cyrus
dovecot
Поскольку мы планируем использовать Dovecot как IMAP/POP3 сервер — логично, если Postfix будет обращаться к Dovecot для аутентификации SMTP-клиентов. Просто потому, что у Dovecot есть механизм аутентификации клиентов из коробки — и не нужно ничего устанавливать дополнительно.

Запуск демона saslauthd

Первым делом нам потребуется демон saslauthd, для этого нужно установить пакет sasl2-bin.

# apt install sasl2-bin

Демон saslauthd создает сокет в своем рабочем каталоге /var/run/saslauthd. Демон smtpd нуждается в доступе к этому сокету. Если smtpd работает в среде chroot, saslauthd также должен работать в этой среде chroot. Но есть и другие службы, которые ожидают сокет saslauthd в его «обычном» месте.

Рекомендуемый способ решения этой проблемы — запустить отдельные процессы для smtpd и для других. Для этого создаем отдельный файл в директории /etc/default (подробее см. в скрипте управления демоном /etc/init.d/saslauthd).

# cp /etc/default/saslauthd /etc/default/saslauthd-smtpd
# nano /etc/default/saslauthd-smtpd
START = yes
DESC = "SASL Authentication Daemon for Postfix"
NAME = "saslauthd-smtpd" # максимум 15 символов
# указываем демону, к какому бэкенду аутентификации обращаться для проверки пароля;
# здесь возможны разные варианты, например — pam, shadow, ldap, getpwent и т.д.
MECHANISMS = "sasldb"
OPTIONS = "-c -m /var/spool/postfix/var/run/saslauthd"

Далее нужно создать директорию для сокета и установить на нее права, владельца и группу

# dpkg-statoverride --add root postfix 710 /var/spool/postfix/var/run/saslauthd

Добавляем пользователя postfix в группу sasl — для возможности чтения сокета

# usermod -a -G sasl postfix
По умолчанию в файле /etc/default/saslauthd нет директивы START=yes — так что демон будет обслуживать только запросы на аутентификацию от Postfix, используя файл /etc/default/saslauthd-smtpd. Если в дальнейшем потребуется, чтобы демон обслуживал запросы на аутентификацию от других сервисов с настройками по умолчанию — нужно просто добавить директиву START=yes в файл /etc/default/saslauthd.
# service saslauthd restart
# service saslauthd status
● saslauthd.service - LSB: saslauthd startup script
     Loaded: loaded (/etc/init.d/saslauthd; generated)
     Active: active (running) since Fri 2025-01-24 14:46:04 MSK; 3s ago
       Docs: man:systemd-sysv-generator(8)
    Process: 47924 ExecStart=/etc/init.d/saslauthd start (code=exited, status=0/SUCCESS)
      Tasks: 5 (limit: 1067)
     Memory: 2.9M
        CPU: 22ms
     CGroup: /system.slice/saslauthd.service
             ├─47944 /usr/sbin/saslauthd -a sasldb -c -m /var/spool/postfix/var/run/saslauthd -n 5
             ├─47945 /usr/sbin/saslauthd -a sasldb -c -m /var/spool/postfix/var/run/saslauthd -n 5
             ├─47946 /usr/sbin/saslauthd -a sasldb -c -m /var/spool/postfix/var/run/saslauthd -n 5
             ├─47947 /usr/sbin/saslauthd -a sasldb -c -m /var/spool/postfix/var/run/saslauthd -n 5
             └─47948 /usr/sbin/saslauthd -a sasldb -c -m /var/spool/postfix/var/run/saslauthd -n 5

У меня это работало для Ubuntu 22.04, но перестало работать для Ubuntu 24.04 — теперь нужно настроить и запустить службу saslauthd-smtpd.service, которая будет работать под управлением Systemd.

# nano /etc/default/saslauthd-smtpd # переменные окружения, доступные службе
START = yes
DESC = "SASL Authentication Daemon for Postfix"
NAME = "saslauthd-smtpd" # максимум 15 символов
# указываем демону, к какому бэкенду аутентификации обращаться для проверки пароля;
# здесь возможны разные варианты, например — pam, shadow, ldap, getpwent и т.д.
MECHANISMS = "sasldb"
MECH_OPTIONS = ""
THREADS = 5
OPTIONS = "-c -m /var/spool/postfix/var/run/saslauthd"
# mkdir -p /var/spool/postfix/var/run/saslauthd # директория сокета, чтобы Postfix имел доступ
# chmod 710 /var/spool/postfix/var/run/saslauthd # права доступа для директории сокета
# chown :sasl /var/spool/postfix/var/run/saslauthd # группа для директории сокета
# usermod -a -G sasl postfix # добавляем пользователя postfix в группу sasl
# nano /etc/systemd/system/saslauthd-smtpd.service # создаем новый юнит Systemd
[Unit]
Description=SASL Authentication Daemon for Postfix
 
[Service]
Type=forking
PIDFile=/var/spool/postfix/var/run/saslauthd/saslauthd.pid
EnvironmentFile=/etc/default/saslauthd-smtpd
ExecStart=/usr/sbin/saslauthd -a $MECHANISMS $MECH_OPTIONS $OPTIONS -n $THREADS
RuntimeDirectory=saslauthd
 
[Install]
WantedBy=multi-user.target
# systemctl daemon-reload # сообщаем Systemd о новом юните
# systemctl enable saslauthd-smtpd.service # добавляем службу в автозагрузку
# systemctl start saslauthd-smtpd.service # запускаем службу в работу
# systemctl status saslauthd-smtpd.service 
● saslauthd-smtpd.service - SASL Authentication Daemon for Postfix
     Loaded: loaded (/etc/systemd/system/saslauthd-smtpd.service; enabled; preset: enabled)
     Active: active (running) since Sat 2025-03-22 16:28:23 MSK; 5s ago
    Process: 45071 ExecStart=/usr/sbin/saslauthd -a $MECHANISMS $MECH_OPTIONS $OPTIONS -n $THREADS (code=exited, status=0/SUCCESS)
   Main PID: 45072 (saslauthd)
      Tasks: 5 (limit: 1090)
     Memory: 3.0M (peak: 3.5M)
        CPU: 20ms
     CGroup: /system.slice/saslauthd-smtpd.service
             ├─45072 /usr/sbin/saslauthd -a sasldb -c -m /var/spool/postfix/var/run/saslauthd -n 5
             ├─45073 /usr/sbin/saslauthd -a sasldb -c -m /var/spool/postfix/var/run/saslauthd -n 5
             ├─45074 /usr/sbin/saslauthd -a sasldb -c -m /var/spool/postfix/var/run/saslauthd -n 5
             ├─45075 /usr/sbin/saslauthd -a sasldb -c -m /var/spool/postfix/var/run/saslauthd -n 5
             └─45076 /usr/sbin/saslauthd -a sasldb -c -m /var/spool/postfix/var/run/saslauthd -n 5

.......... systemd[1]: Starting saslauthd-smtpd.service - SASL Authentication Daemon for Postfix...
.......... saslauthd[45072]:                 : master pid is: 45072
.......... saslauthd[45072]:                 : listening on socket: /var/spool/postfix/var/run/saslauthd/mux
.......... systemd[1]: Started saslauthd-smtpd.service - SASL Authentication Daemon for Postfix.

Конфигурация демона smtpd

Создаем файл конфигурации для демона smtpd

# nano /etc/postfix/sasl/smtpd.conf
# проверкой логина-пароля будет заниматься демон saslauthd, обращаясь к базе данных BerkeleyDB
# (файл /etc/sasldb2, пароли без шифрования), который мы уже настроили и запустили
pwcheck_method: saslauthd
# список механизмов аутентификации, которые может предложить служба, заданная в pwcheck_method;
# когда pwcheck_method имеет значение saslauthd — допускется использовать только PLAIN и LOGIN
mech_list: PLAIN LOGIN
Механизм LOGIN отправялет логин и пароль как две отдельные строки. Механизм PLAIN отправляет логин и пароль одной строкой с разделителем.

Создание базы данных клиентов

Cyrus SASL поставляется с двумя плагинами — saslpasswd2 для управления пользователями и sasldblistusers2 для получения списка всех пользователей в базе данных BerkeleyDB (файл /etc/sasldb2, пароли без шифрования).

# saslpasswd2 -c -u $(postconf -h mydomain) evgeniy
Password: qwerty
Again (for verification): qwerty
# saslpasswd2 -u $(postconf -h mydomain) sergey
Password: 123456
Again (for verification): 123456
# sasldblistusers2
evgeniy@example.com: userPassword
sergey@example.com: userPassword

Проверяем SASL аутентификацию для пользователя evgeniy

# testsaslauthd -u evgeniy -p qwerty -f /var/spool/postfix/var/run/saslauthd/mux -r $(postconf -h mydomain)
0: OK "Success."

Наконец, редактируем файл конфигурации Postfix main.cf

# nano /etc/postfix/main.cf
# АУТЕНТИФИКАЦИЯ КЛИЕНТОВ С ИСПОЛЬЗОВАНИЕМ CYRUS SASL

# Включаем аутентификацию клиентов с использованием Cyrus или Dovecot
smtpd_sasl_auth_enable = yes
# Может принимать значения cyrus или dovecot, сейчас используем cyrus
smtpd_sasl_type = cyrus

# Имя файла конфигурации демона smtpd, но без расширения .conf. Путь к
# файлу конфигурации задается опцией cyrus_sasl_config_path, см.ниже
smtpd_sasl_path = smtpd
# Путь к файлу конфигурации smtpd.conf демона smtpd (который входит в
# состав Postfix и работает под управлением демона master)
cyrus_sasl_config_path = /etc/postfix/sasl
# Добавлять доменное имя к логину клиента, который не имеет доменной
# части, то есть преобразовать «username» в «username@example.com»
smtpd_sasl_local_domain = $mydomain
# Запрещаем анонимную аутентификацию клиентов на почтовом сервере
smtpd_sasl_security_options = noanonymous

Если от клиента получен логин типа username — Postfix может его дополнить до username@example.com с помощью директивы smtpd_sasl_local_domain. Демон saslauthd тоже может дополнить логин, если его запустить с опцией -x example.com.

Для аутентификации клиента сервер Postfix отправляет Cyrus SASL область аутентификации REALM вместе с логином и паролем клиента. Простые механизмы SASL (PLAIN, LOGIN) — используют только имя пользователя (чаще всего это адрес почты). Сложные механизмы SASL (DIGEST-MD5, CRAM-MD5) предназначены обслуживать несколько независимых «областей» пользователей. В качестве области аутенификации удобно использовать домен, в нашем случае example.com.

При создании базы данных логинов-паролей клиентов с помощью утилиты saslpasswd2 — область задается при помощи опции -u. При проверке SASL аутентификации с использованием утилиты testsaslauthd — область задается при помощи опции -r.

Даем указание Postfix перечитать файлы конфигурации

# systemctl reload postfix.service

Проверка логина и отправителя

Директива smtpd_sender_login_maps используется для проверки соответствия между аутентифицированным пользователем (SASL login username) и адресом отправителя, указанным в команде MAIL FROM во время SMTP-сессии. Эта директива работает только в паре с ограничением reject_sender_login_mismatch — которое тоже нужно добавить.

# nano /etc/postfix/main.cf
# Проверка соответствия между аутентифицированным пользователем и адресом
# отправителя, указанным в команде MAIL FROM во время SMTP-сессии
smtpd_sender_login_maps = hash:/etc/postfix/sender_login_map

# Ограничения на этапе команды MAIL FROM
smtpd_sender_restrictions =
    # Отклонять почту от отправителей с неполным доменным именем
    reject_non_fqdn_sender
    # Отклонять почту от отправителей из несуществующих доменов
    reject_unknown_sender_domain
    # Разрешить клиентов, прошедших аутентификацию по RFC 4954
    permit_sasl_authenticated
    # Отклонять, если логин не соответствует отправителю в MAIL FROM
    reject_sender_login_mismatch
    # Разрешить клиентов из доверенных сетей (опция $mynetworks)
    permit_mynetworks
    # Запросить сервер, обслуживающий указанный адрес отправителя,
    # на предмет существования на нём пользователя с этим адресом
    reject_unverified_sender

Создаем карту соответствия между адресом почты отправителя и SASL-логином

# nano /etc/postfix/sender_login_map
# Адрес отправителя (MAIL FROM)    Список разрешенных SASL-логинов через запятую
evgeniy@example.com                evgeniy
sergey@example.com                 sergey
# postmap hash:/etc/postfix/sender_login_map

Отправка тестового сообщения

Нужно отправить сообщение с ip-адреса 101.101.101.101, который не входит в доверенную сеть. И сообщение должно быть отправлено получателю, для которого наш сервер не является местом назначения. Мы уже знаем, что в этом случае сработает правило reject_unauth_destination — которое отбросит такое сообщение. Но выше расположено правило permit_sasl_authenticated — которое разрешит принять сообщение от аутентифицированного пользователя.

$ echo -ne '\0evgeniy\0qwerty' | base64
AGV2Z2VuaXkAcXdlcnR5
$ telnet mail.example.com 25
Trying 222.222.222.222...
Connected to mail.example.com.
Escape character is '^]'.
220 mail.example.com ESMTP Postfix
EHLO [101.101.10.101]
250-mail.example.com
250-PIPELINING
250-SIZE 15728640
250-ETRN
250-AUTH PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING
AUTH PLAIN AGV2Z2VuaXkAcXdlcnR5
235 2.7.0 Authentication successful
MAIL FROM: <evgeniy@example.com>
250 2.1.0 Ok
RCPT TO: <somebody@mail.ru>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Hello, Somebody!
.
250 2.0.0 Ok: queued as 4150C21500
QUIT
221 2.0.0 Bye

Посмотрим, что у нас в логе почтового сервера

# cat /var/log/mail.log
... postfix/smtpd[60464]: connect from unknow [101.101.101.101]
... postfix/smtpd[60464]: 4150C21500: client=unknown[101.101.101.101], sasl_method=PLAIN,
    sasl_username=evgeniy@example.com
... postfix/cleanup[60467]: 4150C21500: message-id=<>
... postfix/qmgr[60240]: 4150C21500: from=<evgeniy@example.com>, size=201, nrcpt=1 (queue active)
... postfix/smtpd[60464]: disconnect from unknown[101.101.101.101] ehlo=1 auth=1 mail=1 rcpt=1 data=1 commands=5
... postfix/smtp[60468]: 4150C21500: to=<somebody@mail.ru>, relay=mxs.mail.ru[94.100.180.31]:25, delay=1.5, 
    delays=0.07/1/0.04/0.36, dsn=5.0.0, status=bounced (host mxs.mail.ru[94.100.180.31] said: 550 Message
    was not accepted -- invalid mailbox. Local mailbox somebody@mail.ru is unavailable: user is terminated 
    (in reply to end of DATA command))
... postfix/cleanup[60467]: B85FB21591: message-id=<20250323091924.B85FB21591@mail.example.com>
... postfix/bounce[60469]: 4150C21500: sender non-delivery notification: B85FB21591
... postfix/qmgr[60240]: B85FB21591: from=<>, size=2405, nrcpt=1 (queue active)
... 2283199-evgeniy345 postfix/qmgr[60240]: 4150C21500: removed
... postfix/local[60470]: B85FB21591: to=<evgeniy@example.com>, relay=local, delay=0.03, delays=0/0.01/0/0.02,
    dsn=2.0.0, status=sent (delivered to maildir)
... postfix/qmgr[60240]: B85FB21591: removed

Наш почтовый сервер принял сообщение и попробовал его отправить на почтовый сервер mxs.mail.ru — тот ответил отказом. Тогда Postfix создал новое почтовое сообщение для evgeniy@example.com о неудачной попытке доставки. После этого менеджер очередей postfix/qmgr удалил сообщение 4150C21500 из очереди. А вот сообщение B85FB21591 с отчетом о неудачной доставке было доставлено в ящик пользователя evgeniy. После этого менеджер очередей postfix/qmgr удалил сообщение B85FB21591 из очереди.

Использование плагина

Главный недостаток описанной выше аутентификации — логин и пароль почтовый клиент отправляет на сервер в открытом виде. Но есть альтернативный вариант — использовать плагины, которые входят в поставку Cyrus SASL. Мы будем использовать базу данных паролей BerkeleyDB — это файл /etc/sasldb2, куда мы записали логины и пароли двух пользователей.

Редактируем файл конфигурации демона smtpd

# nano /etc/postfix/sasl/smtpd.conf
pwcheck_method: auxprop
# плагин поддерживает проверку паролей, сохраненных в файле /etc/sasldb2 (BerkeleyDB, без шифрования)
auxprop_plugin: sasldb
# список механизмов аутентификации, которые может предложить плагин, заданный в опции auxprop_plugin
mech_list: PLAIN LOGIN CRAM-MD5 DIGEST-MD5

Редактируем файл конфигурации Postfix main.cf

# nano /etc/postfix/main.cf
# Запрещаем анонимную аутентификацию и использование PLAIN и LOGIN
smtpd_sasl_security_options = noanonymous, noplaintext

При использовании плагина не нужно запускать службу saslauthd — демон smtpd будет сам обращаться к плагину для проверки логина-пароля. Но демон smtpd работает в среде chroot и не может оттуда получить доступ к базе данных паролей /etc/sasldb2 — давайте изменим это.

# nano /usr/lib/postfix/configure-instance.sh
FILES="etc/localtime etc/services etc/resolv.conf etc/hosts \
    etc/host.conf etc/nsswitch.conf etc/nss_mdns.config  \
    $chroot_extra_files etc/sasldb2"

Теперь нужно перезагрузить службу Postfix (команды reload будет недостаточно)

# systemctl restart postfix.service
Мы запрещаем аутентификацию клиентов с использованием PLAIN и LOGIN, но когда мы настроим TLS/SSL шифрование — эти методы аутентификации можно будет использовать безопасно. Но в этом случае нужно для почтовых клиентов сделать обязательных использование TLS/SSL — за это отвечает опция smtpd_tls_auth_only.

Отправка тестового сообщения

При использовании механизма аутентфикации CRAM-MD5 пароль не передается по сети в открытом виде. Вместо этого сервер отправляет клиенту токен, клиент должен получить md5-сумму от токена и пароля. И отправить серверу свой логин и полученную md5-сумму. Сервер тоже вычисляет md5-сумму, используя токен и пароль клиента. Если md5-суммы совпадают — аутентификация пройдена.

  1. сервер отправляет клиенту токен base64(token)
  2. клиент вычисляет md5_hash_client = md5(token + password)
  3. клиент отправляет base64(username md5_hash_client)
  4. сервер вычисляет md5_hash_server = md5(token + password)
  5. успешно, если md5_hash_client == md5_hash_server

Нам нужно взять токен от почтового сервера и сформировать ответ для отправки на сервер. Другими словами, нам нужно два терминала — на одном будем выполнять команду telnet, на другом — запускать скрипт для формирования ответа.

$ telnet mail.example.com 25 # первый терминал
Trying 222.222.222.222...
Connected to mail.example.com.
Escape character is '^]'.
220 mail.example.com ESMTP Postfix
EHLO [101.101.10.101]
250-mail.example.com
250-PIPELINING
250-SIZE 15728640
250-ETRN
250-AUTH CRAM-MD5 DIGEST-MD5
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING
AUTH CRAM-MD5
334 PDMxMjcwMz.....5vbmxpbmU+ # сообщение от сервера
ZXZnZW5peS.....c1ZDRhMQ== # наш ответ серверу
235 2.7.0 Authentication successful
QUIT
221 2.0.0 Bye
$ /path/to/cram-md5.sh evgeniy qwerty PDMxMjcwMz.....5vbmxpbmU+ # второй терминал
ZXZnZW5peS.....c1ZDRhMQ== # наш ответ серверу

Скрипт для формирования ответа почтовому серверу

#!/bin/bash

USERNAME=$1
PASSWORD=$2
CHALLENGE=$3

CHALLENGE=$(echo $CHALLENGE | base64 -d)
HMAC_HEX=$(printf '%s' "$CHALLENGE" | openssl dgst -md5 -mac HMAC -macopt key:$PASSWORD | awk '{print $2}')
RESPONSE="$USERNAME $HMAC_HEX"
RESPONSE=$(echo -n "$RESPONSE" | base64)

echo $RESPONSE

Добавляем TLS/SSL шифрование

Чтобы защитить данные, которые клиент отправляет серверу — нужно добавить шифрование (особенно при использовании аутентификации PLAIN и LOGIN). Для этого на почтовом сервере получаем TLS сертификат и редактируем файл конфигурации main.cf.

Сертификат Let's Encrypt

Чтобы получить сертификат Let's Encrypt — устанавливаем пакет certbot (подробнее см. здесь).

# apt install certbot

Плагин standalone позволяет запустить встроенный в certbot веб-сервер, чтобы ответить на http-запросы проверки принадлежности домена. Если уже есть работающий на 80-м порту веб-сервер — то его нужно остановить на время получения сертификата. В этом помогут хуки хук pre-hook и post-hook.

Регистрируем аккаунт в Let's Encrypt

# certbot register --email somebody@yandex.ru
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.4-April-3-2024.pdf. You must agree in
order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: N
Account registered.

На первом шаге запрашивается согласие с условиями использования сервиса Let's Encrypt, с которыми можно предварительно ознакомиться по предложенному адресу. На втором шаге предлагается выразить согласие на получение новостной рассылки от разработчиков certbot.

Получаем сертификат для домена mail.example.com

# certbot certonly --standalone -d mail.example.com
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for mail.example.com

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/mail.example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/mail.example.com/privkey.pem
This certificate expires on 2025-05-04.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

Теперь в директории /etc/letsencrypt/live/mail.example.com есть четыре файла (на самом деле — символические ссылки)

  • cert.pem — собственно, сертификат для домена mail.example.com
  • chain.pem — цепочка доверия, включает корневой и промежуточный сертификаты
  • fullchain.pem — полная цепочка, включает в себя cert.pem и chain.pem
  • privkey.pem — закрытый ключ сертификата, данный файл является секретным

Файл конфигурации main.cf

Прописать в файле конфигурации приватный ключ и сертификат можно с помощью одной опции — если предварительно записать ключ и цепочку сертификатов в один файл.

# Приватный ключ + сертификат почтового сервера + сертификат вышестоящего CA,
# который подписал сертификат сервера и которому доверяют почтовые клиенты
smtpd_tls_chain_files = /etc/letsencrypt/live/mail.example.com/privkey-fullchain.pem

Но у нас будет работать регламентное задание, которое обновляет сертификаты Let's Encrypt — при этом certbot ожидает найти определенные файлы в определенном месте. А создавать отдельное регламентное задание, которое будет дополнительно объединять приватный ключ и цепочку сертификатов в один файл — не очень неудобно. Поэтому будем использовать две опции.

# TLS/SSL ПРИ ПОЛУЧЕНИИ СООБЩЕНИЙ ПО SMTP-ПРОТОКОЛУ (МЫ — SMTP-СЕРВЕР)

# Приватный ключ + сертификат почтового сервера + сертификат вышестоящего
# CA, который подписал сертификат и которому доверяют почтовые клиенты
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.com/privkey.pem
# Уровень подробности логирования при использовании TLS/SSL шифрования
smtpd_tls_loglevel = 1
# Включить информацию о протоколе и используемом шифре, а также CommonName
# клиента и эмитента в заголовок сообщения Received (полезно при отладке)
smtpd_tls_received_header = yes
# Опция предписывает Postfix сообщать удаленным SMTP-клиентам о поддержке
# STARTTLS, но не требует от клиентов использования шифрования TLS/SSL. Если
# установить значение «encrypt» — Postfix не будет принимать сообщения без
# шифрования, для публичного сервера такое поведение нежелательно (RFC 2487).
smtpd_tls_security_level = may
# Postfix поддерживает возобновление сеанса TLS RFC 5077, если  клиент SMTP
# также поддерживает RFC 5077. Это экономит ресурсы и сокращает задержку.
# Значение ноль отключает кэширование.
smtpd_tls_session_cache_timeout = 3600s
# Аутентфикация SASL возможна только при использовании TLS/SSL шифрования
smtpd_tls_auth_only = yes

Сервер Postfix может выступать как в роли SMTP-сервера, так и в роли SMTP-клиента. Рекомендуемая настройка для клиента — оставить значения по умолчанию. Для этого — закомментировать указанные ниже опции, если они присутствуют в файле конфигурации.

# TLS/SSL ПРИ ОТПРАВКЕ СООБЩЕНИЙ ПО SMTP-ПРОТОКОЛУ (МЫ — SMTP-КЛИЕНТ)

# Удалить или закомментировать, чтобы использовать значения по умолчанию
smtp_tls_cert_file =
smtp_tls_dcert_file =
smtp_tls_key_file =
smtp_tls_dkey_file =
smtp_tls_eccert_file =
smtp_tls_eckey_file =
smtp_tls_chain_files =

Значение may для опций smtpd_tls_security_level и smtp_tls_security_level означает, что шифрование TLS/SSL является оппортунистическим. Другими словами, транзакция SMTP шифруется, если другая сторона поддерживает функцию STARTTLS. В противном случае данные передаются без шифрования.

Теперь, когда все данные шифруются при передаче — можем разрешить PLAIN и LOGIN

# nano /etc/postfix/main.cf
# Запрещаем анонимную аутентификацию, разрешаем использовать PLAIN и LOGIN
smtpd_sasl_security_options = noanonymous

Даем указание Postfix перечитать файлы конфигурации

# systemctl reload postfix.service

Проверка TLS/SSL сертификата

Давайте отправим сообщение, используя утилиту openssl — как раньше использовали telnet. Отправлять будем с доверенного ip-адреса 123.123.123.123 без аутентификации — чтобы немного упростить задачу.

$ openssl s_client -connect mail.example.com:25 -starttls smtp -crlf -quiet
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = R11
verify return:1
depth=0 CN = mail.example.com
verify return:1
250 CHUNKING
helo [123.123.123.123]
250 mail.example.com
mail from: <evgeniy@example.com>
250 2.1.0 Ok
rcpt to: <somebody@mail.ru>
250 2.1.5 Ok
data
354 End data with <CR><LF>.<CR><LF>
Hello, Somebody!
.
250 2.0.0 Ok: queued as 8B6AFF60
quit
221 2.0.0 Bye

Хотя сертфикат обновляется автоматически — лучше не пускать это дело на самотек, а проверять сертификат самостоятельно. Давайте напишем скрипт, который будет проверять сертфикат и сообщать в телеграм, когда заканчивается срок действия.

#!/bin/bash
TELEGRAM_API_KEY='.........'
TELEGRAM_CHAT_ID='.........'

SERVER_HOST='mail.example.com'
# Порт для проверки SSL (465 для SMTPS, 587 для SMTP+STARTTLS)
SERVER_PORT='587'
# Для STARTTLS нужно добавить -starttls smtp к команде openssl
START_TLS=''
if [[ $SERVER_PORT == '587' ]]; then
    START_TLS='-starttls smtp'
fi

function telegram {
    curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_API_KEY/sendMessage" \
        -d chat_id="$TELEGRAM_CHAT_ID" \
        -d text="${1}" > /dev/null
}

# Получаем информацию о сертификате с помощью openssl
tmp=$(printf '' | openssl s_client -connect $SERVER_HOST:$SERVER_PORT $START_TLS -servername $SERVER_HOST 2>/dev/null)
cert_not_after=$(echo "$tmp" | openssl x509 -noout -enddate 2>/dev/null)
if [[ -z "$cert_not_after" ]]; then
    telegram "CRITICAL! Не удалось получить SSL-сертификат для ${SERVER_HOST}:${SERVER_PORT}"
    exit 1
fi

# Убираем из "notAfter=Month Day HH:MM:SS YYYY GMT" лишнее
expire_date_string=$(echo "$cert_not_after" | sed 's/notAfter=//')
if [[ -z "$expire_date_string" ]]; then
    telegram "CRITICAL! Не удалось извлечь дату истечения SSL-сертификата для ${SERVER_HOST}"
    exit 1
fi

# Преобразуем дату истечения в формат TIMESTAMP и YYYY-MM-DD
expire_timestamp=$(date --date="$expire_date_string" "+%s" 2>/dev/null)
if [[ $? -ne 0 ]]; then
    telegram "CRITICAL! Не удалось распознать дату истечения SSL-сертификата для ${SERVER_HOST}"
    exit 1
fi
expire_formatted=$(date -d "@$expire_timestamp" "+%Y-%m-%d")

# Текущая дата в формате TIMESTAMP и в формате YYYY-MM-DD
current_timestamp=$(date "+%s")
current_formatted=$(date -d "@$current_timestamp" "+%Y-%m-%d")

# Проверяем, не истек ли срок действия сертификата
if [[ "$expire_timestamp" -lt "$current_timestamp" ]]; then
    telegram "DANGER! Срок действия SSL-сертификата для ${SERVER_HOST} закончился ${expire_formatted}"
    exit 1
fi

# Количество дней, которые остались до завершения сертификата
(( diff_expire_secs = expire_timestamp - current_timestamp ))
(( diff_expire_days = diff_expire_secs / 86400 ))

if (( diff_expire_days < 10 )); then
    telegram "WARNING! До окончания SSL-сертификата для ${SERVER_HOST} осталось ${diff_expire_days} дней"
fi

# Для отладки можно вывести информацию, если все в порядке
debug="SSL-сертификат для ${SERVER_HOST} истекает ${expire_formatted}, осталось ${diff_expire_days} дней"
# echo $debug
# telegram "$debug"

exit 0

Настройка MSA (Message submission agent)

Postfix демон submission

Порт 25 предназначен только для ретрансляции почты, т.е. взаимодействия между почтовыми серверами (Mail Transport Agent, MTA). Для взаимодейтсвия с почтовыми клиентами (Mail User Agent, MUA) — нужно дать указание демону master, чтобы он запустил демона submission (MSA). Демон submission — это все тот же smtpd, только на порту 587 и запущенный с другими настройками.

# nano /etc/postfix/main.cf
# ОГРАНИЧЕНИЯ НА ПРИЕМ ПОЧТЫ ДЕМОНАМИ SUBMISSION(S)
# ОТ «СВОИХ» ПОЧТОВЫХ КЛИЕНТОВ, ПОРТ 587 ИЛИ 465

# Ограничения на этапе установке подключения
submission_client_restrictions = 
    permit_sasl_authenticated
    reject
# Ограничения на этапе команды HELO/EHLO
submission_helo_restrictions =
# Ограничения на этапе команды MAIL FROM
submission_sender_restrictions =
    reject_sender_login_mismatch
    reject_non_fqdn_sender
    reject_unknown_sender_domain
    permit_sasl_authenticated
    reject
# Ограничения на этапе команды RCPT TO (relay)
submission_relay_restrictions =
    permit_sasl_authenticated
    reject
# Ограничения на этапе команды RCPT TO (spam)
submission_recipient_restrictions =
    reject_non_fqdn_recipient
    reject_unknown_recipient_domain
    permit_sasl_authenticated
    reject_unauth_destination
    reject
# nano /etc/postfix/master.cf
# Choose one: enable submission for loopback clients only, or for any client.
#127.0.0.1:submission    inet    n    -    y    -    -    smtpd
submission    inet    n    -    y    -    -    smtpd
     -o syslog_name=postfix/submission
     -o smtpd_tls_security_level=encrypt
     -o smtpd_sasl_auth_enable=yes
     -o smtpd_tls_auth_only=yes
     -o smtpd_client_restrictions=$submission_client_restrictions
     -o smtpd_helo_restrictions=$submission_helo_restrictions
     -o smtpd_sender_restrictions=$submission_sender_restrictions
     -o smtpd_relay_restrictions=$submission_relay_restrictions
     -o smtpd_recipient_restrictions=$submission_recipient_restrictions

Даем указание Postfix перечитать файлы конфигурации

# systemctl reload postfix.service

Мы здесь переопределяем директивы конфигурации, заданые глобально в файле main.cf, используя переменные $submission_xxxxx_restrictions. Правило permit_mynetworks для ограничений smtpd_xxxxx_restrictions не используется — только аутентификация, никаких доверенных сетей.

Теперь можем убрать правила permit_sasl_authenticated и permit_mynetworks для демона smtpd на 25-ом порту, который принимает сообщения от других MTA. Другие MTA не могут быть из нашей доверенной сети и они не должны проходить процедуру аутентификации.

# nano /etc/postfix/main.cf
# ОГРАНИЧЕНИЯ НА ПРИЕМ ПОЧТЫ ОТ ДРУГИХ MTA НА 25-ОМ ПОРТУ

# Ограничения на этапе установке подключения
smtpd_client_restrictions =
    # Отклонять клиентов, у которых доменное имя из PTR-записи
    # не разрешается в тот же ip-адрес по A-записи
    reject_unknown_client_hostname

# Ограничения на этапе команды HELO/EHLO
smtpd_helo_restrictions =
    # Отклонять клиентов, использующих неправильный синтаксис домена
    reject_invalid_helo_hostname
    # Отклонять клиентов, указывающих не полное доменное имя FQDN
    reject_non_fqdn_helo_hostname
    # Отклонять клиентов, ДНС-имя которых из команды HELO/EHLO не
    # имеет A- или MX-записи или ip-адрес не является правильным
    reject_unknown_helo_hostname

# Ограничения на этапе команды MAIL FROM
smtpd_sender_restrictions =
    # Отклонять почту от отправителей с неполным доменным именем
    reject_non_fqdn_sender
    # Отклонять почту от отправителей из несуществующих доменов
    reject_unknown_sender_domain
    # Запросить сервер, обслуживающий указанный адрес отправителя,
    # на предмет существования на нём пользователя с этим адресом
    reject_unverified_sender

# Ограничения на этапе команды RCPT TO (relay)
smtpd_relay_restrictions =
    # Отклонять почту, если домена получателя нет в $mydestination,
    # $virtual_alias_domains, $virtual_mailbox_domains, $relay_domains
    reject_unauth_destination
    # Отклонять почту для получателя, если наш сервер не является
    # конечной точкой, а домен получателя не имеет A- и MX-записей
    reject_unknown_recipient_domain

# Ограничения на этапе команды RCPT TO (spam)
smtpd_recipient_restrictions =
    # Отклонять почту для получателей с неполным доменным именем
    reject_non_fqdn_recipient
    # Всегда принимать почту для учетных записей postmaster и abuse
    check_recipient_access hash:/etc/postfix/permit_recipients
    # Отклонять почту на адреса, для которых нет почтовых ящиков
    reject_unlisted_recipient

Давайте отправим сообщение с ip-адреса 123.123.123.123 на порт 587 — требуется обязательная аутентификация

$ echo -ne '\0evgeniy\0qwerty' | openssl base64
AGV2Z2VuaXkAcXdlcnR5
$ openssl s_client -connect mail.example.com:587 -starttls smtp -quiet
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = R11
verify return:1
depth=0 CN = mail.example.com
verify return:1
250 CHUNKING
ehlo [123.123.123.123]
250-mail.example.com
250-PIPELINING
250-SIZE 10240000
250-ETRN
250-AUTH PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING
auth plain AGV2Z2VuaXkAcXdlcnR5
235 2.7.0 Authentication successful
mail from: <evgeniy@example.com>
250 2.1.0 Ok
rcpt to: <somebody@mail.ru>
250 2.1.5 Ok
data
354 End data with <CR><LF>.<CR><LF>
Hello, Somebody!
.
250 2.0.0 Ok: queued as 075EF144C
quit
221 2.0.0 Bye

Теперь попробуем отправить сообщение с ip-адреса 123.123.123.123 на порт 25 — правила permit_sasl_authenticated и permit_mynetworks больше не действуют, потому что мы их удалили. Наш почтовый сервер на 25-ом порту ожидает соединений от других почтовых серверов — и применяет большой набор правил, прежде чем принять сообщение.

Первым правилом в этом наборе будет reject_unknown_client_hostname — ip-адрес клиента должен резолвится в имя через PTR-запись, а имя через A-запись должно резолвится в тот же ip-адрес.

Второе правило в этом наборе — reject_unknown_helo_hostname. Если в команде HELO/EHLO мы представились как smtp.mail.ru — то для этого доменного имени должна быть A-запись. Если в команде HELO/EHLO мы представились как [123.123.123.123] — то 123.123.123.123 должен быть синтаксически корректным ip-адресом.

Мы должны отправлять сообщение с хоста, у которого есть прямая и обратная ДНС-запись — иначе сразу получим отказ. Команда HELO/EHLO допускает любой ip-адрес или любое имя, например yandex.ru или google.com. Эти две проверки — достаточно формальные, обойти их не представляет труда.

Следующее правило будет reject_unknown_sender_domain — домен отправителя из команды MAIL FROM должен иметь MX-запись или A-запись.

Следующее правило reject_unverified_sender требует убедиться в существовании пользователя с этим адресом. Наш сервер открывает встречную SMTP-сессию, пытаясь отправить письмо по адресу отправителя.

Можно отправлять сообщения от имени существующего пользователя на популярном почтовом сервере типа mail.ru или yandex.ru. Так что эти две проверки — тоже достаточно формальные, обойти их не представляет труда.

Следующее правило reject_unauth_destination не сработает, если будем отправлять на username@example.com — это «свой» домен. Но сработает, если будем отправлять на username@gmail.com — это «чужой» домен.

Правило reject_unauth_destination — самое важное и строгое, не позволяет всем желающим отправлять сообщения через наш почтовый сервер на любые адреса. Только «свои» почтовые клиенты и только через демона submission могут отправлять сообщения на любые адреса.

Следующее правило reject_unknown_recipient_domain — домен получателя из команды RCPT TO должен иметь MX-запись или A-запись. Если будем отправлять сообщение для username@example.com — то правило не сработает, это «свой» домен. Но сработает, если домен «чужой» и ДНС-записи не найдены.

Правило reject_unlisted_recipient сработает, если будем отправлять сообщение для username@example.com, но пользователя username нет в файле /etc/passwd или в карте поиска $aliases.

Наш почтовый сервер будет принимать сообщения для «своего» домена example.com — при условии, что почтовый ящик получателя существует. И будет отвергать попытки переслать сообщение на «чужой» домен — будет срабатывать правило reject_unauth_destination. И даже если пройти аутентификацию — это не поможет, правило permit_sasl_authenticated мы удалили.

$ openssl s_client -connect mail.example.com:25 -starttls smtp -quiet
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = E6
verify return:1
depth=0 CN = mail.example.com
verify return:1
250 CHUNKING
ehlo [123.123.123.123]
250-mail.example.com
250-PIPELINING
250-SIZE 15728640
250-ETRN
250-AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING
mail from: <evgeniy@example.com>
250 2.1.0 Ok
rcpt to: <somebody@mail.ru>
454 4.7.1 <somebody@mail.ru>: Relay access denied
quit
221 2.0.0 Bye

Postfix демон submissions

Протокол SMTPS на 465-ом порту в настоящее время не используется почтовыми серверами для взаимодействия друг с другом. Но зато этот протокол часто использует служба MSA, которая взаимодействует с почтовыми клиентами MUA. В этом случае служба MSA часто называется submissions (последняя s означает SSL/TLS).

Редактируем файл конфигурации master.cf — даем указание демону master, чтобы он запустил демона submissions.

# nano /etc/postfix/master.cf
# Choose one: enable submissions for loopback clients only, or for any client.
#127.0.0.1:submissions    inet    n    -    y    -    -    smtpd
submissions    inet    n    -    y    -    -    smtpd
     -o syslog_name=postfix/submissions
     -o smtpd_tls_wrappermode=yes
     -o smtpd_sasl_auth_enable=yes
     -o smtpd_client_restrictions=$submission_client_restrictions
     -o smtpd_helo_restrictions=$submission_helo_restrictions
     -o smtpd_sender_restrictions=$submission_sender_restrictions
     -o smtpd_relay_restrictions=$submission_relay_restrictions
     -o smtpd_recipient_restrictions=$submission_recipient_restrictions

С помощью директивы smtpd_tls_wrappermode мы включаем для протокола SMTP «TLS wrapper mode» (то есть SMTPS) вместо использования расширения STARTTLS.

# systemctl reload postfix.service

Что такое transport в Postfix?

Transport в Postfix — это способ доставки почтового сообщения до получателя, то есть выбор конкретного механизма как и куда отправить письмо.

В Postfix под транспортом обычно понимают «транспортный агент передачи сообщений», то есть компонент, который занимается передачей почты дальше — другому SMTP-серверу, локальному агенту доставки (LDA), по протоколу LMTP и т.п. Основные виды транспорта перечислены ниже.

  • smtp — доставка сообщений на внешний SMTP сервер
  • local — доставка в локальный почтовый ящик
  • virtual — используется для виртуальных пользователей
  • lmtp — отправка через LMTP, наприемер к Dovecot
  • pipe — отправка сообщения сторонней программе (скрипту)
  • relay — для передачи сообщения через внешний relay-сервер
  • discard — удаление сообщения, используется для фильтрации
  • error — принудительный возврат ошибки

Транспорт для почты определяется с помощью директивы transport_maps

transport_maps = hash:/etc/postfix/transport_map

Пример создания индексированной карты транспорта

# nano /etc/postfix/transport_map
# домен или адрес почты получателя —> какой транспорт использовать для доставки сообщения

# письма для домена some-domain.com будут отправляться через указанный SMTP-сервер (relay)
some-domain.com        smtp:[smtp.other-domain.com]
# письма для домена some-domain.local будут отправляться с использованием транспорта local
some-domain.local      local
# письма для домена dovecot-one.local будут отправляться к Dovecot по LMTP через unix-сокет
dovecot-one.local      lmtp:unix:private/dovecot-lmtp
# письма для домена dovecot-two.local будут отправляться к Dovecot по LMTP по сети, порт 24
dovecot-two.local      lmtp:[127.0.0.1]:24
# использовать транспорт telegram (транспорт должен быть определен в /etc/postfix/master.cf)
alert@example.com      telegram
# postmap hash:/etc/postfix/transport_map

Когда Postfix получает письмо, он проверяет, какой транспорт применим к адресу получателя. Если находит домен или конкретный адрес в transport_maps — будет применён соответствующий транспорт. Если не находит — будет использован транспорт по умолчанию, который задается в local_transport, virtual_transport и default_transport.

# для адреса получателя, указанного в опции mydestination — будет использован транспорт local
local_transport = local:$myhostname
# для адреса получателя в virtual_mailbox_domains/virtual_alias_domains — будет использован транспорт virtual
virtual_transport = virtual
# для всех прочих адресов получателя — будет использован транспорт smtp
default_transport = smtp
# postconf -d local_transport
local_transport = local:$myhostname
# postconf -d virtual_transport
virtual_transport = virtual
# postconf -d default_transport
default_transport = smtp

Все транспорты — это демоны, которых запускает главный демон master, используя файл конфигурации /etc/postfix/master.cf. Имя демона — в первой колонке, команда на запуск — в последней колонке.

smtp      unix  -       -       y       -       -       smtp
relay     unix  -       -       y       -       -       smtp
    -o syslog_name=postfix/$service_name
    -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
error     unix  -       -       y       -       -       error
discard   unix  -       -       y       -       -       discard
local     unix  -       n       n       -       -       local
virtual   unix  -       n       n       -       -       virtual
lmtp      unix  -       -       y       -       -       lmtp

Опция mailbox_transport определяет транспорт доставки почты в почтовый ящик конкретного пользователя. Она позволяет переопределить значение local_transport и применяется на уровне каждого отдельного пользователя.

# для адреса получателя, указанного в mydestination — использовать транспорт local
local_transport = local:$myhostname
# для некоторых пользователей переопределить транспорт, заданный в local_transport
mailbox_transport_maps = hash:/etc/postfix/mailbox_transport_map

Создание индексированной карты транспорта

# nano /etc/postfix/mailbox_transport_map
# Для пользователя admin отправлять сообщения в Dovecot по протоколу LMTP через unix-сокет,
# доставкой сообщений будет заниматься Dovecot, а не локальный агент доставки Postfix local
admin    lmtp:unix:private/dovecot-lmtp
# Для пользователя alert использовать транспорт telegram вместо транспорта local (транспорт
# telegram должен быть определен файле конфигурации /etc/postfix/master.cf)
alert    telegram
# postmap hash:/etc/postfix/mailbox_transport_map

Опция mailbox_transport может быть задана глобально, переопределяя значение local_transport сразу для всех почтовых пользователей.

mailbox_transport = lmtp:unix:private/dovecot-lmtp

Мы до этого говорили только о почтовых ящиках пользователей операционной системы. Но есть вариант настройки Postfix, когда почтовые пользователи не являются пользователями Linux. И этот вариант настройки в настоящее время является основным. Для таких почтовых пользователей тоже можно переопределить транспорт по умолчанию.

                                  Приходит почтовое сообщение 
                                             ▼
                             Поиск транспорта через transport_maps
                                             ▼
                                Есть правило для домена/адреса?
                                             ▼
       Да ┌─────────────────────────────────────────────────────────────┐ Нет
          │                                                             │
  Использовать транспорт,                                  Адрес попадает в какой-либо
указанный в transport_maps                                из внутренних доменов сервера?
                                                                        │
                                  Да ┌──────────────────────────────────┴─────────────────────┐ Нет
                                     │                                                        │
          ┌──────────────────────────┼───────────────────────────┐                      Адрес попадает
mydestination (локальный)   virtual_mailbox_domains     virtual_alias_domains          в relay_domains?
          │                          │                           │                            │
      Использовать               Использовать                Использовать       Да ┌──────────┴─────────┐ Нет
    local_transport            virtual_transport           virtual_transport       │                    │
          ▼                          ▼                           ▼                 │                    │
┌─────────────────────────────────────────────────────────────────────────┐   Использовать        Использовать
│              Внутри local_transport или virtual_transport               │  relay_transport    default_transport
│                                    │                                    │  (внешний адрес)     (внешний адрес)
│                Существуют карты mailbox_transport_maps?                 │
│                                    │                                    │
│             Да ┌───────────────────┴─────────────────┐ Нет              │
│                │                                     │                  │
│ Использовать транспорт, указанный   Использовать транспорт, указанный в │
│ в mailbox_transport_maps            local_transport/virtual_transport   │
└─────────────────────────────────────────────────────────────────────────┘

Настройка для виртуальных пользователей почтового сервера

# Домены виртуальных почтовых ящиков, сообщения для почтовых пользователей на
# новых доменах example.net и example.org перенаправляются в почтовые ящики
# /var/vmail/domain/username. Мы оставляем почтовые ящики пользователей ОС
# на старом месте /var/mail/username
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
mydestination = $mydomain, localhost

virtual_mailbox_domains = example.net, example.org
virtual_uid_maps = static:2000
virtual_gid_maps = static:2000
virtual_mailbox_base = /var/vmail
virtual_mailbox_maps = hash:/etc/postfix/virtual_mailbox_map
# для адреса получателя, чей домен указан в директиве mydestination — использовать транспорт local
local_transport = local:$myhostname
# для адреса получателя в virtual_mailbox_domains/virtual_alias_domains — использовать транспорт virtual
virtual_transport = virtual
# для некоторых пользователей переопределить транспорт, заданный в local_transport или virtual_transport
mailbox_transport_maps = hash:/etc/postfix/mailbox_transport_map

Создание индексированной карты транспорта

# nano /etc/postfix/mailbox_transport_map
# Для доставки сообщений получателям evgeniy@example.com, evgeniy@example.net,
# evgeniy@example.net — использовать транспорт telegram (транспорт должен быть
# определен файле конфигурации /etc/postfix/master.cf)
evgeniy    telegram
# postmap hash:/etc/postfix/mailbox_transport_map
Postfix может сам доставлять почту для «своих» пользователей, используя транспорт local и virtual. А может отправлять их Dovecot по протоколу LMTP — по сети либо через unix-сокет. И тогда распределением сообщений по почтовым ящикам пользователей будет заниматься Dovecot — этот вариант предпочтительнее.

Отправка сообщений в Телеграм

Рассмотрим небольшой пример работы с транспортом в Postfix — все сообщения для пользователей будем отправлять в группу Телеграм.

Для начала создаем новый транспорт telegram для доставки сообщений

# nano /etc/postfix/master.cf
telegram  unix  -       n       n       -       -       pipe
       flags=RO user=nobody argv=/usr/local/bin/telegram.sh
       $sender $recipient $original_recipient

Теперь напишем bash-скрипт telegram.sh, который будет получать на stdin почтовое сообщение. В качестве параметров скрипту передаются адрес почты отправителя и адрес почты получателя.

# nano /usr/local/bin/telegram.sh
#!/bin/bash
TELEGRAM_API_KEY='.........'
TELEGRAM_CHAT_ID='.........'

function telegram {
    curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_API_KEY/sendMessage" \
        -d chat_id="$TELEGRAM_CHAT_ID" \
        -d text="${1}" > /dev/null
}

# отправитель и получатель сообщения
sender=$1
recipient=$2
original=$3
# почтовое сообщение с заголовками
message=$(cat)
# почтовое сообщение без заголовков
content=$(echo "$message" | awk 'BEGIN {content=0} /^$/ {content=1; next} content==1 {print $0}')

# отправляем в телеграм отправителя, получателя и тело сообщения
telegram "Отправитель: $sender\nПолучатель: $original\nПеренаправлено: $recipient\n\n$content"

Изменяем владельца файла скрипта и предоставляем права на выполнение

# chown nobody:nogroup /usr/local/bin/telegram.sh
# chmod 550 /usr/local/bin/telegram.sh

Редактируем файл конфигурации Postfix для добавления нового транспорта

# nano /etc/postfix/main.cf
telegram_destination_recipient_limit = 1
transport_maps = hash:/etc/postfix/transport_map

Создаем индексированную карту транспорта

# nano /etc/postfix/transport_map
example.com    telegram
# postmap hash:/etc/postfix/transport_map

Отправим сообщение root (согласно /etc/aliases получит evgeniy)

$ sendmail -f noreply@example.com root@example.com
Used space on hard disk partitions
Root 45%, Home 43%, MySQL 51%, Logs 26%, Backup 65%
Used inode on hard disk partitions
Root 37%, Home 8%, MySQL 1%, Logs 1%, Backup 7%
.

На нашем почтовом сервере есть только два администратора evgeniy и sergey — так что пересылка всех сообщений в группу Телеграм будет подходящим вариантом. Но если пользователей больше, то пересылать все подряд — будет не самым лучшим выбором. Однако, можно сделать пересылку сообщений только для некоторых пользователей — например, только для админов.

# nano /etc/postfix/transport_map
evgeniy@example.com    telegram
sergey@example.com     telegram
# postmap hash:/etc/postfix/transport_map

Не обязательно задавать транспорт для доставки в карте transport_maps. Если Postfix не найдет транспорт в этой карте — будет использован транспорт по умолчанию. А транспорт по умолчанию можно задать самостоятельно. Или использовать карту mailbox_transport_maps, чтобы переопределить транспорт по умолчанию для конкретных пользователей.

# Первый вариант настройки пересылки в телеграм (для двух пользователей)
telegram_destination_recipient_limit = 1
transport_maps = hash:/etc/postfix/transport_map
# Второй вариант настройки пересылки в телеграм (для всех пользователей)
telegram_destination_recipient_limit = 1
#transport_maps = hash:/etc/postfix/transport_map
local_transport = telegram
# Третий вариант настройки пересылки в телеграм (для всех пользователей)
telegram_destination_recipient_limit = 1
#transport_maps = hash:/etc/postfix/transport_map
#local_transport = telegram
mailbox_transport = telegram
# Четвертый вариант настройки пересылки в телеграм (для двух пользователей)
telegram_destination_recipient_limit = 1
#transport_maps = hash:/etc/postfix/transport_map
#local_transport = telegram
#mailbox_transport = telegram
mailbox_transport_maps = hash:/etc/postfix/mailbox_transport_map
# nano /etc/postfix/mailbox_transport_map
evgeniy    telegram
sergey     telegram
# postmap hash:/etc/postfix/mailbox_transport_map
Запись вида pipe:/usr/local/bin/telegram.sh, когда используется встроенный транспорт pipe, в настоящее время устарела и не рекомендуется. Транспорт должен быть явно определен в файле /etc/postfix/master.cf, как мы это делали, создавая транспорт telegram.

Что такое сервер пересылки (relay)

Relay — это почтовый сервер, который принимает сообщения от одного отправителя и передаёт (пересылает) их дальше к другому серверу. Relay-сервер действует как промежуточное звено между исходным отправителем письма и получателем, то есть не является конечным пунктом доставки сообщения.

Основные задачи relay-сервера

  • Принять письмо от почтового клиента или другого почтового сервера
  • Определить маршрут дальнейшей доставки — другой relay или на сервер домена получателя
  • Переслать сообщение на следующий сервер — другой relay или на сервер домена получателя
  • При необходимости — добавить служебные заголовки для отслеживания пути сообщения

Поведение relay-сервера настраивается через директивы relay_domains, relay_recipient_maps и политики аутентификации клиентов. Директива relay_domains определяет домены, для которых пересылка разрешена. Директива relay_recipient_maps уточняет для этих доменов, какие конкретно адреса допустимы к обработке, обеспечивая более жёсткий контроль на уровне пользователей.

Транспорт для пересылки можно задать в карте transport_maps, если подходящий для домена транспорт не будет найдет, то будет использован транспорт по умолчанию relay_transport, который мо умолчанию имеет значение relay.

Пересылка для двух почтовых ящиков

Разрешаем пересылку на почтовый сервер, который обслуживает почтовый домен example.net, но только для двух почтовых адресов.

# Почтовые домены, для которых разрешается пересылка сообщений
relay_domains = example.net
# Не обязательно, но рекомендуется для явного указания next-hop
transport_maps = hash:/etc/postfix/transport_map
# На какие адреса и на какие домены можно или нельзя пересылать,
# здесь можно настроить более точно, чем в relay_domains
relay_recipient_maps = hash:/etc/postfix/relay_recipient_map

# Ограничения на этапе команды RCPT TO (relay)
smtpd_relay_restrictions =
    # Отклонять почту, если домена получателя нет в $mydestination,
    # $virtual_alias_domains, $virtual_mailbox_domains, $relay_domains
    reject_unauth_destination
    # Отклонять почту для получателя, если наш сервер не является
    # конечной точкой, а домен получателя не имеет A- и MX-записей
    reject_unknown_recipient_domain
# nano /etc/postfix/transport_map
# Разрешается пересылка только на почтовый сервер mail.example.net
example.net    smtp:[mail.example.net]
# postmap hash:/etc/postfix/transport_map
# nano /etc/postfix/relay_recipient_map
# Разрешается пересылка только для двух почтовых ящиков
user-one@example.net    PERMIT
user-two@example.net    PERMIT
example.net             REJECT
# postmap hash:/etc/postfix/relay_recipient_map

Пересылка от одного почтового сервера

Разрешаем пересылку на почтовый сервер, который обслуживает почтовый домен example.net, но пересылка разрешается только почтовому серверу mail.example.org

# Почтовые домены, для которых разрешается пересылка сообщений
relay_domains = example.net
# Не обязательно, но рекомендуется для явного указания next-hop
transport_maps = hash:/etc/postfix/transport_map

smtpd_recipient_restrictions =
    # Отклонять почту, если домена получателя нет в $mydestination,
    # $virtual_alias_domains, $virtual_mailbox_domains, $relay_domains
    reject_unauth_destination
    # Отклонять почту, если домен получателя не имеет A- и MX-записей
    reject_unknown_recipient_domain
    # Разрешаем пересылку только для почтового сервера mail.example.org
    check_client_access hash:/etc/postfix/check_relay_access
# nano /etc/postfix/transport_map
# Разрешается пересылка только на почтовый сервер mail.example.net
example.net    smtp:[mail.example.net]
# postmap hash:/etc/postfix/transport_map
# nano /etc/postfix/check_relay_access
# Разрешается пересылка только от почтового сервера mail.example.org
mail.example.org    PERMIT
# postmap hash:/etc/postfix/check_relay_access

Защита от спама в Postfix

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

Postfix демон postscreen

Postfix демон postscreen предназначен для борьбы со спамом на раннем этапе подключения. Чтобы его активировать, нужно в файле конфигурации master.cf изменить порядок подключения клиентов к серверу. Входящее SMTP-соединение сначала передается демону postscreen и уже потом, после проверки клиента, передается демону smtpd.

# nano /etc/postfix/master.cf
# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (no)    (never) (100)
# ==========================================================================
#smtp      inet  n       -       y       -       -       smtpd
smtp      inet  n       -       y       -       1       postscreen
smtpd     pass  -       -       y       -       -       smtpd
dnsblog   unix  -       -       y       -       0       dnsblog
tlsproxy  unix  -       -       y       -       0       tlsproxy
Вспомогательные демоны dnsblog и tlsproxy делают postscreen более эффективным, потому что выполняют ресурсоемкие операции (DNS и TLS) отдельно от основной логики проверки на спам.

Директивы для управления демоном postscreen

# Включение DNSBL (черные списки)
postscreen_dnsbl_action = enforce
postscreen_dnsbl_sites = zen.spamhaus.org bl.spamcop.net
postscreen_dnsbl_threshold = 1
# Включение теста SMTP Greeting Delay
postscreen_greet_action = enforce
postscreen_greet_wait = 6s
# Сохранение истории ip-адресов
postscreen_cache_map = btree:$data_directory/postscreen_cache
postscreen_cache_cleanup_interval = 12h
postscreen_cache_retention_time = 7d
# Реакция на нарушение протокола
postscreen_pipelining_action = enforce
postscreen_non_smtp_command_action = drop
postscreen_bare_newline_action = drop

Черные списки ip-адресов DNSBL

DNSBL (Domain Name System Blacklist) — это чёрный список ip-адресов, замеченных за рассылкой спама. Когда почтовый сервер получает сообщение, он проверяет наличие ip-адреса отправителя в этом списке с помощью DNS-запроса. Если адрес присутствует, письмо можно отклонить или пометить как спам.

# Ограничения на этапе команды RCPT TO (spam)
smtpd_recipient_restrictions =
    # Отклонять почту для получателей с неполным доменным именем
    reject_non_fqdn_recipient
    # Всегда принимать почту для учетных записей postmaster и abuse
    check_recipient_access hash:/etc/postfix/permit_trusted_recipients
    # Отклонять почту на адреса, для которых нет почтовых ящиков
    reject_unlisted_recipient
    # Отклонять клиентов, которые есть в черных списках DNSBL
    reject_rbl_client zen.spamhaus.org
    reject_rbl_client bl.spamcop.net

Если уже настроен и запущен демон postscreen с проверкой по черным спискам ip-адресов — то повторная проверка не имеет смысла.

Внутренние фильтры Postfix

В Postfix двухуровневая система фильтрации, директивы smtp_xxxxx_checks — первая линия обороны, директивы xxxxxx_checks — вторая линия обороны. Директивы smtp_xxxxx_checks выполняются демоном smtpd во время SMTP-сессии, до того момента, как сообщение полностью принято. Директивы xxxxxx_check выполняются демоном cleanup после того, как сообщение принято, но до постановки в очередь.

Тут важно понимать, как сообщения попадают в Postfix. Это может быть сообщение, полученное демоном smtpd от МТА (другой почтовый сервер). Это может быть сообщение, полученое демоном submission(s) (демон smtpd с другими настройками) от MUA («свой» почтовый клиент). Кроме того, сообщение может быть создано локально без SMTP-транзакции — системным процессом или командой sendmail.

Проверять на спам нужно как сообщения, полученные от других почтовых серверов, так и сообщения, полученные от «своих» почтовых клиентов. Зачем проверять «своих»? Потому что могла быть утечка пароля или заражение вирусом компьютера пользователя. Но действия для «своих» и для «чужих» может быть разной — например, «своему» клиенту нужно поменять пароль и проверить компьютер на вирусы.

Когда демон smtpd или submission(s) принимает сообщение от SMPT-клиента, оно может быть проверено с использованием директив smtp_xxxxx_checks. После этого сообщение попадает к демону cleanup, где оно может быть проверено еще раз с использованием директив xxxxxx_checks.

Директивы smtp_xxxxx_checks используются редко, обычно для настройки достаточно использовать xxxxx_checks. Однако, smtp_xxxxx_checks предоставляют важное преимущество — возможность отклонить нежелательное письмо на самой ранней стадии, еще во время SMTP-сессии.

Внутренние фильтры и SpamAssassin

При подключении SpamAssassin через content_filter (см. ниже) — сперва сработают проверки smtp_xxxxx_checks, потом сообщение отправляется для анализа на спам, потом передаётся обратно в Postfix через утилиту sendmail (re-injection). От утилиты sendmail сообщение через pickup попадет к демону cleanup, где сработают проверки xxxxx_checks.

Директивы конфигурации xxxxx_checks, заданные глобально в файле конфигурации /etc/postfix/main.cf, будут действовать как для демона smtpd на 25-ом порту, так и для демонов submission(s). Это значит, что для «своих» и «чужих» сообщений применяется одна политика, которую мы определим в картах поиска xxxxx_checks.

Но для демонов submission(s) можно задать другие значения директив xxxxx_checks в файле конфигурации /etc/postfix/master.cf. Тогда для сообщений, принятых демоном smtpd на 25-ом порту, будут действовать глобальные значения. А для сообщений, принятых демонами submission(s), будут дейстововать локальные значения.

# Отправлять сообщения на внешний фильтр для проверки на спам
content_filter = spam_filter
# Отключаем преобразования адреса получателя, чтобы фильтр мог
# проверить оригинальное сообщение, до любых изменений.
receive_override_options = no_address_mappings

# Проверять заголовки почтовых сообщений, первая линия обороны. Выполняет демон
# smtpd во время SMTP-сессии, до того момента, как сообщение полностью принято.
smtp_header_checks =
# Проверять заголовки почтовых сообщений, вторая линия обороны. Выполняет демон
# cleanup после того, как сообщение принято, но до постановки в очередь.
header_checks = pcre:/etc/postfix/header_checks
# Проверять сообщения на спам и отправлять обратно в Postfix через sendmail
spam_filter  unix  -     n       n       -       -       pipe
     user=spamd argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -i -f $sender $recipient

# Choose one: enable submission for loopback clients only, or for any client.
#127.0.0.1:submission  inet  n -   y       -       -       smtpd
submission  inet  n       -       y       -       -       smtpd
     ..........
     -o smtp_header_checks=
     # Для «своих» почтовых клиентов, которые прошли аутентификацию
     -o header_checks=pcre:/etc/postfix/msa_header_checks

# Choose one: enable submissions for loopback clients only, or for any client.
#127.0.0.1:submissions  inet  n  -       y       -       -       smtpd
submissions  inet  n       -       y       -       -       smtpd
     ..........
     -o smtp_header_checks=
     # Для «своих» почтовых клиентов, которые прошли аутентификацию
     -o header_checks=pcre:/etc/postfix/msa_header_checks

При подключении SpamAssassin через smtpd_milters (см. ниже) — демон smtpd передает сообщение cleanup, где срабатывают проверки xxxxx_checks. После этого сообщение передается фильтрам, указанным в директиве smtpd_milters. После обработки фильтрами — сообщение возвращется демону smtpd, который помещает его в очередь incoming.

Заголовки X-Spam-Status, X-Spam-Level, X-Spam-Score, добавленные SpamAssassin, не могут быть «отловлены» в header_checks — их еще нет в момент проверки. В этом случае, отреагировать на заголовки может только сам милтер, когда получает сообщение обратно от SpamAssassin.

При этом не забываем, что демоны submission(s) — это все тот же smtpd, но на другом порту и с другими настройками. Все сказанное выше для демона smtpd — будет справедливо и для submission(s). Отреагировать на заголовки, добавленные SpamAssassin, может только сам милтер.

# Применять фильтр для сообщений, которые попадают в Postfix через демона smtpd
smtpd_milters = unix:spamass/spamass.sock
# Применять фильтр для писем, которые попадают в Postfix не через демона smtpd
non_smtpd_milters = $smtpd_milters
# Запускать от имени пользователя spamass-milter, игнорировать письма с localhost (127.0.0.1)
OPTIONS="-u spamass-milter -i 127.0.0.1"
# Отклонять сообщения (550 reject), у которых scores, присвоенный SpamAssassin, больше 15
OPTIONS="${OPTIONS} -r 15"

Директивы фильтрации xxxxxx_checks

Каждая директива отвечает за свою часть почтового сообщения

  • header_checks — проверки применяются к заголовкам сообщения, т.е. от первой строки сообщения до первой пустой строки
  • body_checks — проверки применяются к телу сообщения; телом считается все, что находится между заголовками
  • mime_header_checks — проверки применяются к MIME-заголовкам верхнего уровня и к MIME-заголовкам вложенных сообщений
  • nested_header_checks — проверки применяются к заголовкам вложенных сообщений message/rfc822, за исключением MIME-заголовков
# Проверять заголовки почтовых сообщений на совпадение с
# шаблоном и выполнять указанное действие при совпадении
header_checks = pcre:/etc/postfix/incoming_header_checks
/шаблон/модификаторы    ДЕЙСТВИЕ [опциональный текст]
/шаблон/модификаторы    ДЕЙСТВИЕ [опциональный текст]
/шаблон/модификаторы    ДЕЙСТВИЕ [опциональный текст]

Для каждого шаблона поиска нужно задать одно из следующих действий

  • IGNORE — удалить из сообщения строку, которая совпадает с шаблоном поиска.
  • REPLACE text — заменить в сообщении строку, которая совпадает с шаблоном поиска, на строку текст.
  • BCC user@domain — добавить в сообщение заголовок Bcc (скрытая копия) при совпадении с шаблоном.
  • REJECT [текст] — не принимать сообщение, необязательный текст будет отправлен клиенту и записан в лог-файл.
  • WARN [текст] — записать в лог-файл предупреждение warn:, необязательный текст тоже будет записан в лог-файл. При этом сообщение будет доставлено без каких-либо изменений.
  • INFO [текст] — записать в лог-файл сообщение info:, необязательный текст тоже будет записан в лог-файл. При этом сообщение будет доставлено без каких-либо изменений.
  • PREPEND [текст] — добавить указанный текст перед строкой, каторая совпадает с шаблоном поиска. Как правило, используется для добавления заголовков.
  • HOLD [текст] — поместить сообщение в очередь отложенных сообщений (пока администратор не решит, что с ним делать). Если указан необязательный текст — записать его в лог-файл вместе со строкой, в которой найдено совпадение.
  • DISCARD [текст] — сообщить клиенту об успешной доставке, но молча удалить сообщение. Если указан необязательный текст — записать его в лог-файл вместе со строкой, в которой найдено совпадение.
  • FILTER transport:nexthop — отправить сообщение с использованием указанного транспорта (транспорт должен быть определен в файле конфигурации master.cf).
  • REDIRECT user@domain — перенаправить сообщение по указанному адресу вместо того, чтобы доставить его первоначальному получателю (получателям). Подменяет любое действие FILTER.

Действия REJECT, DISCARD, REDIRECT, FILTER, HOLD являются завершающими, то есть прекращают обработку сообщения. Действия WARN, INFO, IGNORE, BCC, PREPEND не являются завершающими, то есть обработка сообщения продолжается.

# Добавляем запись в лог-файл, если SpamAssassin пометил письмо как спам
/^X-Spam-Flag: YES/ WARN Spam detected
# Отправляем скрытую копию админу, если SpamAssassin пометил письмо как спам
/^X-Spam-Flag: YES/ BCC evgeniy@example.com
# Перенаправляем сообщение админу, если SpamAssassin пометил письмо как спам
/^X-Spam-Level: \*\*\*\*\*/ REDIRECT evgeniy@example.com

Пример отправки сообщения с использованием транспорта, определенного в master.cf

# Отправить сообщение с использованием транспорта, определенного в master.cf
/^X-Spam-Level: \*\*\*\*\*/ FILTER alertspam
# определяем транспорт alertspam в файле конфигурации master.cf
alertspam  unix  -       n       n       -       -       pipe
     flags=RO user=nobody argv=/usr/local/bin/alertspam.sh $sender $recipient
#!/bin/bash
TELEGRAM_API_KEY='.........'
TELEGRAM_CHAT_ID='.........'
# почтовое сообщение без заголовков
content=$(echo "$(cat)" | awk 'BEGIN {content=0} /^$/ {content=1; next} content==1 {print $0}')
# отправляем в телеграм отправителя, получателя и тело сообщения
message="SPAM MESSAGE\nОтправитель: $1\nПолучатель: $2\n\n$content"
curl -s -X POST "$TELEGRAM_API_KEY" -d chat_id="$TELEGRAM_CHAT_ID" -d text="$message" > /dev/null
# Для каждого получателя сообщения с использованием транспорта
# alertspam будет отдельный вызов скрипта alertspam.sh
alertspam_destination_recipient_limit = 1

Для поддержки карт pcre нужно установить пакет postfix-pcre

# apt install postfix-pcre

Директивы фильтрации smtp_xxxxxx_checks

Директивы smtp_xxxxx_checks используются редко, обычно для настройки достаточно использовать smtp_xxxxx_checks. Однако, smtp_xxxxx_checks предоставляют важное преимущество — возможность отклонить нежелательное письмо на самой ранней стадии, еще во время SMTP-сессии, до того, как оно будет полностью принято сервером. Это экономит ресурсы (сеть, процессор, диск) и позволяет немедленно уведомить отправителя об отказе.

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

smtp_header_checks = pcre:/ect/postfix/smtp_header_checks
/^Subject: Your account has been hacked, follow the link!/i REJECT Phishing attempt detected

Как только smtpd получает этот заголовок во время команды DATA, он немедленно прерывает сессию с ошибкой 5xx. Письмо не принимается, не записывается во временные файлы, не передается демону cleanup — это экономит ресурсы почтового сервера.

2. Блокировка по известным вредоносным X-Mailer или другим специфичным заголовкам

smtp_header_checks = pcre:/ect/postfix/smtp_header_checks
/^X-Mailer: SpamBotPro v2.1/i REJECT Known spambot client

3. Раннее обнаружение нежелательных типов вложений (.exe, .bat) по MIME-заголовкам

smtp_mime_header_checks = pcre:/ect/postfix/smtp_mime_header_checks
/^\s*Content-Type:.*name\s*=\s*"?.*\.(exe|bat|scr)"?/i REJECT Executable attachment type not allowed
/^\s*Content-Disposition:.*filename\s*=\s*"?.*\.(exe|bat|scr)"?/i REJECT Executable attachment type not allowed

4. Предотвращение некоторых видов DoS-атак через длинный заголовок темы сообщения

smtp_header_checks = pcre:/ect/postfix/smtp_header_checks
/^Subject:.{100,}/ REJECT Subject too long

Внешние фильтры Postfix

Postfix позволяет подключать внешние фильтры — SpamAssassin, ClamAV и другие. Сделать это можно двумя способами — с использованием Milter (Mail Filter API) или через директиву content_filter.

Подключение фильтра через Milter

Milter — это протокол, разработанный Sendmail и адаптированный Postfix, который позволяет внешним программам (milter-ам) взаимодействовать с Postfix на различных этапах SMTP-сессии. Postfix передает данные о соединении, отправителе, получателях и теле сообщения milter-приложению. Milter может изменять эти данные, добавлять/удалять заголовки, отклонять сообщение или временно откладывать его.

# Обычно проверки на вирусы делают раньше проверок на спам, так как зараженное
# письмо часто и так является спамом, и отклонять его лучше по причине вируса.
smtpd_milters =
    # tcp-сокет для clamav-milter
    inet:7357@localhost
    # tcp-сокет для spamass-milter
    inet:localhost:8892
# При использовании unix-сокетов — убедитесь, что Postfix имеет права доступа
# к ним. Это особенно важно, если Postfix работает в chroot (по умолчанию).
smtpd_milters =
    # unix-сокет /var/spool/postfix/clamav/milter.ctl для clamav-milter
    unix:clamav/milter.ctl
    # unix-сокет /var/spool/postfix/spamass/spamass.sock для spamass-milter
    unix:spamass/spamass.sock
# Milter-ы для сообщений, не проходящих через smtpd (например, через sendmail)
non_smtpd_milters = $smtpd_milters

# Действие по умолчанию, если milter временно недоступен или вернул ошибку
milter_default_action = tempfail

# Таймауты для внешних фильтров в секундах (это не обязательно, но желательно)
milter_connect_timeout = 30s
milter_command_timeout = 30s
milter_content_timeout = 300s

# Версия протокола Milter, 6-я версия рекомендуется для современных фильтров
milter_protocol = 6

Postfix поддерживает два формата для указания TCP-сокета — inet:host:port и inet:port@host, но лучше использовать какой-то один формат.

Подключение через content_filter

При использовании директивы content_filter — Postfix принимает все письмо целиком (после команды DATA и точки). Затем передает сообщение внешнему фильтру через один из поддерживаемых механизмов (по SMTP-протоколу или через pipe). Внешний фильтр обрабатывает сообщение, а затем возвращает его обратно в Postfix (по SMTP-протоколу или через pipe) для дальнейшей доставки или отклонения. Главный недостаток — можно указать только одно значение для директивы content_filter (хотя есть обходные пути).

Подключение SpamAssassin через content_filter

# Отправлять сообщения на внешний фильтр для проверки на спам
content_filter = spam_filter
# Отключаем преобразования адреса получателя, чтобы фильтр мог
# проверить оригинальное сообщение, до любых изменений.
receive_override_options = no_address_mappings
spam_filter  unix  -     n       n       -       -       pipe
     user=spamd argv=/usr/bin/spamc -e /usr/sbin/sendmail -i -f $sender $recipient

Подключение ClamAV через content_filter

# Отправлять сообщения на внешний фильтр для проверки на вирус
content_filter = virus_filter
# Отключаем преобразования адреса получателя, чтобы фильтр мог
# проверить оригинальное сообщение, до любых изменений.
receive_override_options = no_address_mappings
virus_filter  unix  -     n       n       -       -       pipe
     user=filter argv=/usr/local/bin/virus-filter.sh $sender $recipient

Скрипт virus-filter.sh получает сообщение на stdin и проверяет на наличие вируса. Если вирус не обнаружен — возвращает сообщение в Postfix через утилиту sendmail. Если обнаружен вирус — демон smtpd отправит удаленному SMTP-клиенту (отправителю) постоянную ошибку (5xx). Если была ошибка сканирования — демон smtpd отправит удаленному SMTP-клиенту временную ошибку (4xx).

Что такое Milter в Postfix

Milter — это протокол, разработанный Sendmail и адаптированный Postfix, который позволяет внешним программам (milter-ам) взаимодействовать с Postfix на различных этапах SMTP-сессии. Postfix передает данные о соединении, отправителе, получателях и теле сообщения milter-приложению. Milter может изменять эти данные, добавлять/удалять заголовки, отклонять сообщение или временно откладывать его.

Postfix и Milter общаются через tcp-сокет или unix-сокет. Milter может вмешиваться на этапах CONNECT, HELO, MAIL FROM, RCPT TO, DATA, END-OF-MESSAGE. Данные передаются последовательно по мере поступления, а не все сообщение целиком. Решение об отклонении может быть принято до полного получения сообщения. Postfix может использовать цепочку из нескольких milter-ов, каждый из которых выполняет свою задачу. Порядок milter-ов в smtpd_milters важен, так как они обрабатывают сообщение последовательно.

# Обычно проверки на вирусы делают раньше проверок на спам, так как зараженное
# письмо часто и так является спамом, и отклонять его лучше по причине вируса.
smtpd_milters =
    # tcp-сокет для clamav-milter
    inet:7357@localhost
    # tcp-сокет для spamass-milter
    inet:localhost:8892
# При использовании unix-сокетов — убедитесь, что Postfix имеет права доступа
# к ним. Это особенно важно, если Postfix работает в chroot (по умолчанию).
smtpd_milters =
    # unix-сокет /var/spool/postfix/clamav/milter.ctl для clamav-milter
    unix:clamav/milter.ctl
    # unix-сокет /var/spool/postfix/spamass/spamass.sock для spamass-milter
    unix:spamass/spamass.sock
# Milter-ы для сообщений, не проходящих через smtpd (например, через sendmail)
non_smtpd_milters = $smtpd_milters

# Действие по умолчанию, если milter временно недоступен или вернул ошибку
milter_default_action = tempfail

# Таймауты для внешних фильтров в секундах (это не обязательно, но желательно)
milter_connect_timeout = 30s
milter_command_timeout = 30s
milter_content_timeout = 300s

# Версия протокола Milter, 6-я версия рекомендуется для современных фильтров
milter_protocol = 6

Директива milter_default_action может принимать значения tempfail (временное отклонение, попробовать позже), accept (принять, как будто фильтр одобрил сообщение), reject (отклонить, как будто фильтр не одобрил сообщение).

Таймауты milter_connect_timeout, milter_command_timeout, milter_content_timeout важны для предотвращения зависания Postfix в ожидании ответа от медленного или неисправного фильтра.

Директива non_smtpd_milters используется для применения фильтров к почтовым сообщениям, которые поступают не через стандартный SMTP-протокол, обрабатываемый демоном smtpd. То есть, это сообщения, которые создаются локально на сервере или отправляются через утилиту sendmail. В этом случае письмо подхватывается демоном pickup, который затем передает его демону cleanup. Именно cleanup передает сообщение на обработку в фильтры, заданные в директиве non_smtpd_milters.

Когда демоны smtpd и cleanup запускаются в chroot-окружении, их файловая система «запирается» внутри /var/spool/postfix. Если unix-сокет Milter-приложения находится вне этой chroot-директории, то Postfix демоны не смогут его найти и подключиться.

Postfix предоставляет механизм, который позволяет «пробросить» unix-сокеты внутрь chroot-окружения. Демон master Postfix (который сам не работает в chroot) может создать копию или специальную точку доступа к unix-сокету внутри chroot-директории перед запуском chroot-демона.

cleanup   unix  n       -       n       -       0       cleanup
     # проброс unix-сокета opendkim внутрь chroot для демона cleanup
     -o unix:private/opendkim_chroot_sock=unix:/var/run/opendkim/opendkim.sock
# обращение к фильтру opendkim через «проброшенный» unix-сокет
non_smtpd_milters = unix:private/opendkim_chroot_sock

Важно проверить, что права доступа к оригинальному unix-сокету и к «проброшенному» сокету внутри chroot корректно настроены, чтобы Postfix демоны (работающие под пользователем postfix) могли к ним подключаться.

SpamAssassin для защиты от спама

Postfix будет передавать каждое полученное сообщение SpamAssassin для анализа. SpamAssassin пометит письмо (добавит заголовки, изменит тему) и вернет его обратно Postfix для дальнейшей доставки. Подключить SpamAssassin можно с помощью директивы content_filter, либо с использованием Milter.

Подключение SpamAssassin через content_filter

Нужно установить демона spamd и клиента spamc

# apt install spamassassin spamc
# sytemctl status spamd.service
● spamd.service - Perl-based spam filter using text analysis
     Loaded: loaded (/usr/lib/systemd/system/spamd.service; enabled; preset: enabled)
     Active: active (running) since Fri 2025-05-16 12:30:07 MSK; 1min 25s ago
   Main PID: 175488 (spamd)
      Tasks: 3 (limit: 1090)
     Memory: 138.7M (peak: 139.7M)
        CPU: 2.354s
     CGroup: /system.slice/spamd.service
             ├─175488 /usr/bin/perl "-T -w" /usr/sbin/spamd --pidfile=/run/spamd.pid --create-prefs --max-children 5...
             ├─175601 "spamd child"
             └─175602 "spamd child"

.......... systemd[1]: Started spamd.service - Perl-based spam filter using text analysis.
.......... spamd[175488]: zoom: able to use 388/388 'body_0' compiled rules (100%)
.......... spamd[175488]: spamd: server started on IO::Socket::IP [::1]:783, IO::Socket::IP [127.0.0.1]:783 (running...
.......... spamd[175488]: spamd: server pid: 175488
.......... spamd[175488]: spamd: server successfully spawned child process, pid 175601
.......... spamd[175488]: spamd: server successfully spawned child process, pid 175602
.......... spamd[175488]: prefork: child states: IS
.......... spamd[175488]: prefork: child states: II

Cron-задание для автоматического обновления SpamAssassin

# crontab -e
0 3 * * * /usr/bin/sa-update && /bin/systemctl restart spamd.service

Сообщаем Postfix, что нужно использовать внешний фильтр

# nano /etc/postfix/main.cf
# Отправлять сообщения на внешний фильтр для проверки на спам
content_filter = spam_filter
# Отключаем преобразования адреса получателя, чтобы фильтр мог
# проверить оригинальное сообщение, до любых изменений.
receive_override_options = no_address_mappings

Создаем демона spam_filter в файле конфигурации /etc/postfix/master.cf

# nano /etc/postfix/master.cf
spam_filter  unix  -     n       n       -       -       pipe
     user=spamd argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -i -f $sender $recipient

Postfix передаёт сообщение spamc, который передает его демону spamd, а потом возвращает обратно в Postfix через утилиту sendmail.

Демон spamd.service добавляет к письмам специальные заголовки и может изменять тему сообщения

  • X-Spam-Flag: YES — если письмо признано спамом
  • X-Spam-Status: Yes..... — детальный статус
  • X-Spam-Level: *** — уровень спама звездочками

Редактируем файл конфигурации SpamAssassin, чтобы добавлять в заголовок Subject строку ***SPAM***

# nano /etc/spamassassin/local.cf
# Добавлять в заголовок Subject строку ***SPAM***
rewrite_header Subject ***SPAM***
# SpamAssassin может просто добавлять заголовки (0)
# или создавать новое письмо с отчетом и прикреплять
# исходное сообщение как вложение к отчету (1)
report_safe 0

Когда spamc возвращает обработанное письмо обратно в Postfix через sendmail, оно через maildrop и pickup попадает к демону cleanup. На этом этапе можно использовать директиву header_checks для реакции на заголовки, которые добавил SpamAssassin.

# nano /etc/postfix/main.cf
# Это глобальная настройка, действует для демноа smtpd на 25 порту и для демонов submission
# и submissions, если для них не задано другое значение в файле /etc/postfix/master.cf
header_checks = pcre:/etc/postfix/header_checks
# nano /etc/postfix/header_checks
# Очень мягкая реакция — добавляем запись в лог-файл, если SpamAssassin пометил письмо как спам. Сообщение
# будет доставлено получателю, почтовый клиент может поместить сообщение в отдельную папку для спама.
/^X-Spam-Flag: YES/ WARN Spam detected
# Если SpamAssassin пометил письмо как спам — оно не будет доставлено получателю. Postfix отправит NDR
# (Non-Delivery Report) на адрес конвертного отправителя MAIL FROM и добавит запись в лог-файл о причинах.
/^X-Spam-Flag: YES/ REJECT Spam detected
# Довольно опасная реакция — письмо не будет доставлено получателю. Non-Delivery Report не отправляется,
# добавляется запись в лог-файл с указанием причины. С точки зрения отправителя — письмо просто пропало.
/^X-Spam-Flag: YES/ DISCARD Spam detected

SpamAssassin для демона submission(s)

Директивы content_filter и receive_override_options, заданные в файле /etc/postfix/main.cf — действуют глобально, то есть не только для демона smtpd на 25-ом порту, но и для демонов submission(s). При этом, мы можем установить другое значение header_checks для submission(s) в файле /etc/postfix/master.cf и применять другую политику для «своих» почтовых клиентов.

# nano /etc/postfix/master.cf
# Choose one: enable submission for loopback clients only, or for any client.
#127.0.0.1:submission inet  n -   y       -       -       smtpd
submission  inet  n       -       y       -       -       smtpd
     -o syslog_name=postfix/submission
     -o smtpd_tls_security_level=encrypt
     -o smtpd_sasl_auth_enable=yes
     -o smtpd_tls_auth_only=yes
     -o smtpd_client_restrictions=$submission_client_restrictions
     -o smtpd_helo_restrictions=$submission_helo_restrictions
     -o smtpd_sender_restrictions=$submission_sender_restrictions
     -o smtpd_relay_restrictions=$submission_relay_restrictions
     -o smtpd_recipient_restrictions=$submission_recipient_restrictions
     # Для «своих» почтовых клиентов, которые прошли аутентификацию
     -o header_checks=pcre:/etc/postfix/msa_header_checks

# Choose one: enable submissions for loopback clients only, or for any client.
#127.0.0.1:submissions inet  n  -       y       -       -       smtpd
submissions  inet  n       -       y       -       -       smtpd
     -o syslog_name=postfix/submissions
     -o smtpd_tls_wrappermode=yes
     -o smtpd_sasl_auth_enable=yes
     -o smtpd_client_restrictions=$submission_client_restrictions
     -o smtpd_helo_restrictions=$submission_helo_restrictions
     -o smtpd_sender_restrictions=$submission_sender_restrictions
     -o smtpd_relay_restrictions=$submission_relay_restrictions
     -o smtpd_recipient_restrictions=$submission_recipient_restrictions
     # Для «своих» почтовых клиентов, которые прошли аутентификацию
     -o header_checks=pcre:/etc/postfix/msa_header_checks

Будем отправлять скрытую копию в специальный почтовый ящик (который нужно предварительно создать)

# nano /etc/postfix/msa_header_checks
# Отправляем скрытую копию в специальный ящик, если письмо помечено как спам
/^X-Spam-Flag: YES/ BCC auth-user-spam@example.com

Добавим мониторинг количества спам-сообщений в этом ящике

#!/bin/bash
TELEGRAM_API_KEY='..........'
TELEGRAM_CHAT_ID='..........'

SPAM_MAILDIR='/var/mail/auth-user-spam' # ящик спам-сообщений
OLD_MINUTES=1440 # удалять старые сообщения (больше 24 часов)
ALERT_COUNT=50 # уведомлять, если 50 или больше спам-сообщений

function telegram {
    curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_API_KEY/sendMessage" \
        -d chat_id="$TELEGRAM_CHAT_ID" \
        -d text="${1}" > /dev/null
}

# Удаление старых писем из ящика
find $SPAM_MAILDIR -type f -mmin +$OLD_MINUTES -delete

# Подсчет оставшихся сообщений
count=$(find $SPAM_MAILDIR -type f | wc -l)

# Отправка уведомления в телеграм
if [ "$count" -ge "$ALERT_COUNT" ]; then
    telegram "Внимание! Почтовые клиенты отправили $count спам-сообщений"
fi

Подключение SpamAssassin с использованием Milter

Нужно установить пакет spamass-milter — это демон, который будет принимать сообщения от Postfix по Milter-протоколу. Далее демон spamass-milter передает сообщение на проверку демону spamd через tcp-сокет 127.0.0.1:783.

# apt install spamass-milter
# systemctl status spamass-milter.service 
● spamass-milter.service - LSB: milter for spamassassin
     Loaded: loaded (/etc/init.d/spamass-milter; generated)
     Active: active (running) since Sun 2025-05-25 11:42:18 MSK; 7min ago
       Docs: man:systemd-sysv-generator(8)
    Process: 265822 ExecStart=/etc/init.d/spamass-milter start (code=exited, status=0/SUCCESS)
      Tasks: 5 (limit: 1090)
     Memory: 660.0K (peak: 1.3M)
        CPU: 33ms
     CGroup: /system.slice/spamass-milter.service
             └─265844 /usr/sbin/spamass-milter -P /var/run/spamass/spamass.pid -f
                      -p /var/spool/postfix/spamass/spamass.sock -u spamass-milter -i 127.0.0.1

.......... systemd[1]: Starting spamass-milter.service - LSB: milter for spamassassin...
.......... spamass-milter[265844]: spamass-milter 0.4.0 starting
.......... spamass-milter[265822]: Starting Sendmail milter plugin for SpamAssassin: spamass-milter
.......... systemd[1]: Started spamass-milter.service - LSB: milter for spamassassin.
$ systemctl status spamd.service 
● spamd.service - Perl-based spam filter using text analysis
     Loaded: loaded (/usr/lib/systemd/system/spamd.service; enabled; preset: enabled)
     Active: active (running) since Fri 2025-05-16 12:30:07 MSK; 1 week 1 day ago
   Main PID: 175488 (spamd)
      Tasks: 3 (limit: 1090)
     Memory: 133.3M (peak: 139.7M)
        CPU: 2min 15.137s
     CGroup: /system.slice/spamd.service
             ├─175488 /usr/bin/perl "-T -w" /usr/sbin/spamd --pidfile=/run/spamd.pid --create-prefs --max-children 5 ...
             ├─175601 "spamd child"
             └─175602 "spamd child"

.......... systemd[1]: Started spamd.service - Perl-based spam filter using text analysis.
.......... spamd[175488]: zoom: able to use 388/388 'body_0' compiled rules (100%)
.......... spamd[175488]: spamd: server started on IO::Socket::IP [::1]:783, IO::Socket::IP [127.0.0.1]:783
.......... spamd[175488]: spamd: server pid: 175488
.......... spamd[175488]: spamd: server successfully spawned child process, pid 175601
.......... spamd[175488]: spamd: server successfully spawned child process, pid 175602
.......... spamd[175488]: prefork: child states: IS
.......... spamd[175488]: prefork: child states: II

Редактируем файл конфигурации Postfix

# nano /etc/postfix/main.cf
# Применять фильтр для сообщений, которые попадают в Postfix через демона smtpd
smtpd_milters = unix:spamass/spamass.sock
# Применять фильтр для писем, которые попадают в Postfix не через демона smtpd
non_smtpd_milters = $smtpd_milters

Файл конфигурации spamass-milter можно не трогать

# nano /etc/default/spamass-milter
# Запускать от имени пользователя spamass-milter,
# игнорировать письма с localhost (127.0.0.1)
OPTIONS="-u spamass-milter -i 127.0.0.1"

# Отклонять письма (550 reject), у которых scores,
# присвоенный SpamAssassin, больше 15
#OPTIONS="${OPTIONS} -r 15"

# Запретить SpamAssassin изменять Subject или тело
# сообщения, можно только добавлять заголовки
# X-Spam-Status, X-Spam-Level, X-Spam-Score
#OPTIONS="${OPTIONS} -m"

# Если Postfix установлен (файл /usr/sbin/postfix
# существует и исполняемый), то настройки ниже
# будут установлены по умолчанию
# SOCKET="/var/spool/postfix/spamass/spamass.sock"
# SOCKETOWNER="postfix:postfix"
# SOCKETMODE="0660"

Отправка тестового сообщения

Для этого отправим сообщение с localhost, которое будет содержать специальную строку, которую предлагает SpamAssassin в документации. Но для этого нам нужно, чтобы сообщения, отправленные с localhost — тоже проверялись на спам. Поэтому редактируем файл конфигурации spamass-milter (временно убираем из OPTIONS опцию -i 127.0.0.1) и перезапускаем демона.

# telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 mail.example.com ESMTP Postfix
EHLO mail.example.com
250-mail.example.com
250-PIPELINING
250-SIZE 15728640
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING
MAIL FROM: <somebody@mail.ru>
250 2.1.0 Ok
RCPT TO: <evgeniy@example.com>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Subject: Test spam

XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X
.
250 2.0.0 Ok: queued as 9FB852185D
QUIT
221 2.0.0 Bye
Connection closed by foreign host.

ClamAV для защиты от вирусов

Postfix будет передавать каждое полученное сообщение ClamAV для анализа. ClamAV может предпринять какие-то действия при обнаружении вируса или просто добавить заголовки и вернуть сообщение Postfix. Подключить ClamAV можно с помощью директивы content_filter, либо с использованием Milter.

Подключение ClamAV с использованием Milter

Если SpamAssassin был подключен с помощью директивы content_filter — подключить ClamAV можно только с помощью Milter, потому что директива разрешает только одно значение (хотя есть обходные пути).

# apt install clamav-daemon clamav-freshclam

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

# chmod u+w /etc/clamav/freshclam.conf
# nano /etc/clamav/freshclam.conf
# Имя хоста для DNS-запроса номера последний версии базы
DNSDatabaseInfo current.cvd.clamav.net

# Сколько раз в сутки проверять наличие обновлений базы
Checks 4

# Не обращаться к этим зеркалам для скачивания обновлений
#DatabaseMirror db.local.clamav.net
#DatabaseMirror database.clamav.net

# Можно подключиться к этим зеркалам через прокси-сервер
#HTTPProxyServer proxy.server.com
#HTTPProxyPort 8080
#HTTPProxyUsername username
#HTTPProxyPassword password

# Обновление из приватного зеркала. У зеркала PrivateMirror
# приоритет по отношению к зеркалу DatabaseMirror.
PrivateMirror https://packages.microsoft.com/clamav
PrivateMirror https://mirror.truenetwork.ru/clamav
PrivateMirror https://clamav-mirror.ru

Для начала базу вирусов нужно обновить вручную (можно с подробным выводом всего процесса)

# freshclam -vvv
..........
ERROR: Database load killed by signal 9
..........

Тут меня поджидал неприятный сюрприз — нехватка оперативной памяти. Так что пришлось создать файл подкачки, подробнее здесь. Если вместо обновления базы freshclam выдал предупреждение о превышении лимит запросов к серверу обновлений — нужно удалить файл /var/lib/clamav/freshclam.dat и попробовать еще раз.

Отредактируем файл конфигурации clamav-daemon.service

# nano /etc/clamav/clamd.conf
# Путь к файлу логов демона clamd
LogFile /var/log/clamav/clamd.log
# Добавлять метку времени в логи
LogTime true

# Файл для PID процесса clamd
PidFile /var/run/clamav/clamd.pid

# User, под которым запущен clamd
User clamav
# Разрешить clamav использовать доп.группы
# (для взаимодействия с другими сервисами)
AllowSupplementaryGroups true

# Директория для хранения базы вирусов
DatabaseDirectory /var/lib/clamav
# Директория для распаковки файлов
TemporaryDirectory /var/tmp

# Сокет для связи с Milter
LocalSocket /var/run/clamav/clamd.ctl
# Группа, которой принадлежит сокет
LocalSocketGroup clamav
# Права доступа к сокету (rw-rw----)
LocalSocketMode 660
# Удалять зависший сокет
FixStaleSocket true

# Включить сканирование почтовых сообщений
ScanMail true
# Включить сканирование архивов
ScanArchive true
# Макс. уровень вложенности архивов
MaxRecursion 16
# Макс. кол-во файлов, извлекаемых из архива
MaxFiles 10000

# Максимальный объем данных, который демон будет принимать от клиента,
# все остальное будет отброшено. Нужно установить значение, равное или
# чуть больше message_size_limit из файла конфигурации Postfix.
StreamMaxLength 16M
# Максимальный объем данных, который будет сканироваться — из того объема,
# который был принят с учетом StreamMaxLength. Все, что выходит за это
# ограничение — просто не сканируется (хотя там может быть вирус).
MaxScanSize 16M
# Максимальный размер отдельного файла (например, вложение или файл
# внутри архива), который будет сканироваться. Файлы большего размера
# не будут сканироваться (хотя в них может быть вирус).
MaxFileSize 16M

Запустим в работу демонов и добавим в автозагрузку

# systemctl enable --now clamav-daemon.service # демон сканирования на вирусы
# systemctl enable --now clamav-freshclam.service # демон для обновления базы вирусов

Теперь нам нужен Milter для передачи сообщений от Postfix в ClamAV

# apt install clamav-milter
# systemctl status clamav-milter.service 
● clamav-milter.service - LSB: ClamAV virus milter
     Loaded: loaded (/etc/init.d/clamav-milter; generated)
     Active: active (running) since Wed 2025-05-28 09:52:06 MSK; 15min ago
       Docs: man:systemd-sysv-generator(8)
    Process: 31904 ExecStart=/etc/init.d/clamav-milter start (code=exited, status=0/SUCCESS)
      Tasks: 6 (limit: 1090)
     Memory: 684.0K (peak: 4.1M swap: 1.2M swap peak: 1.2M)
        CPU: 187ms
     CGroup: /system.slice/clamav-milter.service
             └─32026 /usr/sbin/clamav-milter --config-file=/etc/clamav/clamav-milter.conf

.......... systemd[1]: Starting clamav-milter.service - LSB: ClamAV virus milter...
.......... clamav-milter[31904]:  * Starting Sendmail milter plugin for ClamAV clamav-milter
.......... clamav-milter[31904]:    ...done.
.......... systemd[1]: Started clamav-milter.service - LSB: ClamAV virus milter.

Отредактируем файл конфигурации Milter

# nano /etc/clamav/clamav-milter.conf
# Доступ к Milter-у через tcp-сокет, самый простой вариант
MilterSocket inet:7357@localhost

# Если обраружен вирус — отклонить письмо с ошибкой SMTP,
# отправитель получит от Postfix уведомление о недоставке
OnInfected Reject

# Если сканирование не удалось по любой причине — Postfix
# временно отклонит сообщение (самый безопасный вариант)
OnFail Defer

# Максимальный размер сообщения, нужно установить значение,
# которое задано для message_size_limit или чуть больше
MaxFileSize 16M

# Пользователь, от имени которого будет работать milter
User clamav

# Максимальное время ожидания данных от Postfix или от
# ClamAV, 120 секунд (2 минуты) обычно достаточно
ReadTimeout 120

# Запускать milter в фоновом режиме для продакшен (false)
# или на переднем плане для отладки (true).
Foreground false

# Путь к файлу, где будет храниться PID clamav-milter
PidFile /var/run/clamav/clamav-milter.pid

# Путь к сокету демона clamd (основного сканера ClamAV)
ClamdSocket unix:/var/run/clamav/clamd.ctl

# Что делать, если сообщение чистое (не содержит вируса)
OnClean Accept

# Как добавлять заголовок X-Virus-Scanned: Replace (заменит
# существующий), Add (добавит новый), Off (не добавлять)
AddHeader Replace

# Логировать в syslog (true) или в отдельный файл (false)
LogSyslog false
# Facility для syslog, если LogSyslog имеет значение true
LogFacility LOG_LOCAL6

# Путь к файлу логов, если LogSyslog задано значение false
LogFile /var/log/clamav/clamav-milter.log
# Разблокировать лог-файл после каждой записи? Может замедлить
# работу, поэтому обычно false
LogFileUnlock false

# Включить более подробное логирование? Установить false для
# продакшена или true для отладки
LogVerbose false
# Логировать информацию о зараженных письмах? Off (выключить),
# Basic (кратко), Full (подробно)
LogInfected Basic
# Логировать информацию о письмах, если вирус не обнаружен?
LogClean Off
# Добавлять временную метку к каждой записи в лог-файле?
LogTime true

# Включить встроенную ротацию лог-файла? Обычно не нужно,
# потому что при установке создается конфиг для logrotate
LogRotate false
# Максимальный размер лог-файла перед ротацией (если вкл)
LogFileMaxSize 1M

# Обрабатывать письмо с несколькими получателями один раз
# (false) или несколько раз по кол-ву получателей (true)
SupportMultipleRecipients false

# Директория для временных файлов, используемых при работе
TemporaryDirectory /tmp

# Группа для unix-сокета MilterSocket (если используется), может
# принимать значение postfix или clamav. Чтобы Postfix имел доступ
# к сокету, либо группа postfix должна быть задана как владелец
# сокета, либо пользователь postfix должен принадлежать группе
# clamav, либо права на сокет должны быть 666 (доступ для всех).
#MilterSocketGroup clamav
# Права доступа для unix-сокета MilterSocket, если используется
#MilterSocketMode 666
# При обнаружении зависшего сокета от прошлого сбоя — удалить
#FixStaleSocket true
Более подробно о файле конфигурации демона clamav-milter.service — можно прочитать в документации man clamav-milter.conf.

Редактируем файл конфигурации Postfix

# nano /etc/postfix/main.cf
# Проверять на вирусы сообщения, которые попадают в Postfix через демона smtpd
smtpd_milters = inet:7357@localhost

# Проверять на вирусы письма, которые попадают в Postfix не через демона smtpd
non_smtpd_milters = $smtpd_milters

# Действие по умолчанию, если milter временно недоступен или вернул ошибку
milter_default_action = tempfail

# Таймауты для внешних фильтров в секундах (это не обязательно, но желательно)
milter_connect_timeout = 30s
milter_command_timeout = 30s
milter_content_timeout = 300s

# Версия протокола Milter, 6-я версия рекомендуется для современных фильтров
milter_protocol = 6

Отправка тестового сообщения

Нужно отправить сообщение, которое будет содержать строку EICAR-Test или файл вложения с этой строкой. EICAR-Test-File — стандартный файл, применяемый для проверки, работает ли антивирус.

# telnet localhost 25
Trying ::1...
Connection failed: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 mail.example.com ESMTP Postfix
EHLO mail.example.com
250-mail.example.com
250-PIPELINING
250-SIZE 15728640
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING
MAIL FROM: <somebody@mail.ru>
250 2.1.0 Ok
RCPT TO: <evgeniy@example.com>
250 2.1.5 Ok
DATA
354 End data with <CR><LF>.<CR><LF>
Subject: Test virus

X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
.
550 5.7.1 Command rejected
QUIT
221 2.0.0 Bye
Connection closed by foreign host.
# tail -1 /var/log/clamav/clamav-milter.log
..... -> Message from <somebody@mail.ru> to <evgeniy@example.com> infected by Eicar-Signature
# tail -1 /var/log/clamav/clamav.log 
..... -> /tmp/clamav-a1f165437fd25df78669fc69eafed450.tmp (deleted): Eicar-Signature ... FOUND
# tail -5 /var/log/mail.log 
..... postfix/smtpd[4700]: connect from localhost[127.0.0.1]
..... postfix/smtpd[4700]: 5F01E21889: client=localhost[127.0.0.1]
..... postfix/cleanup[5073]: 5F01E21889: message-id=<20250711151715.5F01E21889@mail.example.com>
..... postfix/cleanup[5073]: 5F01E21889: milter-reject: END-OF-MESSAGE from localhost[127.0.0.1]: 5.7.1 Command rejected;
      from=<somebody@mail.ru> to=<evgeniy@example.com> proto=ESMTP helo=<mail.example.com>
..... postfix/smtpd[4700]: disconnect from localhost[127.0.0.1] ehlo=1 mail=1 rcpt=1 data=0/1 quit=1 commands=4/

Подключение ClamAV через content_filter

Сообщаем Postfix, что нужно использовать внешний фильтр

# nano /etc/postfix/main.cf
# Отправлять сообщения на внешний фильтр для проверки на вирус
content_filter = virus_filter
# Отключаем преобразования адреса получателя, чтобы фильтр мог
# проверить оригинальное сообщение, до любых изменений.
receive_override_options = no_address_mappings

Создаем демона virus_filter в файле конфигурации /etc/postfix/master.cf

# nano /etc/postfix/master.cf
virus_filter  unix  -       n       n       -       -       pipe
      user=filter argv=/usr/local/bin/virus-filter.sh $sender $recipient

Скрипт virus-filter.sh получает сообщение на stdin и проверяет на наличие вируса. Если вирус не обнаружен — возвращает сообщение в Postfix через утилиту sendmail. Если обнаружен вирус — демон smtpd отправит удаленному SMTP-клиенту (отправителю) постоянную ошибку (5xx). Если была ошибка сканирования — демон smtpd отправит удаленному SMTP-клиенту временную ошибку (4xx).

#!/bin/sh

SENDMAIL='/usr/sbin/sendmail -i'
# Сканирует stdin, выводит на stdout, если вирус не обнаружен
CLAMDSCAN='/usr/bin/clamdscan --stdout --no-summary -'
# Код возврата для Postfix при обнаружении вируса (EX_UNAVAILABLE)
EXIT_VIRUS=69
# Код возврата для Postfix для временной ошибки (EX_TEMPFAIL)
EXIT_TEMPFAIL=75

# Использование временного файла безопаснее для больших писем
TMPFILE=$(mktemp /tmp/postfix-clamav.XXXXXX)
# Если mktemp не сработал, выходим из скрипта с временной ошибкой
if [ -z "$TMPFILE" ]; then
    logger -t postfix-clamav 'Failed to create temporary file'
    exit $EXIT_TEMPFAIL
fi
trap "rm -f $TMPFILE" EXIT SIGHUP SIGINT SIGQUIT SIGTERM
cat > "$TMPFILE"

# Если clamdscan находит вирус — возвращает код 1, ничего не выводит.
# Если вирус не найден, возвращает код 0 и выводит письмо на stdout.
OUTPUT_FROM_SCAN=$("$CLAMDSCAN" < "$TMPFILE")
SCAN_RESULT=$?

if [ $SCAN_RESULT -eq 0 ]; then
    # Вирус не найден, возвращаем письмо в Postfix с использованием sendmail
    echo "$OUTPUT_FROM_SCAN" | $SENDMAIL -f "$SENDER" -- "$RECIPIENT"
    exit $?
elif [ $SCAN_RESULT -eq 1 ]; then
    # Найден вирус, выходим с кодом $EXIT_VIRUS (код будет получен Posfix-ом)
    # Добавляем запись в системный журнал syslog с тегом postfix-clamav
    logger -t postfix-clamav "ClamAV find virus: From <$SENDER> To <$RECIPIENT>"
    exit $EXIT_VIRUS
else
    # Ошибка, выходим с кодом $EXIT_TEMPFAIL (код будет получен Posfix-ом)
    # Добавляем запись в системный журнал syslog с тегом postfix-clamav
    logger -t postfix-clamav "ClamAV scan error ($SCAN_RESULT): From <$SENDER> To <$RECIPIENT>"
    exit $EXIT_TEMPFAIL
fi

В качестве параметров передаем скрипту отправителя и получателя из конверта — хотя в этом нет необходимости. Скрипту будут доступны переменные окружения $SENDER (envelope sender), $RECIPIENT (envelope recipient), $SASL_SENDER (адрес отправителя, аутентифицированного через SASL), $SASL_USERNAME (имя пользователя, аутентифицированного через SASL) и другие.

Установка и настройка OpenDKIM

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

# apt install opendkim opendkim-tools
$ systemctl status opendkim.service 
● opendkim.service - OpenDKIM Milter
     Loaded: loaded (/usr/lib/systemd/system/opendkim.service; enabled; preset: enabled)
     Active: active (running) since Wed 2025-05-28 15:43:48 MSK; 14min ago
       Docs: man:opendkim(8)
             man:opendkim.conf(5)
             man:opendkim-lua(3)
             man:opendkim-genkey(8)
             man:opendkim-genzone(8)
             man:opendkim-testkey(8)
             http://www.opendkim.org/docs.html
    Process: 35405 ExecStart=/usr/sbin/opendkim (code=exited, status=0/SUCCESS)
   Main PID: 35406 (opendkim)
      Tasks: 6 (limit: 1090)
     Memory: 2.1M (peak: 7.2M)
        CPU: 51ms
     CGroup: /system.slice/opendkim.service
             L-35406 /usr/sbin/opendkim

.......... systemd[1]: Starting opendkim.service - OpenDKIM Milter...
.......... systemd[1]: Started opendkim.service - OpenDKIM Milter.
.......... opendkim[35406]: OpenDKIM Filter v2.11.0 starting

Создаем директории для ключей и конфигурации

# mkdir -p /etc/opendkim/keys/example.com
# chown -R opendkim:opendkim /etc/opendkim
# chmod -R go-rwx /etc/opendkim/keys

Создаем ключи для домена example.com

# opendkim-genkey -s default -d example.com -D /etc/opendkim/keys/example.com/ -b 2048
# chown opendkim:opendkim /etc/opendkim/keys/example.com/*
# chmod 600 /etc/opendkim/keys/example.com/default.private

Здесь default — селектор для использования в ДНС-записи, /etc/opendkim/keys/example.com/ — директория для сохранения ключей, 2048 — размер ключа в битах (2048 — хороший выбор). В директории будут созданы два файла, default.private — приватный ключ, default.txt — публичный ключ для ДНС-записи.

Теперь редактируем файл конфигурации OpenDKIM

# nano /etc/opendkim.conf
# Общие настройки OpenDKIM
Syslog                  yes
UMask                   002
UserID                  opendkim:opendkim # пользователь и группа
PidFile                 /var/run/opendkim/opendkim.pid
Canonicalization        relaxed/simple
Mode                    sv # s — подписывать, v — верифицировать
SubDomains              no # подписывать или нет субдомены?

# Настройки для подписи
KeyTable                refile:/etc/opendkim/KeyTable
SigningTable            refile:/etc/opendkim/SigningTable
ExternalIgnoreList      refile:/etc/opendkim/TrustedHosts
InternalHosts           refile:/etc/opendkim/TrustedHosts

# Алгоритм подписи (рекомендуется)
SignatureAlgorithm      rsa-sha256

# Ведение статистики (не обязательно, но полезно для отладки)
Statistics              /var/spool/opendkim/stats.dat
StatisticsDisabled      no # установить yes, чтобы включить

# Адрес почты, с которого будем отправлять отчет владельцу домена,
# если DKIM-подпись сообщения от этого домена, не прошла проверку
ReportAddress           "Postmaster <postmaster@example.com>"
SendReports             yes

# Настройка tcp-сокета для связи с Postfix
Socket                  inet:8891@localhost
# Настройка unix-сокета для связи с Postfix
#Socket                  local:/var/spool/postfix/opendkim/opendkim.sock
Использование tcp-сокета — простой и надежный вариант. При использовании unix-сокета — директория должна быть доступна пользователю postfix.

Директория для статистики (не обязательно, но полезно для отладки)

# mkdir -p /var/spool/opendkim
# chown opendkim:opendkim /var/spool/opendkim
# chmod 750 /var/spool/opendkim

Создаем файлы KeyTable, SigningTable, TrustedHosts

# nano /etc/opendkim/KeyTable
# Связывает имя ключа (селектор и домен) с путем к приватному ключу
default._domainkey.example.com example.com:default:/etc/opendkim/keys/example.com/default.private
# nano /etc/opendkim/SigningTable
# Какие ключи использовать для подписи писем от определенных отправителей,
# * означает, что для всех отправителей будет использоваться один ключ
*@example.com default._domainkey.example.com
# nano /etc/opendkim/TrustedHosts
# Содержит список внутренних хостов InternalHosts, письма от которых будут
# подписываться. Также является списком внешниих хостов ExternalIgnoreList,
# для которых не нужно проверять DKIM-подпись, потому что мы им доверяем.
# Элемент списка может быть IP-адресом, CIDR-блоком или доменом.
127.0.0.1
localhost
*.example.com

Настроим Postfix для использования OpenDKIM

# nano /etc/postfix/main.cf
# Применять фильтры для сообщений, которые попадают в Postfix через демона smtpd
smtpd_milters =
    # проверка на вирусы
    inet:7357@localhost
    # проверка на спам
    unix:spamass/spamass.sock
    # подпись OpenDKIM
    inet:localhost:8891
# Применять фильтры для писем, которые попадают в Postfix не через демона smtpd
non_smtpd_milters = $smtpd_milters

# если в /etc/opendkim.conf указано использовать unix-сокет
#smtpd_milters = unix:/var/spool/postfixopendkim/opendkim.sock
#non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock

Наконец, добавим ДНС-запись из файла /etc/opendkim/keys/example.com/default.txt

Name Type TTL Value
example.com A 3600 111.111.111.111
www.example.com A 3600 111.111.111.111
mail.example.com A 3600 222.222.222.222
example.com MX 10 3600 mail.example.com
example.com TXT 3600 v=spf1 a mx -all
default._domainkey.example.com TXT 3600 v=DKIM1; h=sha256; k=rsa; p=MIIBI...TEEeN...

В файле в скобках будет три строки в кавычках, эти три части единого целого — их нужно «склеить», чтобы получить значение TXT-записи.

default._domainkey  IN  TXT  ( "v=DKIM1; h=sha256; k=rsa; " "p=MIIBI..." "TEEeN..." )

Подписывать или проверять сообщение?

Решение о том, подписывать или проверять, OpenDKIM принимает на основе директивы конфигурации InternalHosts. Если сообщение от хоста, который есть в InternalHosts — это исходящее сообщение, нужно добавить DKIM-подпись. Если сообщение от хоста, который нет в InternalHosts — это входящее сообщение, нужно проверить DKIM-подпись.

Директива Mode в файле opendkim.conf определяет, какие операции будет выполнять OpenDKIM. Обычно устанавливают значение sv (sign and verify), что позволяет одному экземпляру OpenDKIM выполнять обе функции.

# Файл сопоставления доменов и селекторов с ключами для подписи
KeyTable                refile:/etc/opendkim/KeyTable
# Файл сопоставления адресов отправителей с записями в KeyTable
SigningTable            refile:/etc/opendkim/SigningTable

# Сообщения от хостов, которые есть в списке InternalHosts будут
# подписываться. Сообщения от хостов, которых нет в этом списке,
# будут проверяться (исключая хосты из списка ExternalIgnoreList)
InternalHosts           refile:/etc/opendkim/InternalHosts
# Не проверять подпись от этих внешних хостов, мы им доверяем
ExternalIgnoreList      refile:/etc/opendkim/ExternalIgnoreList

Это значит, что нам не нужно делать разные настройки для демона smtpd (для проверки сообщений от «чужих» клиентов) и демонов submission(s) (для подписи сообщений от «своих» клиентов) — заботу об этом OpenDKIM берет на себя.

Подпись для нескольких доменов

Тут все как и для одного домена — создание ключей, редактирование файлов, публикация ДНС-записи.

# mkdir -p /etc/opendkim/keys/example.net
# mkdir -p /etc/opendkim/keys/example.org
# chown -R opendkim:opendkim /etc/opendkim/keys/example.net
# chown -R opendkim:opendkim /etc/opendkim/keys/example.org
# opendkim-genkey -s default -d example.net -D /etc/opendkim/keys/example.net/ -b 2048
# chown opendkim:opendkim /etc/opendkim/keys/example.net/*
# chmod 600 /etc/opendkim/keys/example.net/default.private
# opendkim-genkey -s default -d example.org -D /etc/opendkim/keys/example.org/ -b 2048
# chown opendkim:opendkim /etc/opendkim/keys/example.org/*
# chmod 600 /etc/opendkim/keys/example.org/default.private
# nano /etc/opendkim/KeyTable
# Связывает имя ключа (селектор и домен) с путем к приватному ключу
default._domainkey.example.com example.com:default:/etc/opendkim/keys/example.com/default.private
default._domainkey.example.net example.net:default:/etc/opendkim/keys/example.net/default.private
default._domainkey.example.org example.org:default:/etc/opendkim/keys/example.org/default.private
# nano /etc/opendkim/SigningTable
# Какие ключи использовать для подписи писем от определенных отправителей,
# * означает, что для всех отправителей будет использоваться один ключ
*@example.com default._domainkey.example.com
*@example.net default._domainkey.example.net
*@example.org default._domainkey.example.org
# nano /etc/opendkim/TrustedHosts
# Содержит список внутренних хостов InternalHosts, письма от которых будут
# подписываться. Также является списком внешниих хостов ExternalIgnoreList,
# для которых не нужно проверять DKIM-подпись, потому что мы им доверяем.
# Элемент списка может быть IP-адресом, CIDR-блоком или доменом.
127.0.0.1
localhost
*.example.com
*.example.net
*.example.org

Политика DMARC для почтового сервера

Получение отчетов от других MTA

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

Name Type TTL Value
example.com A 3600 111.111.111.111
www.example.com A 3600 111.111.111.111
mail.example.com A 3600 222.222.222.222
example.com MX 10 3600 mail.example.com
example.com TXT 3600 v=spf1 a mx -all
default._domainkey.example.com TXT 3600 v=DKIM1; h=sha256; k=rsa; p=MIIBI...TEEeN...
_dmarc.example.com TXT 3600 v=DMARC1; p=none; rua=mailto:dmarc@example.com; sp=reject; adkim=s; aspf=s; fo=1

Директива v=DMARC1 — указывает версию протокола (обязательно). Директива p=none — предприсывает серверу-получателю ничего не предпринимать, даже если проверки не пройдены, а просто отправлять отчеты. Директива rua=mailto:dmarc@example.com указывает адрес почты, на который нужно отправлять отчеты.

Директива sp=none — это политика для поддоменов, если не указана — наследуется от директивы p. Директива adkim=s — домен отправителя должен совпадать с доменом, который прошел проверку DKIM. Директива aspf=s — домен отправителя должен совпадать с доменом, который прошел проверку SPF.

Директива fo=1 — предписывает серверу-получателю отправлять отчет, если любая из проверок не пройдена (fo=1 — если все проверки не пройдены). Директива pct=100 — процент писем (от 0 до 100), к которым применяется политика p (по умолчанию 100).

На основе результатов проверок и политики, указанной в DMARC-записи, принимающий сервер решает, что делать с сообщениями, которые не прошли проверку

  • p=none — ничего не предпринимать, просто отправлять отчеты на dmarc@example.com
  • p=quarantine — могут быть помечены как спам или подвергнуты дополнительной проверке
  • p=reject — должны быть отклонены сервером получателя (не доставляться получателю)

Начинать следует с политики p=none и изучения отчетов, которые приходят на адрес почты dmarc@example.com. Следующий шаг — политика p=quarantine и pct=10 (для остальных писем, который не прошли проверку, будет применяться политика p=none). Следующий шаг — политика p=reject и pct=10. Заключительный этап — политика p=reject и pct=100.

Политика DMARC для входящих сообщений

OpenDMARC интегрируется с Postfix через milter, применяет политику DMARC (none, quarantine, reject) для входящих сообщений, отправляет RUA-отчеты серверам-отправителям.

# apt install opendmarc

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

# nano /etc/opendmarc.conf
# Идентификатор или имя почтового сервера, который выполнил проверки
# (SPF, DKIM, DMARC), будет добавлен заголовок Authentication-Results
AuthservID mail.example.com

# Не отправлять подробные (RUF) отчеты о каждом сбое при проверке
FailureReports false 

# Путь к файлу, в котором будет храниться идентификатор процесса
PidFile /run/opendmarc/opendmarc.pid

# Путь к файлу, содержащему список общедоступных суффиксов доменов
# (важно для правильной работы с субдоменами)
PublicSuffixList /usr/share/publicsuffix/public_suffix_list.dat
 
# Применять политику, которая указана в ДНС-записи p= для домена
# отправителя. Если указано p=reject — сообщения нужно отклонять.
# На начальном этапе лучше оставить false (режим мониторинга).
RejectFailures false 

# Используем TCP-сокет (порт 8893 — стандартный для OpenDMARC)
Socket inet:8893@localhost

# Включить логирование событий OpenDMARC через системный syslog
Syslog true

# Если сообщение уже прошло проверку SPF/DKIM на другом доверенном
# сервере — OpenDMARC будет доверять этим результатам
TrustedAuthservIDs HOSTNAME, mail.example.com

# Маска прав доступа для файлов, создаваемых OpenDMARC (socket и пр).
# Владелец и группа — чтение и запись, все остальные — только чтение.
UMask 0002

# Имя пользователя, от имени которого будет работать демон OpenDMARC
UserID opendmarc

# Записывать историю DMARC-проверок входящей почты в этот файл. Потом
# ReportCommand использует этот файл для генерации отчетов.
HistoryFile /var/spool/opendmarc/opendmarc.dat

# Утилита генерации отчетов из файла истории проверок HistoryFile
ReportCommand /usr/sbin/opendmarc-reports

# Периодичность отправки отчетов, которые были созданы ReportCommand
ReportInterval 86400

# Организация, которой принадлежит сервер, по умолчанию AuthservID
ReportOrgName "MAIL SERVER mail.example.com"

# С какого адреса почты отправлять отчеты от имени нашего сервера
ReportSender dmarc-reports-do-not-reply@example.com

# Список хостов (IP-адресов или имен), для которых DMARC-проверки
# проводиться не будут (для доверенных источников)
IgnoreHosts /etc/opendmarc/ignore.hosts

# Добавляет заголовок OpenDMARC к обработанным сообщениям, указывая
# версию и результаты проверки (полезно для отладки)
SoftwareHeader true

Директория для хранения HistoryFile

# mkdir -p /var/spool/opendmarc
# chown opendmarc:opendmarc /var/spool/opendmarc
# chmod 750 /var/spool/opendmarc

Редактируем файл конфигурации Postfix

# nano /etc/postfix/main.cf
# Применять фильтры для сообщений, которые попадают в Postfix через демона smtpd
smtpd_milters =
    # проверка на вирусы
    inet:7357@localhost
    # проверка на спам
    unix:spamass/spamass.sock
    # подпись OpenDKIM
    inet:localhost:8891
    # политика DMARC
    inet:localhost:8893
# Применять фильтры для писем, которые попадают в Postfix не через демона smtpd
non_smtpd_milters = $smtpd_milters

Взаимодействие Postfix и Dovecot

Передача сообщений от Postfix к Dovecot

Postfix может сам доставлять почту для «своих» пользователей, используя транспорт local и virtual. А может отправлять их Dovecot по протоколу LMTP — по сети либо через unix-сокет. И тогда распределением сообщений по почтовым ящикам пользователей будет заниматься Dovecot — этот вариант предпочтительнее.

Через unix-сокет /var/spool/postfix/private/dovecot-lmtp

# файл конфигурации /etc/postfix/main.cf
local_transport = lmtp:unix:private/dovecot-lmtp
virtual_transport = lmtp:unix:private/dovecot-lmtp
# файл конфигурации /etc/dovecot/conf.d/10-master.conf
service lmtp {
    unix_listener /var/spool/postfix/private/dovecot-lmtp {
        mode = 0660
        user = postfix
        group = postfix
    }
}

По сети, через интерфейс обратной петли loopback

# файл конфигурации /etc/postfix/main.cf
local_transport = lmtp:[127.0.0.1]:24
virtual_transport = lmtp:[127.0.0.1]:24
# файл конфигурации /etc/dovecot/conf.d/10-master.conf
service lmtp {
    # требуется установка пакета dovecot-lmtpd
    inet_listener lmtp {
        address = 127.0.0.1
        port = 24
    }
}

Есть еще один способ передачи сообщений от Postfix к Dovecot — использовать утилиту dovecot-lda. Можно запускать команду из директивы mailbox_command или создать транспорт dovecot (как мы уже создавали транспорт telegram). Первый вариант подходит для небольшого кол-ва системных пользователей, второй вариант позволяет обслуживать большое кол-во виртуальных пользователей.

Usage: dovecot-lda [-c <config file>] [-d <username>] [-p <path>]
                   [-m <mailbox>] [-e] [-k] [-f <envelope sender>]
                   [-a <original envelope recipient>]
                   [-r <final envelope recipient>]

Запускать команду, указанную в директиве mailbox_command

# без поиска по базе userdb (например, для определения квоты)
mailbox_command = /usr/lib/dovecot/dovecot-lda -f "$SENDER" -a "$RECIPIENT"
# с поиском по базе userdb (например, для определения квоты)
mailbox_command = /usr/lib/dovecot/dovecot-lda -f "$SENDER" -a "$RECIPIENT" -d "$USER"

Cоздать транспорт dovecot в файле /etc/postfix/master.cf

dovecot   unix  -       n       n       -       -       pipe
     flags=DRSOu user=vmail:vmail argv=/usr/lib/dovecot/dovecot-lda -f $sender -d $recipient
# использовать транспорт dovecot для виртуальных пользователей
dovecot_destination_recipient_limit = 1
virtual_transport = dovecot

Файл конфигурации /etc/dovecot/conf.d/15-lda.conf

protocol lda {
    log_path = /var/log/dovecot-lda.err
    info_log_path = /var/log/dovecot-lda.log
}

# Адрес, с которого отправляются сообщения об ошибке (например, при отклонении
# письма). По умолчанию используется postmaster@%d, где %d — домен получателя.
#postmaster_address =

# Имя хоста, которое будет использоваться в заголовках Message-ID и в ответах
# LMTP. Если не указано, используется системное имя по умолчанию.
#hostname =

# Если включено (yes), то при превышении квоты письмо не отклоняется с ошибкой,
# а временно задерживается для повторной попытки доставки.
#quota_full_tempfail = no

# Путь к утилите sendmail, которая используется для отправки писем из Dovecot
# (например, для автоматических ответов или сообщений об ошибках).
#sendmail_path = /usr/sbin/sendmail

# Если задано, сообщения будут отправляться через этот SMTP-хост, а не через
# утилиту sendmail. Формат значения директивы — smtp.example.com[:port]
#submission_host =

# Тема для сообщений с отказами; здесь %s — это тема оригинального письма.
#rejection_subject = Rejected: %s

# Сообщение об ошибке при отклонении письма. Здесь %n — перевод строки (CRLF),
# %r — причина отказа, %s — тема оригинального письма, %t — адрес получателя
#rejection_reason = Your message to <%t> was automatically rejected:%n%r

Передача сообщений от Dovecot к Postfix

Почтовые клиенты MUA могут отправлять сообщения для доставки демону Postfix submission(s) или демону Dovecot submission(s). В первом случае Postfix должен сам аутентифицировать клиентов, используя Cyrus SASL или Dovecot SASL. Во втором случае аутенификацией клиентов занимается Dovecot — и потом передает SMTP-соединение Postfix, выступая в качестве прокси.

# файл конфигурации /etc/dovecot/dovecot.conf
protocols = imap pop3 submission
# файл конфигурации /etc/dovecot/conf.d/10-master.conf
service submission-login {
    inet_listener submission {
        port = 587
    }
    inet_listener submissions {
        port = 465
    }
}

service submission {
    # требуется установка пакета dovecot-submissiond
}
# файл конфигурации /ect/dovecot/conf.d/20-submission.conf
hostname = mail.example.com
submission_relay_host = 127.0.0.1
submission_relay_port = 10025
submission_relay_trusted = yes

Мы здесь отправляем клиентов на 127.0.0.1:10025 — соответственно, нужно запустить Postfix демона, который будет прослушивать tcp-сокет 127.0.0.1:10025.

127.0.0.1:10025    inet    n    -    n    -    -    smtpd
     -o syslog_name=postfix/msa_auth_dovecot
     -o mynetworks=0.0.0.0/0
     -o smtpd_tls_security_level=none
     -o smtpd_client_restrictions=
     -o smtpd_helo_restrictions=
     -o smtpd_sender_restrictions=reject_sender_login_mismatch
     -o smtpd_relay_restrictions=permit_mynetworks,reject_unauth_destination
     -o smtpd_recipient_restrictions=reject_non_fqdn_recipient,reject_unknown_recipient_domain
     -o smtpd_authorized_xclient_hosts=127.0.0.1,localhost
     # Для «своих» почтовых клиентов, которые прошли аутентификацию
     -o header_checks=pcre:/etc/postfix/msa_header_checks

Проверка существования «своих» пользователей

Postfix может сам доставлять почту для «своих» пользователей, используя транспорт local и virtual. А может отправлять их Dovecot по протоколу LMTP — через unix-сокет либо по сети. И тогда распределением сообщений по почтовым ящикам пользователей будет заниматься Dovecot — этот вариант предпочтительнее.

# Сообщения для локальных пользователей отправлять Dovecot по протоколу
# LMTP через unix-сокет /var/spool/postfix/private/dovecot-lmtp
local_transport = lmtp:unix:private/dovecot-lmtp
virtual_transport = lmtp:unix:private/dovecot-lmtp

Но в этом случае возникают трудности с проверкой сущестования виртуального пользователя. Для проверки виртуальных пользователей — Postfix использует карты поиска virtual_alias_maps и virtual_mailbox_maps. Для проверки системных пользователей — Postfix использует файл /etc/passwd и карту $alias_maps.

# Карты локальных получателей сообщения (виртуальные)
virtual_alias_maps = hash:/etc/postfix/virtual_alias_map
virtual_mailbox_maps = hash:/etc/postfix/virtual_mailbox_map

# Карты локальных получателей сообщения (системные)
local_recipient_maps = proxy:unix:passwd.byname $alias_maps

Если сам Postfix не занимается доставкой почты в локальные ящики — ему не обязательно знать, какие пользователи и почтовые ящики существуют. Но если мы используем ограничение reject_unlisted_recipient — карты поиска virtual_alias_maps и virtual_mailbox_maps все-таки придется создавать. И данные почтовых пользователей придется хранить в двух местах — для Postfix и Dovecot.

# Ограничения на этапе команды RCPT TO (spam)
smtpd_recipient_restrictions =
    ..........
    # Отклонять почту на адреса, для которых нет почтовых ящиков
    reject_unlisted_recipient

Проще всего эта проблема решается, если использовать карту поиска mysql — вся информация хранится в одной таблице базы данных MySQL. И выполнять SQL-запросы к этой базе могут как Postfix, так и Dovecot. Трудности возникают, если Postfix использует карту поиска hash, а Dovecot хранит пользователей в файле passwd-file. Для создания почтового пользователя — нужно добавить запись в файл карты поиска virtual_mailbox_maps + добавить запись в Dovecot файл passwd-file.

Скрипт для добавления нового виртуального почтового пользователя

#!/bin/bash
set -e

# Проверка числа аргументов
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
    echo "Использование: $0 email password [aliases]"
    exit 1
fi

EMAIL="$1"
PASSWORD="$2"
ALIASES="${3:-}"

PASS_FILE='/etc/dovecot/users'
MAIL_BASE='/var/vmail'
VMAIL_UID=2000
VMAIL_GID=2000
MAILBOX_MAP='/etc/postfix/virtual_mailbox_map'
ALIASES_MAP='/etc/postfix/virtual_alias_map'

# Проверка на существование пользователя
if grep -q "^$EMAIL:" "$PASS_FILE"; then
    echo "Ошибка: Пользователь $EMAIL уже существует в $PASS_FILE"
    exit 1
fi

# Получение логина и домена из адреса почты
USER_PART=$(echo "$EMAIL" | cut -d@ -f1)
DOMAIN_PART=$(echo "$EMAIL" | cut -d@ -f2)

# Полный путь к ящику для passwd-file Dovecot
MAILDIR_DOVECOT="$MAIL_BASE/$DOMAIN_PART/$USER_PART"
# Относительный путь для карты поиска Postfix
MAILDIR_POSTFIX="$DOMAIN_PART/$USER_PART/"

# Создание директорий почтового ящика (обычно этого не
# требуется, Dovecot сам создает необходимые директории,
# главное — чтобы была запись в passwd-file)
mkdir -p "$MAILDIR_DOVECOT"/{cur,new,tmp}
chown -R vmail:vmail "$MAILDIR_DOVECOT"
chmod -R 770 "$MAILDIR_DOVECOT"

# Формирование поля extra_fields
EXTRA_FIELDS="userdb_mail=maildir:$MAILDIR_DOVECOT"

# Формирование строки пользователя для passwd-file
ENTRY="$EMAIL:{PLAIN}$PASSWORD:$VMAIL_UID:$VMAIL_GID:::::$EXTRA_FIELDS"
# Добавление строки пользователя в passwd-file
echo "$ENTRY" >> "$PASS_FILE"

# Добавление строки в /etc/postfix/virtual_mailbox_map
echo -e "$EMAIL\t$MAILDIR_POSTFIX" >> "$MAILBOX_MAP"
postmap "$MAILBOX_MAP"

# Обработка алиасов, если они предоставлены
if [ -n "$ALIASES" ]; then
    # разбить $ALIASES по запятой и поместить в массив
    IFS=','
    read -ra ARR <<< "$ALIASES"
    for a in "${ARR[@]}"; do
        # убираем пробелы на всякий случай
        ALIAS=$(echo "$a" | xargs)
        # если алиас уже есть — обновим, иначе добавим
        if egrep -q "^$ALIAS\s+" "$ALIASES_MAP"; then
            sed -iE "s/^$ALIAS\s+\S+$/\0,$EMAIL/" $ALIASES_MAP
        else
            echo -e "$ALIAS\t$EMAIL" >> $ALIASES_MAP
        fi
    done
    postmap "$ALIASES_MAP"
fi

Скрипт для синхронизации записей из passwd-file с картой поиска

#!/bin/bash
set -e

PASS_FILE='/etc/dovecot/users'
MAILBOX_MAP='/etc/postfix/virtual_mailbox_map'
TMP_MAILBOX=$(mktemp)

# Обработка строк из passwd-file
while IFS=: read -r EMAIL TMP1 TMP2 TMP3 TMP4 TMP5 TMP6 EXTRA_FIELDS; do
    # Извлечение username и domain из email
    USER_PART=$(echo "$EMAIL" | cut -d@ -f1)
    DOMAIN_PART=$(echo "$EMAIL" | cut -d@ -f2)

    # Извлечение пути к ящику (userdb_mail=)
    MAIL_PATH=$(echo "$EXTRA_FIELDS" | grep -oE 'userdb_mail=maildir:[^ ]+' | cut -d= -f2 | sed 's|^maildir:||')
    
    # Получение относительного пути (относительно /var/vmail)
    REL_PATH=$(echo "$MAIL_PATH" | sed 's|^/var/vmail/||')

    # Запись в карту почтовых ящиков
    if [ -n "$REL_PATH" ]; then
        echo -e "$EMAIL\t$REL_PATH" >> "$TMP_MAILBOX"
    fi
done < "$PASS_FILE"

# Перезапись файла карты поиска
mv "$TMP_MAILBOX" "$MAILBOX_MAP"
# Создание индексированной карты
postmap "$MAILBOX_MAP"

Поиск: Linux • Клиент • Конфигурация • Настройка • Сервер • Postfix • Dovecot • SMTP • POP3 • IMAP • Почта

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.