Почему ваши правила в клиенте не работают
Классическая история. Пользователь настроил в Thunderbird правило «письма из рассылки — в папку Lists», неделю радуется чистому инбоксу, а в понедельник утром находит в INBOX сорок писем от тех же рассылок. Почему? Ноутбук в выходные был выключен, почта прилетала на телефон, а правило живёт внутри Thunderbird и срабатывает только когда он запущен и успел синхронизировать папку.
Это системный дефект клиентской фильтрации, а не случайный сбой. У неё три врождённые проблемы:
- Правила исполняются только при запущенном клиенте. Письмо, пришедшее ночью, разложится не в момент доставки, а когда вы утром откроете приложение — если вообще откроете именно этот клиент.
- Правила не синхронизируются между устройствами. Один набор фильтров в вебе, другой на телефоне, третий на десктопе. Их приходится дублировать вручную, и они неизбежно расходятся.
- POP3 конфликтует с локальными перемещениями. Если один клиент забирает почту по POP3 и утаскивает её к себе, остальные её вообще не увидят, и правила на них молчат.
Вывод простой: фильтр должен стоять до того, как письмо легло в ящик, а не после. То есть на сервере, в момент локальной доставки. Тогда он срабатывает на 100% входящих, одинаково для всех устройств, и ему безразлично, включён ли у вас хоть один клиент.
Где Sieve исполняется на самом деле
Sieve — это не плагин почтовой программы и не «правила», которые кто-то экспортирует. Это язык фильтрации, стандартизированный в RFC 5228, а исполняет его движок на сервере в момент локальной доставки (LDA/LMTP). Клиент к самому исполнению отношения не имеет.
Полная цепочка входящего письма выглядит так:
Ключевой момент: скрипт один на аккаунт, лежит на сервере и срабатывает на каждом входящем письме. Порядок исполнения фиксированный — сначала глобальный sieve_before (сюда обычно кладут раскладку спама), затем пользовательский скрипт, затем sieve_after. Пользователь правит только свой средний слой.
Анатомия Sieve-скрипта
Скрипт состоит из трёх вещей: require для подключения расширений, тестов (header, address, envelope, size) и действий. Вот рабочий пример, который делает три полезных дела сразу:
require ["fileinto", "imap4flags", "mailbox", "vacation"];
if header :contains "List-Id" "newsletter" {
setflag "\\Seen";
fileinto :create "Lists";
stop;
}
if address :is "from" "[email protected]" {
fileinto "Priority";
}
vacation :days 7
:subject "В отпуске до 14 июля"
:addresses ["[email protected]"]
"Отвечу после отпуска.";В этом коротком примере три неочевидных места.
- Implicit keep. Если ни одно действие не разложило письмо (
fileinto) и не отменило доставку (discard), Sieve по умолчанию кладёт письмо в INBOX. Это «неявный keep». Именно поэтому в блоке рассылок стоитstop— без него послеfileintoуправление пошло бы дальше и логика могла задвоиться. - `stop` прекращает исполнение всего скрипта, а не только текущего блока. Здесь рассылка уходит в Lists, и до проверки
[email protected]дело не доходит — это осознанный приоритет. - `vacation` без `:addresses` — граната. Автоответчик обязан знать, какие адреса считаются «вашими», иначе он начнёт отвечать на служебные адреса рассылок, и вы получите mail loop. По стандарту vacation не отвечает на письма с признаками bulk и на списки, но
:addresses— ваша основная страховка.
Расширения, которые стоит держать в голове: vacation (RFC 5230), imap4flags (RFC 5232), variables (RFC 5229), relational (RFC 5231), плюс regex, editheader, spamtest/virustest и date. Каждое подключается через require, и без него соответствующие команды просто не скомпилируются.
ManageSieve: протокол, а не файл
Скрипт живёт в ~/sieve на сервере. Но как дать пользователю его редактировать, не выдавая SSH и права на файловую систему? Для этого есть отдельный протокол — ManageSieve, RFC 5804, TCP-порт 4190.
Важная оговорка про порт. Старые сборки Dovecot слушали ManageSieve на порту 2000, но 2000 официально закреплён IANA за cisco-sccp, поэтому сегодня это антипаттерн. Используйте только 4190.
Сессия текстовая и очень похожа на IMAP: CAPABILITY → STARTTLS → AUTHENTICATE (SASL) → работа. Набор команд компактный:
LISTSCRIPTS— список скриптов с отметкой активного.PUTSCRIPT "name" {...}— загрузить или перезаписать скрипт.GETSCRIPT "name"— забрать тело.CHECKSCRIPT {...}— проверить синтаксис без сохранения.SETACTIVE "name"— сделать скрипт активным (активным может быть только один).DELETESCRIPT/RENAMESCRIPT— удалить или переименовать.
Самое ценное свойство протокола: на PUTSCRIPT и CHECKSCRIPT сервер компилирует и валидирует скрипт и при ошибке возвращает человекочитаемый ответ с номером строки. Невалидный фильтр физически не станет активным. Это принципиально безопаснее правки файла по SSH, где опечатка тихо ломает доставку.
Поднимаем на Dovecot Pigeonhole
Движок Sieve в Dovecot — это Pigeonhole. Нужны два пакета: dovecot-sieve (движок для LDA/LMTP) и dovecot-managesieved (демон протокола на 4190).
Минимальный рабочий конфиг:
protocol sieve {
}
service managesieve-login {
inet_listener sieve {
port = 4190
}
}
protocol lmtp {
mail_plugins = $mail_plugins sieve
}
plugin {
sieve = file:~/sieve;active=~/.dovecot.sieve
sieve_max_script_size = 1M
sieve_before = /var/lib/dovecot/sieve/spam-global.sieve
}Что здесь происходит по строкам. Набор пользовательских скриптов лежит в ~/sieve/, активный симлинкуется в ~/.dovecot.sieve и компилируется в бинарник .svbin рядом — при доставке движок читает именно скомпилированную версию. sieve_max_script_size = 1M отсекает гигантские скрипты. sieve_before подключает глобальную антиспам-раскладку, которая отработает раньше пользовательского скрипта.
Права критичны. В виртуальных ящиках evilmail (/var/mail/vhosts/) каталог ~/sieve и файлы в нём должны принадлежать vmail — 5000:5000. Если владелец другой, managesieved не сможет писать, а LDA — читать, и вы получите тихий отказ без явной ошибки.
Проверить, что демон живой и отдаёт CAPABILITY:
openssl s_client -connect localhost:4190 -starttls sieveВ ответ должен прийти листинг с "SIEVE" "fileinto vacation imap4flags ..." и STARTTLS. И обязательно запретите plaintext-логин без TLS: disable_plaintext_auth = yes. Порт 4190 в открытом виде наружу выставлять нельзя.
Подключаем Roundcube и Thunderbird
В Roundcube всё делает плагин managesieve. В config.inc.php:
$config['plugins'] = ['managesieve'];
$config['managesieve_host'] = 'localhost';
$config['managesieve_port'] = 4190;
$config['managesieve_usetls'] = true;
$config['managesieve_raw_editor'] = true;Пользователь получает в настройках раздел «Фильтры» с визуальным конструктором: он собирает условия и действия мышкой, а плагин под капотом генерирует Sieve-код и отправляет его через PUTSCRIPT + SETACTIVE. Параметр managesieve_raw_editor = true дополнительно включает режим прямого редактирования кода — для того, что визуальный UI построить не умеет: переменные, регулярки, вложенная логика.
Thunderbird работает через расширение Sieve: тот же порт 4190, тот же TLS, но оно сразу открывает редактор кода. Удобно, если вы пишете фильтры руками и хотите видеть исходник, а не собирать его из блоков.
Оба клиента правят один и тот же серверный скрипт. Отредактировали в вебе — телефон и десктоп немедленно фильтруют по новым правилам, потому что фильтрует сервер, а не они.
Отладка: когда письмо не разложилось
Половина работы с Sieve — это разбор «почему письмо оказалось не там». Инструменты:
# список и активный скрипт пользователя
doveadm sieve list -u user@domain
# вытащить тело конкретного скрипта
doveadm sieve get -u user@domain roundcube
# прогнать скрипт на реальном письме, ничего не сохраняя
sieve-test roundcube.sieve message.eml
# массовый прогон по ящику
sieve-filter -u user@domain roundcube.sieve INBOXsieve-test показывает, какие действия сработали бы на конкретном .eml — самый быстрый способ поймать логику. В логах Dovecot ищите строки вида sieve: msgid=<...> stored mail into mailbox 'Lists' — они прямо говорят, куда движок положил письмо.
Типовые грабли, в порядке частоты:
- Письмо всё равно в INBOX. Не отменён implicit keep: забыли
stopилиdiscardпосле нужного действия, либо условие не сматчилось (проверьтеsieve-test). - vacation зациклился. Автоответ ушёл на рассылку или на другого автоответчика. Лечится полным списком
:addressesи тем, что vacation не отвечает на bulk и списки. - TLS handshake fail на 4190. Клиент требует STARTTLS, а сертификат или конфиг демона его не отдаёт. Проверьте
openssl s_clientвручную. - Скрипт не сохраняется. Упёрлись в
sieve_max_script_sizeили нет прав на~/sieve(не 5000:5000).
Чеклист внедрения
- Порт 4190 доступен только через TLS;
disable_plaintext_auth = yes. dovecot-managesievedзапущен и отвечает наopenssl s_client -starttls sieve.sieve_beforeподключён для глобальной раскладки спама.sieve_max_script_sizeвыставлен (1M — разумный дефолт).- Каждый
vacationсодержит:daysи:addresses. - Владелец
~/sieveи.svbin— vmail 5000:5000. - Raw-редактор включён в Roundcube для продвинутых пользователей.
Перенеся фильтрацию с клиента на сервер, вы получаете один набор правил, который отрабатывает на каждом письме независимо от того, включён ли хоть один почтовый клиент. А ManageSieve оставляет пользователю право менять эти правила самому — из привычного веб-интерфейса, без SSH и без риска сломать доставку опечаткой.


