Квоты почтовых ящиков в Dovecot: quota-status, предупреждения и мягкие/жёсткие лимиты без backscatter
Квота в Dovecot — это не про гигабайты, а про репутацию IP. Разбираем backend count, отклонение переполненных ящиков на RCPT TO через quota-status, предупреждающие письма с флагом noenforcing и границу между grace и жёстким лимитом.
EvilMail Team10 июля 2026 г.11 мин чтения
Типичная авария выглядит так. У клиента ящик забит под завязку, 100%. Ваш Postfix спокойно принимает входящее письмо с 250 OK, ставит его в очередь и отдаёт на LMTP в Dovecot. LMTP не может доставить — места нет — и возвращает ошибку. Postfix, приняв на себя ответственность за письмо, обязан сгенерировать bounce. А обратный адрес у спамерского письма, разумеется, подделан. В итоге ваш сервер льёт NDR на невиновного отправителя, которого спамер вписал в MAIL FROM. Это и есть backscatter, и за него прилетает в Spamhaus CBL и другие блэклисты, из которых потом выбираться неделями.
Квота — это не про экономию диска. Это про то, в какой момент SMTP-диалога вы говорите «нет» и кто в итоге отвечает за отбитое письмо. Всё, что ниже, крутится вокруг двух директив — quota-status (чтобы отказать ещё на RCPT TO) и quota_warning (чтобы предупредить пользователя до того, как ящик встанет колом).
Backend квоты: почему только `count`
У плагина quota четыре backend, и выбор здесь определяет, будете ли вы годами ловить рассинхрон.
`maildir` — читает и пишет файл maildirsize в каждом ящике. Дрейфует. Любая ручная операция с файлами Maildir мимо Dovecot (переложили, удалили rsync-ом, восстановили из бэкапа) — и счётчик врёт, пока не сделаете recalc.
`fs` — считает реальный размер обходом файловой системы, фактически du. Медленно на больших ящиках, а на системах с хардлинк-дедупликацией одинаковых сообщений размер вообще перестаёт соответствовать логике «сколько занял пользователь».
`dict` — хранит значение в отдельном словаре (SQL, Redis, файл) и правит его при каждой доставке и удалении. Читается быстро, но болезнь ровно та же, что у maildir: источник правды живёт сбоку от почты и рассинхронизируется при любой операции мимо Dovecot.
`count` — ничего отдельно не хранит. Dovecot и так кэширует виртуальный размер каждого сообщения в index-файлах ящика; count просто суммирует их на лету. Рассинхрон в штатной работе невозможен — значение всегда выводится из актуального состояния индексов, а не из счётчика, который кто-то мог забыть обновить.
В 2026 году вменяемый выбор один: count с обязательным quota_vsizes = yes. Последнее заставляет считать не размер файла на диске, а виртуальный RFC822-размер сообщения — это стабильно и не зависит от файловой системы, сжатия и дедупликации. При переходе со старого maildir ничего переносить руками не нужно: включаете count и прогоняете doveadm quota recalc -A — заполнение пересчитывается из индексов.
Базовая конфигурация плагина
Всё живёт в /etc/dovecot/conf.d/90-quota.conf. Сначала подключаем плагин, потом настраиваем.
Обратите внимание на %%. В dovecot.conf знак процента экранируется удвоением, иначе Dovecot попытается интерпретировать %u, %d и прочие переменные там, где вы имели в виду буквальный процент. Забытое экранирование — классическая причина, по которой конфиг молча применяется не так, как вы читаете его глазами.
quota = count:User quota задаёт backend и человекочитаемое имя корневой квоты («User quota» будет светиться в выводе doveadm). Backend count ничего дополнительно не требует — ни блока dict {}, ни quota_dict: всё, что ему нужно, уже лежит в индексах ящика.
Мягкий и жёсткий лимит: grace и персональные квоты
Жёсткий лимит — это значение из quota_rule, здесь 1 ГБ. Мягкий буфер сверху задаёт quota_grace: по умолчанию 10% от лимита, значение можно переопределить своим процентом или абсолютным числом. Ключевая деталь в поведении на границе: письмо, которое пересекает лимит, ещё принимается — оно попадает в grace-зону. А вот следующее письмо, когда ящик уже за пределом, отклоняется. То есть grace — это не «можно немного больше навсегда», а «последнее письмо не потеряется на самой границе».
Персональные квоты задаются через userdb. В SQL-запросе userdb возвращаете дополнительное поле, и оно переопределяет глобальное правило для конкретного пользователя:
sql
user_query = SELECT \
'/var/mail/vhosts/%d/%n' AS home, \
5000 AS uid, 5000 AS gid, \
concat('*:storage=', quota_bytes, 'B') AS quota_rule \
FROM virtual_users WHERE email = '%u'
Проверить, что персональная квота реально доехала до пользователя, а не осталась глобальной:
Вывод doveadm quota get покажет столбцы Type / Value / Limit / %. Правило quota_rule2 = Trash:storage=+100M даёт корзине отдельные 100 МБ поверх общего лимита, чтобы удаление писем не упиралось в ту же квоту. По той же логике часто исключают Junk — держать спам в счётчике пользователя бессмысленно.
quota-status: отказ на RCPT TO вместо backscatter
Это ядро всей затеи. Dovecot умеет работать как Postfix policy service: Postfix во время SMTP-диалога спрашивает у Dovecot, влезет ли письмо, и получает ответ ещё до того, как сказал отправителю 250 OK. Если ящик переполнен — Postfix отвечает удалённому серверу 552 5.2.2 прямо на RCPT TO, и генерацию NDR берёт на себя отправляющая сторона. Ваш IP не производит ни одного bounce.
Поднимаем сервис в Dovecot (/etc/dovecot/conf.d/90-quota.conf или отдельный файл):
service quota-status {
executable = quota-status -p postfix
inet_listener {
port = 12340
}
client_limit = 1
}
Ответы уже заданы в plugin {} выше: quota_status_success = DUNNO (в пределах квоты — пусть решают следующие проверки), quota_status_nouser = DUNNO (нет пользователя — не наше дело, разберутся recipient-проверки), quota_status_overquota = "552 5.2.2 Mailbox is full".
В Postfix (main.cf) добавляем проверку в smtpd_recipient_restrictions:
Порядок критичен. check_policy_service должен стоять послеreject_unauth_destination. Иначе Postfix будет дёргать quota-status для чужих доменов, для которых вы не являетесь получателем, — лишняя нагрузка и утечка информации о существовании ящиков. Сначала отсекаем релей, только потом спрашиваем про квоту своих.
Проверяется это одной telnet-сессией на переполненный ящик: подключаетесь к своему smtpd, делаете MAIL FROM, затем RCPT TO: <[email protected]> — и должны увидеть 552 5.2.2 Mailbox is full прямо в ответе на RCPT, без входа в DATA.
Предупреждающие письма при заполнении
Отказ на RCPT спасает вашу репутацию, но пользователю от этого не легче — он просто перестал получать почту. Поэтому его нужно предупредить заранее. За это отвечают quota_warning и quota_warning2 из блока plugin {}: при пересечении порога вверх (80% и 95%) Dovecot вызывает сервис quota-warning.
service quota-warning {
executable = script /usr/local/bin/quota-warning.sh
user = dovecot
unix_listener quota-warning {
user = vmail
}
}
Сам скрипт доставляет письмо-предупреждение через dovecot-lda. Здесь прячется самая коварная грабля всей темы — флаг noenforcing:
bash
#!/bin/sh
PERCENT=$1
USER=$2
cat << EOF | /usr/lib/dovecot/dovecot-lda -d "$USER" \
-o "plugin/quota=count:User quota:noenforcing"
From: [email protected]
Subject: Ваш почтовый ящик заполнен на ${PERCENT}%
Content-Type: text/plain; charset=UTF-8
Ящик $USER заполнен на ${PERCENT}%. Освободите место,
иначе новые письма перестанут приниматься.
EOF
Без :noenforcing доставка самого предупреждения проверяется квотой — а ящик на 95% или уже за 100%. Письмо-предупреждение не влезает, отбрасывается, и пользователь так ничего и не узнаёт: молча, без явной ошибки в логе. noenforcing отключает enforcement именно для этой доставки, и предупреждение проходит даже в grace-зоне.
Предупреждение отправляется один раз при пересечении порога вверх — Dovecot хранит признак «порог пройден» и не спамит на каждое новое письмо. Когда пользователь освободит место и опустится ниже порога, состояние сбросится, и следующее пересечение снова вызовет warning.
Эксплуатация и типичные грабли
Основные команды сопровождения:
bash
doveadm quota get -u [email protected] # текущее заполнение
doveadm quota recalc -u [email protected] # пересчёт по одному ящику
doveadm quota recalc -A # пересчёт по всем ящикам
recalc — ваш инструмент против рассинхрона. Любая ручная возня с Maildir мимо Dovecot (миграция, восстановление из бэкапа, rsync) делает index-кэш ящика неактуальным, и count начинает показывать неправду — лечится только пересчётом.
Грабли, на которые наступают чаще всего:
Забытый `:noenforcing` в warning-скрипте — предупреждения молча не доставляются, пользователь узнаёт о проблеме, только когда почта уже не идёт.
quota-status слушает, но bounce всё равно уходит — почти всегда это неверный порядок в smtpd_recipient_restrictions: проверка стоит до reject_unauth_destination или вообще не в той секции restrictions.
Включили `quota_vsizes = yes` на живой системе без recalc — старые размеры считались по файлу на диске, новые по vsize, значения не сходятся. Всегда recalc -A сразу после смены.
Слишком большой абсолютный grace на маленьких ящиках — если задать quota_grace фиксированным числом вместо процента, в буфер целиком влезает тяжёлое письмо там, где вы этого не ждали.
Права на `unix_listener quota-warning` — если сокет недоступен пользователю доставки (vmail), warning-сервис не вызывается, и предупреждений нет вообще.
Мониторинг стройте на doveadm quota get -A: разбирайте вывод скриптом, экспортируйте в Prometheus, вешайте алерт на всё, что перевалило за 90%. Так вы узнаёте о заполнении раньше пользователя.
Чек-лист перед продом
Backend — count с quota_vsizes = yes, не maildir, не fs и не dict.