Розгортання Roundcube: тонкий вебклієнт над Dovecot, а не другий поштовий сервер
Roundcube не зберігає жодного листа — він лише показує те, що живе в Dovecot Maildir. Розбираємо продакшн-конфіг 1.6.x: TLS-стик з Dovecot і Postfix, збіг special-use тек, Sieve через managesieve і три плагіни, які реально варті включення.
EvilMail Team20 липня 2026 р.11 хв читання
Лист відправився, у логах Postfix status=sent, отримувач його бачить — а в теці Sent порожньо. Класика. І це не баг Roundcube. Це симптом того, що Roundcube і Dovecot дивляться на дві різні IMAP-теки під надіслані листи: клієнт складає копію в Sent, а Dovecot оголошує special-use \Sent на теці Надіслані. Дві теки, один зміст розмазаний навпіл.
Перш ніж чіпати config, треба прийняти одну модель світу: Roundcube не зберігає пошту. Взагалі. Це stateless IMAP/SMTP-клієнт на PHP. Стан — листи, теки, прочитаність, прапорці — живе в Dovecot Maildir. Власна БД Roundcube тримає лише UI-речі: контакти, ідентичності, налаштування, кеш. Тому будь-яка «зникла папка» чи «втрата листів» — це діагностика Dovecot, а не PHP. І 90% болю при розгортанні народжується саме на стику клієнта з Dovecot та Postfix.
Roundcube 1.6.x + Dovecot: правильне розгортання вебпошти та плагінів — EvilMail Blog
Архітектура: де закінчується Roundcube і починається Dovecot
Потік простий: браузер по HTTPS стукає в Roundcube, той відкриває IMAP-сесію до Dovecot (порт 993 через ssl:// або 143 через STARTTLS) і SMTP submission до Postfix на 587. Окремим каналом іде ManageSieve на 4190 — для серверних фільтрів.
Власна БД Roundcube (MySQL 8 / MariaDB / PostgreSQL) містить таблиці users, identities, contacts, contactgroups, cache, cache_index, cache_messages, dictionary. Жодного тіла листа там немає — кеш cache_messages це лише прискорення повторного рендеру, його можна вигребти будь-коли без втрат. Самі листи лежать у /var/mail/vhosts/domain/user/ як Maildir-файли, якими володіє Dovecot.
Практичний наслідок цієї моделі: видалення листа в UI — це IMAP EXPUNGE у Dovecot, тека в інтерфейсі — це IMAP-папка в Maildir, перетягування — це MOVE. Roundcube нічого не «вирішує» сам, він транслює кліки в IMAP-команди. Тримайте це в голові, і половина питань відпаде ще до того, як ви відкриєте лог.
Встановлення 1.6.x правильно: не «розпакував zip у web-root»
Актуальна стабільна гілка станом на 2026 — 1.6.x: skin Elastic за замовчуванням, PHP 8.1+, config.inc.php замість давно мертвого main.inc.php/db.inc.php. Беріть complete-tarball з релізів (він уже містить вендор-залежності) або git + composer install --no-dev. Ключове — не тримати весь код у DocumentRoot: назовні дивиться лише public_html/.
bash
# окремий DB-користувач тільки для Roundcube, без зайвих прав
mysql -e "CREATE DATABASE roundcube CHARACTER SET utf8mb4;"
mysql -e "CREATE USER 'roundcube'@'localhost' IDENTIFIED BY 'REPLACE_ME';"
mysql -e "GRANT ALL ON roundcube.* TO 'roundcube'@'localhost';"
# схема
bin/initdb.sh --dir=SQL
# права: тільки temp/ і logs/ мають бути writable для веб-процесу
chown -R www-data:www-data temp/ logs/
chmod 640 config/config.inc.php
Мінімальний config/config.inc.php, з якого варто стартувати — навмисно тонкий:
smtp_user = '%u' / smtp_pass = '%p' означає «використай ті ж креденшали, з якими користувач залогінився в IMAP» — так submission автентифікується від імені реального користувача, і Postfix ставить правильний From. des_key шифрує збережений у сесії IMAP-пароль; дефолтне значення з прикладу — публічно відоме, лишити його = віддати сесії будь-кому. Ключ має бути рівно 24 символи — цього вимагає 3DES.
Стик з Dovecot: TLS, special-use, namespace
Це найважливіша секція, бо саме тут ламається той сценарій з порожнім Sent.
Вибір транспорту. ssl://host:993 — імпліцитний TLS: з'єднання зашифроване з першого байта. tls://host:143 — STARTTLS поверх відкритого порту. Обидва валідні; 993 трохи безпечніший, бо немає вікна відкритого plaintext перед STARTTLS. Ніколи не лишайте imap_host без схеми — це plaintext на 143.
Тепер special-use. RFC 6154 дозволяє серверу позначати теки ролями \Sent, \Drafts, \Junk, \Trash незалежно від їхніх імен. Roundcube 1.6 вміє це читати через LIST-EXTENDED — але лише якщо ви не захардкодили імена, які розходяться з Dovecot. У Dovecot 15-mailboxes.conf:
namespace inbox {
mailbox Sent { special_use = \Sent auto = subscribe }
mailbox Drafts { special_use = \Drafts auto = subscribe }
mailbox Junk { special_use = \Junk auto = subscribe }
mailbox Trash { special_use = \Trash auto = subscribe }
}
auto = subscribe критичне: без нього тека існує в Maildir, але не потрапляє в LSUB — і користувач її просто не бачить, доки вручну не підпишеться. Саме тому найкраще класти special-use на англомовні технічні імена (Sent, Drafts), а локалізацію робити на рівні відображення в скіні. Тоді перемикання мови інтерфейсу не плодить дублів Sent / Надіслані.
Ще одна тиха пастка — hierarchy separator. Maildir++ використовує . як роздільник вкладеності, а LAYOUT=fs — /. Якщо конфіг namespace у Dovecot не збігається з тим, що очікує клієнт, вкладені теки Work/Clients розсипаються на плоскі. Перевіряйте separator у блоці namespace inbox.
Плагіни: три варті, решта — поверхня атаки
Кожен увімкнений плагін — це PHP-код, що виконується в периметрі автентифікованого користувача. Тому список плагінів — це рішення про безпеку, а не про «фічі».
Варті беззастережно:
managesieve — серверні фільтри через ManageSieve. Причина нижче, окрема секція.
password — зміна пароля з вебпошти. Для стеку, де паролі лежать у SQL зі схемою PLAIN (uid/gid 5000 у нашому випадку), драйвер — sql з явним UPDATE-запитом. Не тягніть chpasswd/ldap-драйвери, якщо бекенд не такий.
zipdownload — вивантаження кількох вкладень/листів одним zip. Дрібниця, але користувачі просять постійно.
Опційно, за реальної потреби: archive (кнопка «в архів» одним кліком) і newmail_notifier (браузерні сповіщення). Не чіпайте без причини acl, calendar, будь-які сторонні плагіни без явної підтримки 1.6/Elastic — вони або тягнуть окремий бекенд-стек, або ламаються на новому скіні, або обидва.
Драйвер password для SQL-бекенда налаштовується в plugins/password/config.inc.php:
php
$config['password_driver'] = 'sql';
$config['password_query'] = "UPDATE virtual_users
SET password = %P WHERE email = %u";
%P тут — це хеш згідно з password_dovecot_scheme (для PLAIN-схеми у цьому стеку ви ставите відповідне значення). Обов'язково перевірте вживу: змінили пароль у вебпошті → розлогінились → зайшли з новим. Плагін, який мовчки нічого не оновлює в бекенді, — гірший за відсутній.
Sieve через managesieve: фільтри, що переживуть зміну клієнта
Спокуса зробити фільтри «в базі Roundcube» або самописними правилами — головна помилка новачка. Такі правила спрацьовують лише коли ви відкрили браузер. Серверний Sieve працює на доставці: Dovecot LDA/LMTP проганяє скрипт у момент, коли лист падає в поштову скриньку, незалежно від того, чи Roundcube взагалі запущений. Вимкнули вебпошту на ніч для оновлення — фільтри все одно розкладають пошту.
GUI редагує саме цей текст — нічого магічного під кнопками немає. Vacation-автовідповідь із розширенням +vacation виглядає так само декларативно. Хочете переконатись, що скрипт активний на сервері — doveadm sieve list -u user@domain і doveadm sieve get -u user@domain <name>.
Що насправді робить кнопка в UI
Демістифікуємо панель. Кожна дія в інтерфейсі — це конкретна IMAP або ManageSieve-команда до сервера.
Звідси випливає діагностика. «Тека є в Maildir, але у вебпошті її не видно» — це майже завжди питання підписки (LSUB), а не існування теки. Перевіряйте прямо на сервері, минаючи клієнт:
Якщо тека є в першому виводі, але немає в другому — вона просто не підписана. Один doveadm mailbox subscribe або auto = subscribe у конфізі це лікує.
Продакшн-загартовка
Roundcube «з коробки» — не продакшн. Мінімум перед публічним доступом:
Reverse-proxy nginx з TLS, HSTS і рештою security-заголовків спереду; PHP-FPM не дивиться назовні.
enable_installer = false і фізично заблокований доступ до installer/ — цей майстер віддає конфіг і дозволяє тести з'єднань. Найкраще каталог просто видалити.
Сесійний cookie з Secure + HttpOnly, session_lifetime = 10 (хвилин), обережно з ip_check = true — за проксі він може ламати сесії, якщо не прокинутий реальний IP.
Окремий fail2ban jail на logs/errors.log з фільтром на рядки FAILED login — Roundcube чесно їх логує.
config.inc.php поза git і поза web-root (або 640, власник — не веб-процес). des_key і DB-пароль ніколи не потрапляють у репозиторій.
Оновлення — через bin/installto.sh /path/to/roundcube, а не «розпакувати новий zip зверху»; він коректно мігрує схему і зберігає конфіг.
Чеклист перед відкриттям доступу
enable_installer = false, каталог installer/ видалено або закрито
writable лише temp/ і logs/, config.inc.php — 640
special-use теки в Roundcube (sent_mbox тощо) збігаються з блоками mailbox {} у Dovecot, усі з auto = subscribe
managesieve_host б'ється до 4190/TLS, doveadm sieve list показує активний скрипт
тестовий лист туди-назад із перевіркою, що копія лягла саме в одну теку Sent
Тримайте Roundcube настільки тонким, наскільки дозволяє задача, а весь стан вантажте на Dovecot — і той клас «багів вебпошти», що з'їдає вечори, просто перестане існувати, бо його ніде буде народитись.