четверг, 24 ноября 2011 г.

Настройка квот в Dovecot и Postfix с хранением счётчиков и правил в словаре MySQL

Уже третий день подряд мучился с настройкой квот в Dovecot прямо на работающем сервере. Мучился очень аккуратно и имея в запасе бэкапы рабочих конфигураций на начало каждого дня.

Хранение счётчиков квот в MySQL настроил быстро, формирование правил квот по данным из MySQL тоже настроил быстро. Однако после настройки текущие значения счётчиков квот становились отрицательными и со временем только уменьшались. Я сразу подумал, что POP3-сервер обновляет счётчики квот, т.к. его задача лишь забирать почту из ящика, а вот LDA (агент локальной доставки) - не обновляет, т.к. именно он кладёт почту в ящики и должен увеличивать значения счётчиков.

В конечном итоге оказалось, что вместо LDA delivery из Dovecot по-прежнему используется LDA virtual из Postfix. Произошло это потому что действие настройки virtual_transport = dovecot перекрывается настройкой transport_maps = mysql:/etc/postfix/sql/transport.cf, где для тестируемого домена был настроен транспорт virtual. Здесь можно поблагодарить Вьетце Венема за отличную документацию - это я прочитал прямо в man 5 postconf. Dovecot практически не документирован, а вики-страницы с Howto по настройке различных фишек Dovecot за документацию я не считаю.

После исправления значения транспорта в базе данных возникло ещё несколько проблем:
1. В настройках LDA Dovecot не был указан параметр postmaster_address,
2. Не был настроен сокет /var/run/dovecot/auth-master,
3. Имена каталогов, в которые LDA delivery из Dovecot складывал почту, были строчными, а имена существующих каталогов почтовых ящиков, в которые складывал почту LDA virtual из Postfix, содержали символы в разном регистре.

Проблемы решались следующим образом:
1. Указан параметр postmaster_address,
2. Прописана секция master в секции socket listen из секции auth default,
3.1. Добавлены буквы L в настройке mail_location, которые указывают принудительно использовать нижний регистр для каталогов почтовых ящиков.
3.2. Использована команда rename 'y/A-Z/a-z/' * для того, чтобы переименовать каталоги почтовых ящиков. Некоторые каталоги, из которых уже успели попробовать забрать почту, пришлось обрабатывать вручную - нужно было перенести письма из старых каталогов в новые, а старые затем удалить.

Итоговые конфигурационные файлы Dovecot помещаю сюда, чтобы в следующий раз не наступать на те же грабли:
protocols = pop3
disable_plaintext_auth = no
log_timestamp = "%Y-%m-%d %H:%M:%S "
mail_location = maildir:/var/mail/virtual/%Ld/%Ln
first_valid_uid = 999
first_valid_gid = 999

dict {
  quotadict = mysql:/etc/dovecot/dovecot-dict-mysql.conf
}
plugin {
  quota = dict:user::proxy::quotadict
  # Квота по умолчанию - объём ящика 1 гигабайт,
  # не более 1000 писем в ящике
  # Эта квота заменяется квотой, найденной в БД
  quota_rule = *:storage=1G:messages=1000
}
protocol pop3 {
  mail_plugins = quota
  mail_executable = /etc/dovecot/pop-update-lastlog.sh
}
protocol lda {
  # Ящик администратора почтовой системы
  postmaster_address = postadmin@domain.tld
  mail_plugins = quota
}

# Домен по умолчанию для пользователей, пытающихся
# аутентифицироваться в Dovecot без указания домена
auth_default_realm = domain.tld

auth default {
  mechanisms = plain login
  passdb sql {
    args = /etc/dovecot/dovecot-mysql.conf
  }
  userdb sql {
    args = /etc/dovecot/dovecot-mysql.conf
  }
  socket listen {
    client {
      path = /var/spool/postfix/private/auth
      mode = 0660
      user = postfix
      group = postfix
    }
    master {
      path = /var/run/dovecot/auth-master
      mode = 0660
      user = vmail
      group = vmail
    }
  }
}
Уже знакомый скрипт /etc/dovecot/pop-before-smtp.sh для аутентификации POP before SMTP:
#!/bin/sh

mysql -uuser -ppassword -h127.0.0.1 mail <<END
UPDATE users SET lasttime = NOW(), lastip='$IP' WHERE login = '$USER';
END
exec /usr/lib/dovecot/pop3 "$@"
Файл настроек для хранения счётчиков квот в БД MySQL /etc/dovecot/dovecot-dict-mysq.conf:
connect = host=127.0.0.1 dbname=mail user=user password=password
map {
  pattern = priv/quota/storage
  table = users
  username_field = login
  value_field = bytes
}
map {
  pattern = priv/quota/messages
  table = users
  username_field = login
  value_field = messages
}
Файл настроек для проверки учётных данных пользователя и его квот по таблице в БД MySQL /etc/dovecot/dovecot-mysql.conf:
driver = mysql
connect = host=127.0.0.1 dbname=mail user=user password=password
default_pass_scheme = CRYPT
password_query = SELECT password \
                 FROM users \
                 WHERE login = '%u'
user_query = SELECT CONCAT(SUBSTRING_INDEX(login, '@', -1), '/', SUBSTRING_INDEX(login, '@', 1), '/'), \
                    999 AS uid, \
                    999 AS gid, \
                    CONCAT('*:bytes=', max_bytes, ':messages=', max_messages) AS quota_rule \
             FROM users \
             WHERE login = '%u'
В настройки postfix добавлен LDA delivery из Dovecot, в файл /etc/postfix/master.cf добавлены две строчки:
dovecot unix - n n - - pipe
flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${recipient}
Столбцы со счётчиками и со значениями квот для пользователей добавлены с помощью четырёх SQL-запросов:
ALTER TABLE users ADD COLUMN bytes bigint default 0;
ALTER TABLE users ADD COLUMN messages integer default 0;
ALTER TABLE users ADD COLUMN max_bytes bigint default 1073741824;
ALTER TABLE users ADD COLUMN max_messages bigint default 1000;
Значения квот по умолчанию можете поменять на свои. Я поставил ограничения в 1 гигабайт на общий объём ящика и не более 1000 писем в одном почтовом ящике.

В таком виде Dovecot не будет принимать от Postfix письма для адресатов, превысивших квоту. Но такие письма будут просто попадать в очередь отложенных писем Postfix. Нужно же, чтобы письмо не занимало дисковое пространство ни в почтовом ящике получателя, ни в очереди отложенных писем, а отклонялось сразу же с соответствующим сообщением отправителю о том, что адресат превысил квоту.

Для этого нужно настроить проверку квот при приёме почты самим Postfix'ом. Для этого воспользуемся правилом check_recipient_access mysql:/etc/postfix/sql/quotas.cf, которое нужно поместить в том числе до правил permit_mynetworks и permit_sasl_authenticated. Если поместить его после этих двух проверок, то квоты получателя просто не будут проверяться, если отправитель аутентифицировался на почтовом сервере или он отправляет почту из доверенной сети.

Файл /etc/postfix/sql/quotas.cf для проверки квоты получателя:
user = user
password = password
dbname = mail
hosts = 127.0.0.1
query = SELECT '452 Mailbox is over quota'
        FROM users
        WHERE login = '%s'
          AND ((bytes >= max_bytes AND max_bytes > 0)
            OR (messages >= max_messages AND max_messages > 0))
О расширенных статусах REJECT я прочитал в man 5 access, а подходящий код статуса подобрал в RFC2821 на этой странице: http://tools.ietf.org/html/rfc2821.

В случае если квота по объёму или по количеству сообщений равна нулю, то этот SQL-запрос такую квоту не проверяет. Таким образом значения 0 в полях max_bytes или max_messages означают, что ограничение по этому параметру не действует.

При попытке отправить письмо на ящик, превысивший квоту, Outlook Express сообщает следующее:
Не удается отправить сообщение, поскольку сервер отказался принять адрес одного из получателей. В письме был указан адрес: 'user@domain.tld'. Тема 'test1', Учетная запись: 'mail', Сервер: 'domain.tld', Протокол: SMTP, Ответ сервера: '452 4.7.1 <user@domain.tld>: Recipient address rejected: Mailbox is over quota', Порт: 25, Защита (SSL): Нет, Ошибка сервера: 452, Код ошибки: 0x800CCC79

5 комментариев:

Анонимный комментирует...

А проверялось что письмо возвращается на тот же mail.ru? Просто с 452 кодом ни мэйл.ру, ни гмыло, ни любой другой в моем случае не захотели генерировать анделайверед сообщение пока не поставил 550 или 551 код.

morbo комментирует...

Может быть сгенерируют, но попозже, когда письмо проваляется в очереди недоставленным с недельку? Потому что 452 код - это код временной ошибки.

Я считаю, что правильно использовать именно его, потому что в другой раз то же самое письмо может дойти, если адресат почистит свой ящик и квота перестанет действовать.

И потом, этот код описан в RFC именно для этого случая.

Анонимный комментирует...

А как можно настроить квоту не на каждый ящик по отдельности, а на домен?

morbo комментирует...

Можно переделать SQL-запрос так, чтобы он суммировал место, занятое письмами во всех почтовых ящиков домена проверяемого ящика и сравнивал это место с лимитом из другой таблицы.

Хотя мне квоты для домена кажутся несколько глупыми. Представьте, что есть ящик, на который приходит масса спама, но владелец которого им уже не пользуется. В итоге его ящик будет использовать 99% квоты, а остальные, активные пользователи, будут ютиться на 1% квоты, удаляя даже те письма, которые они хотели бы оставить.

Анонимный комментирует...

Благодаря тому, что действуют квоты - ящик не переполнится, так как не примет писем сверх нормы. В итоге bytes всегда меньше max_bytes и до ошибки 452 дело не доходит. Воркераунды находили?