Серверная фильтрация почты через Sieve в Dovecot: сортировка, флаги и vacation без клиента
Клиентские правила Outlook и Thunderbird — это POP3-мышление в мире IMAP: они срабатывают, только когда клиент открыт, и ломаются на телефоне. Sieve переносит всю логику на сервер, в точку доставки. Разбираем, где именно Pigeonhole встроен в конвейер Postfix→Dovecot, как fileinto отменяет implicit keep, и почему серверный vacation не зацикливается и не отвечает рассылкам.
EvilMail Team10 июля 2026 г.13 мин чтения
Правило, которое «не сработало», хотя вы его написали
Классическая жалоба на self-hosted почте: «Я сделал в Thunderbird правило — все письма от заказчика класть в папку Проекты. Половина писем всё равно валится в INBOX». Открываешь логи — с сервером всё в порядке. Проблема в том, что правило живёт в клиенте, а клиент в тот момент был выключен. Письмо пришло ночью, легло в INBOX, вы утром открыли почту с телефона — а телефон про правило Thunderbird ничего не знает. Ноутбук включится вечером, догонит синхронизацию и, может быть, переложит письмо. А может и нет, если вы его уже прочитали с другого устройства.
Клиентские фильтры — это POP3-мышление, застрявшее в IMAP-мире. Они выполняются в точке чтения: когда конкретная программа на конкретном устройстве соединилась с сервером и решила прогнать входящие через свой список правил. Отсюда все симптомы: правила не работают на телефоне, дублируются между устройствами, конфликтуют, а «автоответчик в отпуске» из настроек клиента вообще не отправит ни одного письма, пока приложение закрыто — то есть ровно тогда, когда вы в отпуске.
Фильтрация принадлежит точке доставки, а не точке чтения. На сервере есть ровно один момент, когда письмо гарантированно проходит через ваш код: когда оно ложится в почтовый ящик. Именно туда и встраивается Sieve.
Где именно выполняется Sieve
Sieve в Dovecot: серверная фильтрация почты, сортировка и vacation — EvilMail Blog
Sieve (RFC 5228) — это язык описания правил обработки почты, а Pigeonhole — его реализация для Dovecot. Ключевое свойство: скрипт исполняется один раз, в момент доставки, до того как письмо будет записано на диск и станет видимым по IMAP. Клиент подключается уже к разобранному ящику.
Чтобы понять, где принимается решение, надо видеть весь конвейер. Postfix принимает письмо (smtpd), чистит заголовки (cleanup), ставит в очередь (qmgr) и передаёт его локальному транспорту. Для виртуальных доменов этот транспорт — LMTP-сокет Dovecot. Dovecot принимает письмо своим lmtp-сервисом и перед записью прогоняет через движок Pigeonhole.
Внутри движка порядок фиксирован: сначала глобальные скрипты sieve_before, потом персональный ~/.dovecot.sieve, потом sieve_after. Разница между dovecot-lda и LMTP на практике сводится к транспорту: для виртуальных доменов почти всегда используется LMTP через virtual_transport = lmtp:unix:private/dovecot-lmtp, и именно protocol lmtp должен получить плагин sieve.
Включаем Pigeonhole
На Debian/Ubuntu ставятся два пакета: dovecot-sieve (сам движок) и dovecot-managesieved (удалённое редактирование скриптов, о нём ниже).
bash
apt install dovecot-sieve dovecot-managesieved
Основная правка — в /etc/dovecot/conf.d/90-sieve.conf. Здесь мы задаём, где лежит активный скрипт, подключаем глобальные политики и выставляем лимиты:
vacation и imap4flags включены в Pigeonhole по умолчанию, их достаточно объявить в require внутри скрипта. В sieve_extensions попадают только те расширения, которых нет в дефолтном наборе, — здесь это editheader и spamtest.
Отдельно плагин нужно зарегистрировать именно в том протоколе, через который идёт доставка. Одной строки mail_plugins глобально недостаточно — Sieve должен висеть на LMTP:
Лимиты — не бюрократия. sieve_max_actions = 32 и sieve_max_redirects = 4 защищают от скрипта, который превратит ваш сервер в открытый ретранслятор: пользователь (или тот, кто угнал его ManageSieve-доступ) не сможет настроить редирект на тысячу адресов. При превышении лимита правило падает, и письмо остаётся в INBOX по implicit keep — не молча пропадает.
После правки скрипт компилируется в .svbin — бинарный кэш рядом с исходником. Компиляция происходит автоматически при первой доставке, но её стоит запускать руками при деплое, чтобы поймать синтаксис до того, как на нём споткнётся живое письмо.
Синтаксис на практике: сортировка по папкам
Начнём с рассылок — главного источника мусора в INBOX. У любой нормальной рассылки есть заголовок List-Id, и это надёжнее, чем ловить по From:
require ["fileinto","mailbox","imap4flags","envelope"];
# Рассылки Debian — в отдельную папку, создать при отсутствии
if header :contains "list-id" "<lists.debian.org>" {
fileinto :create "Lists/Debian";
stop;
}
# Весь трафик от домена подрядчика — в проектную папку
if address :domain "from" "acme-corp.com" {
fileinto :create "Проекты/ACME";
stop;
}
Три вещи, которые надо понимать здесь наизусть, иначе правила ведут себя «странно»:
implicit keep — если ни одно действие не сработало, письмо по умолчанию кладётся в INBOX. Sieve никогда не теряет письмо «просто так».
fileinto отменяет implicit keep — как только сработал fileinto, письмо больше не пойдёт в INBOX, оно уходит только в целевую папку. Хотите копию в INBOX — добавьте явный keep.
stop — прерывает выполнение скрипта. Без него следующее правило может переложить письмо ещё раз. Для взаимоисключающих правил stop обязателен.
Флаг :create критичен: без него fileinto "Lists/Debian" в несуществующую папку завершается ошибкой, и письмо падает обратно в keep. Это забытый :create — причина №2 в списке «почему правило молчит».
Спам и флаги без перекладывания на клиент
Если перед Dovecot стоит Rspamd, спам-вердикт уже лежит в заголовках письма. Sieve его читает и раскладывает, а через расширение imap4flags сразу проставляет IMAP-флаги — то, что клиентское правило делает уже постфактум и на каждом устройстве заново:
Здесь спам не только уезжает в Junk, но и помечается прочитанным (\\Seen) — счётчик непрочитанного не дёргает вас ради мусора. Для более тонкой логики есть расширение spamtest, дающее нормализованную числовую оценку: можно, скажем, тихо давить письма с высоким score, но оставлять непрочитанными пограничные, чтобы вы их проверили.
Vacation, который не позорится
Вот где серверный подход выигрывает вчистую. Клиентский автоответ либо не отправляется (клиент закрыт), либо отправляется неправильно — отвечает на рассылки, на нотификации, зацикливается с чужим автоответчиком. Sieve-расширение vacation (RFC 5230) сделано именно чтобы этого не было:
Механика анти-петли встроена в движок. Pigeonhole ведёт базу хэшей пар «получатель + отправитель» и не отвечает одному и тому же человеку повторно в течение :days — семь дней подряд назойливого коллегу автоответ не бомбардирует. Кроме того, vacation автоматически молчит, если у письма есть признаки того, что оно не от живого человека, ожидающего ответа:
присутствует List-Id (рассылка);
Precedence: bulk, list или junk;
любой Auto-Submitted:, кроме no (то есть письмо само сгенерировано);
Параметр :addresses — не украшение. Он перечисляет все ваши адреса и алиасы, чтобы vacation понимал: письмо действительно адресовано вам, а не пришло случайно через список рассылки, где вы один из тысячи. Без корректного :addresses автоответ может либо не сработать, либо ответить не с того адреса.
Редактирование вживую: ManageSieve
Заставлять пользователя (или себя) каждый раз лезть по SSH и править ~/.dovecot.sieve — так себе UX. Для этого есть ManageSieve (RFC 5804) — протокол на TCP-порту 4190, через который клиент загружает, редактирует и активирует скрипты, не касаясь диска напрямую. Сервер сам компилирует и валидирует скрипт при загрузке.
service managesieve-login {
inet_listener sieve { port = 4190 }
}
protocol sieve { managesieve_max_line_length = 65536 }
Дальше подключается любой клиент: плагин managesieve в Roundcube даёт пользователям веб-редактор фильтров, а расширение Sieve для Thunderbird — десктопный. Какой скрипт реально активен у пользователя, видно прямо на сервере — активный скрипт это симлинк в его maildir:
bash
readlink /var/mail/vhosts/evilmail.pro/ivan/.dovecot.sieve
ls /var/mail/vhosts/evilmail.pro/ivan/sieve/
Порт 4190 надо открыть в фаерволе — но только на нужных интерфейсах. Это управляющий доступ к чужим правилам обработки почты, его не выставляют в интернет без TLS и без ограничений.
sieve_before и sieve_after: политика, которую пользователь не отключит
Глобальные скрипты в /var/lib/dovecot/sieve/global/ — инструмент администратора, а не пользователя. sieve_before выполняется до персонального скрипта: сюда кладут приоритетную блокировку, карантин известных угроз, принудительную маркировку. sieve_after — после: форс-архивирование, копия в аудит-папку, добавление служебных заголовков.
Смысл в том, что пользователь не может это отключить или обойти своими правилами — он физически не редактирует эти файлы через ManageSieve. Если оператор решил, что письма с определённым вредоносным вложением уходят в карантин всегда, sieve_before со stop выполнит это решение раньше, чем персональное правило пользователя вообще получит письмо.
Отладка, когда правило молчит
Не деплойте скрипт, не прогнав его на реальном письме. Два инструмента закрывают 90% случаев.
bash
# Только компиляция и синтаксис — генерит .svbin
sievec ~/.dovecot.sieve
# Прогон на реальном .eml с трассировкой всех действий
sieve-test -t - ~/.dovecot.sieve message.eml
sieve-test -t показывает пошагово: какое условие сработало, в какую папку ушло письмо, какие флаги проставлены, был ли редирект. Это единственный честный способ убедиться, что правило делает то, что вы думаете. В логах доставки строки Sieve видны так:
bash
journalctl -u dovecot | grep sieve
# lmtp([email protected]): sieve: msgid=<...>: stored mail into mailbox 'Junk'
Типичные причины «правило не сработало», в порядке частоты:
забыт require для используемого действия — скрипт не компилируется целиком;
нет :create, а целевая папка не существует — fileinto падает в keep;
превышен sieve_max_actions на массовых редиректах;
битые права или владелец у .svbin — Dovecot не может прочитать кэш и молча использует implicit keep.
Чеклист внедрения
Поставлены пакеты dovecot-sieve и dovecot-managesieved.
Плагин sieve добавлен именно в protocol lmtp (а не только глобально).
Postfix отдаёт локальную доставку через virtual_transport = lmtp:unix:private/dovecot-lmtp.
Каталог ~/sieve и ~/.dovecot.sieve принадлежат владельцу ящика, .svbin читается Dovecot.
Выставлены лимиты sieve_max_actions и sieve_max_redirects — защита от релея.
Каждое сортирующее правило использует fileinto :create и завершается stop.
В vacation заполнен :addresses всеми вашими адресами и алиасами.
Глобальные политики вынесены в sieve_before / sieve_after, а не размазаны по пользователям.
ManageSieve на 4190 закрыт фаерволом и работает только по TLS.
Перед каждым деплоем скрипт прогнан через sieve-test -t на реальном письме.
Правило, которое вы хотите видеть работающим на всех устройствах круглосуточно, обязано жить в ~/.dovecot.sieve, а не в настройках клиента. Клиент — это то, чем вы читаете почту; сервер — это то, что решает её судьбу. На evilmail.pro вся серверная логика доставки строится именно так: один проход, в точке доставки, до того как письмо коснётся вашего INBOX.