Серверні Sieve-фільтри у Dovecot: сортування, автовідповіді та переадресація на рівні сервера
Клієнтські правила сортування ненадійні: вони працюють лише коли відкритий поштовий клієнт, дублюються між пристроями й не бачать листа до завантаження. Розбираємо, як побудувати продакшн-пайплайн на Dovecot Pigeonhole — глобальні sieve_before, персональні скрипти через ManageSieve, fileinto з автостворенням папок, vacation без петель і переадресацію, що не ламає SPF.
EvilMail Team9 липня 2026 р.14 хв читання
Чому клієнтські фільтри — це ілюзія порядку
Правило у Thunderbird, яке розкладає листи по папках, працює рівно доти, доки Thunderbird відкритий і синхронізований. Закрили ноутбук — сортування зупинилось. На телефоні цього правила взагалі немає, бо воно живе в профілі десктопного клієнта. А якщо ви ще й забираєте пошту по POP3, лист устигає впасти в INBOX до того, як будь-яке правило його побачить.
Це не дрібна незручність, а принципова хиба архітектури: клієнтське правило — це постобробка вже доставленого листа, прив'язана до конкретного пристрою.
Sieve вирішує проблему на іншому рівні. Це декларативна мова фільтрації пошти (RFC 5228), яку виконує сам сервер — точніше, агент локальної доставки Dovecot — у момент, коли лист лягає в поштову скриньку. Один скрипт на сервері дає однакову поведінку на всіх пристроях одночасно, незалежно від того, чи хтось узагалі залогінений. Мова навмисно обмежена: жодних циклів, змінних без явного розширення, зовнішніх викликів чи запуску процесів. Це не Bash — фільтр не може «зависнути» чи виконати довільний код, тому давати його користувачам безпечно.
Реалізація Sieve для Dovecot називається Pigeonhole (пакет dovecot-sieve або dovecot-pigeonhole
залежно від дистрибутива). Далі — як зібрати з нього робочий продакшн-пайплайн.
Архітектура доставки: де саме спрацьовує Sieve
Щоб фільтри не здавалися магією, треба бачити шлях листа. Postfix приймає пошту по SMTP і передає її Dovecot через LMTP (або старіший dovecot-lda). Саме на цьому кроці — до запису в Maildir — вмикається Sieve-движок.
Ключова ідея на діаграмі — порядок трьох точок скриптів і принцип implicit keep. Якщо жоден fileinto не спрацював і не було явного discard, Sieve за замовчуванням залишає лист в INBOX. Це страховка: помилка в правилі не з'їдає пошту мовчки.
dovecot --version
doveadm sieve list # має відпрацювати без "unknown command"
Далі підключаємо плагін до транспортів доставки. Для LMTP (рекомендований варіант локальної доставки з Postfix — швидший і обробляє кількох одержувачів за сесію) редагуємо /etc/dovecot/conf.d/20-lmtp.conf, для класичного LDA — 15-lda.conf:
Рядок sieve = file:~/sieve;active=~/.dovecot.sieve означає, що персональні скрипти користувача лежать у каталозі ~/sieve, а активний зараз — це символьне посилання ~/.dovecot.sieve. У нашому стеку домашній каталог поштового користувача — це /var/mail/vhosts/<domain>/<user>/, і скрипти виконуються від його uid/gid (vmail, 5000:5000). Це важливо: якщо каталог зі скриптами належить root, Sieve мовчки не зможе їх скомпілювати.
Змушувати користувача правити файли через SSH — тупиковий шлях. Для цього є окремий протокол ManageSieve (RFC 5804) на порту 4190: клієнт завантажує, вмикає й перемикає скрипти по мережі. Вмикаємо його в /etc/dovecot/conf.d/20-managesieve.conf і додаємо sieve до списку протоколів:
protocols = imap lmtp sieve
service managesieve-login {
inet_listener sieve {
port = 4190
}
}
STARTTLS тут обов'язковий — скрипти проходять через ту саму автентифікацію, що й IMAP, і віддавати пароль відкритим текстом неприпустимо. Клієнти з підтримкою ManageSieve: плагін Sieve для Thunderbird, вебпошта Roundcube з плагіном managesieve або CLI sieve-connect для скриптованого деплою.
Перед тим як віддати скрипт у продакшн, скомпілюйте його вручну — синтаксична помилка ловиться на цьому кроці, а не під час доставки живого листа:
bash
sievec ~/sieve/personal.sieve # створює personal.svbin
Автосортування: fileinto, папки, спам і списки розсилки
Ось реальний персональний скрипт. Він розкладає розсилки за List-Id, спам за прапорцем від Rspamd і листи від конкретного відправника — у клієнтську папку:
require ["fileinto", "mailbox", "imap4flags"];
# Спам, позначений Rspamd/SpamAssassin
if header :is "X-Spam-Flag" "YES" {
fileinto "Junk";
stop;
}
# GitHub-нотифікації у власну папку (створити, якщо немає)
if header :contains "List-Id" "github.com" {
fileinto :create "Lists/GitHub";
stop;
}
# Рахунки від провайдера — помітити прапорцем і покласти в папку
if address :is "from" "[email protected]" {
addflag "\\Flagged";
fileinto :create "Finances";
stop;
}
Три речі тут критичні. По-перше, :create автоматично створює папку призначення разом із батьківськими (Lists/ теж з'явиться) — без нього fileinto у неіснуючу папку впаде назад в INBOX. По-друге, stop завершує виконання скрипта: без нього лист може пройти кілька умов і продублюватися в кількох папках. По-третє, порядок правил — це і є пріоритет: спам-перевірку ставимо першою, бо немає сенсу розкладати по темах те, що взагалі сміття.
Прапорці (setflag, addflag) з розширення imap4flags виставляються доfileinto і зберігаються разом із листом — можна одразу позначити прочитаними (setflag "\\Seen") шумні нотифікації.
Автовідповіді: vacation без нескінченних петель
Автовідповідь «мене немає в офісі» — класична пастка для наївних реалізацій, які відповідають на кожен вхідний лист, зокрема на власні автовідповіді співрозмовника. Розширення vacation спроєктоване так, щоб цього не сталося:
require ["vacation"];
vacation
:days 3
:subject "Поза офісом до 10 липня"
:addresses ["[email protected]", "[email protected]"]
"Дякую за листа. Я повернуся 10 липня та відповім особисто.";
Що vacation робить за вас автоматично: тримає базу адрес, яким уже відповів, і не турбує ту саму адресу протягом інтервалу :days; не відповідає на bulk- і list-пошту (розсилки з List-Id, Precedence: bulk); і не відповідає на порожній envelope-from, тобто на bounce-повідомлення. Саме останнє рятує від петель із чужими автовідповідачами.
Дві типові помилки. Перша: :days 1 під час тестування — ви надішлете один тестовий лист, отримаєте відповідь, а другий уже не спрацює, бо адреса «на кулдауні» добу; для тестів беріть :seconds 30. Друга, серйозніша: ніколи не ставте vacation у глобальний sieve_before без :addresses — інакше сервер почне відповідати від імені всіх користувачів на всю вхідну пошту.
Серверна переадресація, що не ламає SPF/DMARC
redirect "[email protected]"; виглядає невинно, але це найчастіша причина того, що переадресована пошта осідає у спамі одержувача. Причина — SPF.
Коли Sieve робить пряму переадресацію, він зберігає оригінальний envelope-from. Приймаючий сервер бачить лист з IP вашого сервера, але MAIL FROM домену відправника, у чиєму SPF вашого IP немає. Результат — SPF fail, а за ним і DMARC fail.
Правильних рішень два. Перше й основне — SRS (Sender Rewriting Scheme) на рівні Postfix через postsrsd: він переписує envelope-from на ваш домен, тож SPF проходить. Друге — залишати копію локально й не покладатися винятково на пересилання:
:copy кладе оригінал у вашу скриньку (implicit keep не скасовується) і паралельно надсилає копію. Від петель захищають два параметри: sieve_max_redirects (за замовчуванням 4) обмежує кількість пересилань на лист, а sieve_redirect_envelope_from = orig_recipient допомагає розірвати цикли між скриньками. Якщо мета — просто читати пошту з кількох адрес в одному місці, часто чесніше зробити fileinto плюс IMAP-клієнт, ніж переадресацію: жодного ризику для репутації домену.
Глобальні політики: sieve_before для всього сервера
sieve_before — це правила, які виконуються до персонального скрипта і які користувач не може перевизначити. Ідеальне місце для політик усього сервера. Файли в каталозі /etc/dovecot/sieve/before.d/ виконуються за алфавітом імен, тому нумеруйте їх:
# /etc/dovecot/sieve/before.d/10-quarantine.sieve
require ["fileinto", "mailbox", "relational", "comparator-i;ascii-numeric"];
# Жорсткий карантин за високим спам-скором
if header :value "ge" :comparator "i;ascii-numeric"
["X-Spam-Score"] ["10"] {
fileinto :create "Junk";
stop;
}
Ця діаграма прибирає найпоширенішу плутанину: «чому моє персональне правило не спрацювало». Відповідь — глобальний prefilter уже зробив stop на вищому рівні, і до персонального скрипта лист просто не дійшов. Проєктуючи sieve_before, ставте stop лише там, де рішення справді остаточне (спам, карантин), інакше ви заберете в користувачів контроль над їхньою поштою.
Дебаг і чекліст перед продакшеном
Найкорисніший інструмент — sieve-test. Він проганяє скрипт на реальному .eml-файлі й показує, яку дію Sieve прийняв би, нічого нікуди не доставляючи:
Прапорець -t - виводить трасування у stdout: видно кожну умову, що спрацювала, і фінальну дію (fileinto "Junk", keep, redirect). Якщо треба застосувати новий скрипт до вже доставлених листів — наприклад, розкласти накопичений INBOX — існує sieve-filter.
Чекліст перед тим, як віддати конфіг у прод:
Компіляція:sievec на кожному скрипті проходить без помилок, .svbin створюється.
Права: каталог ~/sieve і скрипти належать vmail:vmail (5000:5000), не root.
ManageSieve: порт 4190 слухається, STARTTLS увімкнено, автентифікація працює.
vacation: протестовано з :seconds, є :addresses, немає у глобальному sieve_before.
implicit keep: перевірено, що лист без збігів іде в INBOX, а не зникає.
redirect: налаштовано SRS (postsrsd) або :copy; sieve_max_redirects розумний.
Пріоритет:stop у sieve_before стоїть тільки на остаточних рішеннях.
Логи:/var/log/mail.log показує теги sieve без помилок компіляції під час доставки.
На інфраструктурі evilmail.pro цей самий стек — Pigeonhole поверх LMTP — стоїть за тимчасовими адресами, тому сортування й антиспам відпрацьовують ще до того, як лист узагалі стане видимим у скриньці. Механіка та сама, що описана вище; різниця лише в масштабі глобальних політик.