13 Декабрь 2016

Двухуровневое квотирование в Dovecot 2

Two-level quota with Dovecot 2

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

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

Рассмотрим механизм реализации такого двухуровневого квотирования используя возможности POP3 / IMAP4 сервера Dovecot 2.

1. Конфигурация и структура данных

Для демонстрации воспользуемся уже имеющейся системой на базе FreeBSD 11 c установленным серверов Dovecot последней версии.

root@beta:~ # uname -v
FreeBSD 11.0-RELEASE-p2 #0: Mon Oct 24 06:55:27 UTC 2016     root@amd64-builder.daemonology.net:/usr/obj/usr/src/sys/GENERIC
root@beta:~ # dovecot --version
2.2.27 (c0f36b0)

Данная система использует базу виртуальных пользователей — как доменов, так и отдельных почтовых ящиков, хранимую в базе данных MySQL в формате системы управления Postfixadmin.

root@beta:~ # pkg info | grep postfixadmin
postfixadmin-3.0               PHP web-based management tool for Postfix virtual domains and users

Структура таблиц этой базы c именем exim данных следующая.

root@beta:~ # mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 21361
Server version: 5.7.15-log Source distribution

Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

root@localhost [(none)]> SHOW TABLES FROM exim;
+-----------------------+
| Tables_in_exim        |
+-----------------------+
| admin                 |
| alias                 |
| alias_domain          |
| awl                   |
| config                |
| domain                |
| domain_admins         |
| fetchmail             |
| log                   |
| mailbox               |
| quota                 |
| quota2                |
| vacation              |
| vacation_notification |
+-----------------------+
14 rows in set (0,00 sec)
...

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

...
root@localhost [(none)]> SHOW COLUMNS FROM domain FROM exim;
+-------------+--------------+------+-----+---------------------+-------+
| Field       | Type         | Null | Key | Default             | Extra |
+-------------+--------------+------+-----+---------------------+-------+
| domain      | varchar(255) | NO   | PRI | NULL                |       |
| description | varchar(255) | NO   |     | NULL                |       |
| aliases     | int(10)      | NO   |     | 0                   |       |
| mailboxes   | int(10)      | NO   |     | 0                   |       |
| maxquota    | bigint(20)   | NO   |     | 0                   |       |
| quota       | bigint(20)   | NO   |     | 0                   |       |
| transport   | varchar(255) | NO   |     | NULL                |       |
| backupmx    | tinyint(1)   | NO   |     | 0                   |       |
| created     | datetime     | NO   |     | 2000-01-01 00:00:00 |       |
| modified    | datetime     | NO   |     | 2000-01-01 00:00:00 |       |
| active      | tinyint(1)   | NO   |     | 1                   |       |
+-------------+--------------+------+-----+---------------------+-------+
...

Поле quota описывает размер квоты для домена, а maxquota размер квоты, которая устанавливается по умолчанию для конкретного ящика в данном домене в мегабайтах (или иных единицах исходя из квантификатора, указанного в конфигурационном файле Postfixadmin).

root@beta:~ # cat /usr/local/www/postfixadmin/config.local.php | grep quota
$CONF['maxquota'] = '0';
$CONF['quota'] = 'YES';
$CONF['quota_multiplier'] = '1048576';
$CONF['used_quotas'] = 'YES';
$CONF['new_quota_table'] = 'YES';

Оба поля могут быть как заданы, так и быть равными 0. Последнее означает отсутствие назначенной квоты.

Само значение квоты для почтового ящика электронной почты, которое, очевидно, может отличаться от значения по умолчанию, хранится в таблице mailbox в поле quota. Причём, в отличие от таблицы domain, это значение сохраняется уже в байтах.

...
root@localhost [(none)]> SHOW COLUMNS FROM mailbox FROM exim;
+------------+--------------+------+-----+---------------------+-------+
| Field      | Type         | Null | Key | Default             | Extra |
+------------+--------------+------+-----+---------------------+-------+
| username   | varchar(255) | NO   | PRI | NULL                |       |
| password   | varchar(255) | NO   |     | NULL                |       |
| name       | varchar(255) | NO   |     | NULL                |       |
| maildir    | varchar(255) | NO   |     | NULL                |       |
| quota      | bigint(20)   | NO   |     | 0                   |       |
| local_part | varchar(255) | NO   |     | NULL                |       |
| domain     | varchar(255) | NO   | MUL | NULL                |       |
| created    | datetime     | NO   |     | 2000-01-01 00:00:00 |       |
| modified   | datetime     | NO   |     | 2000-01-01 00:00:00 |       |
| active     | tinyint(1)   | NO   |     | 1                   |       |
+------------+--------------+------+-----+---------------------+-------+
10 rows in set (0,01 sec)
root@localhost [(none)]> SELECT `username`, `quota` FROM `exim`.`mailbox`;
+-----------------+------------+
| username        | quota      |
+-----------------+------------+
| box@my.server   | 5368709120 |
...
| max@kostikov.co |          0 |
...
+-----------------+------------+
NNN rows in set (0,00 sec)
...

Кроме того, в той же MySQL базе в таблице quota2 данных хранятся и счётчики использованной квоты, т.н. "словари квот".

...
root@localhost [(none)]> SHOW COLUMNS FROM quota2 FROM exim;
+----------+--------------+------+-----+---------+-------+
| Field    | Type         | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| username | varchar(100) | NO   | PRI | NULL    |       |
| bytes    | bigint(20)   | NO   |     | 0       |       |
| messages | int(11)      | NO   |     | 0       |       |
+----------+--------------+------+-----+---------+-------+
3 rows in set (0,00 sec)
...

В поле username может фигурировать как имя ящика (обычно это логин пользователя в данной системе), так и имя домена в случае использования подоменного квотирования. Также, помимо объёма данных в байтах, учитывается и общее количество сообщений.

2. Настройка Dovecot 2

Настроим механизм двухуровневого квотирования "пользователь" - "домен" средствами Dovecot 2. При этом в случае наличия квоты для данного почтового ящика будет учитываться, в первую очередь, она. Далее будет использоваться уже квота, установленная для домена.

Для этих целей воспользуемся стандартным плагином quota в Dovecot 2. Конфигурация приобретёт следующий вид.

root@beta:~ # cd /usr/local/etc/dovecot/
root@beta:/usr/local/etc/dovecot # cat dovecot.conf
# --- by Max Kostikov (c) 2010...2016 v.20161119
...
mail_location = maildir:/var/mail/%d/%n
...
mail_plugins = acl quota trash
...
passdb {
  args = /usr/local/etc/dovecot/dovecot-sql.conf
  driver = sql
}
userdb {
  args = /usr/local/etc/dovecot/dovecot-sql.conf
  driver = sql
}
...
# -- Quota
plugin {
  quota = dict:user_quota::proxy::sqluserquota
  quota_rule2 = Trash:storage=+10%%
  quota_rule3 = Junk:storage=+10%%
  quota_warning = storage=100%% quota-exceeded 100 %u
  quota_warning2 = storage=95%% quota-warning 95 %u
  quota_warning3 = storage=90%% quota-warning 90 %u
  quota_warning4 = storage=75%% quota-warning 75 %u
  quota2 = dict:domain_quota:%d:proxy::sqldomainquota
}
dict {
  sqluserquota = mysql:/usr/local/etc/dovecot/dovecot-dict-sql-user.conf
  sqldomainquota = mysql:/usr/local/etc/dovecot/dovecot-dict-sql-domain.conf
}
service dict {
  unix_listener dict {
  user = mailnull
  mode = 0660
  }
}
service quota-warning {
  executable = script /usr/local/etc/dovecot/quota_warning.sh
  unix_listener quota-warning {
  user = mailnull
  mode = 0660
  }
}

# -- Trash
plugin {
  trash = /usr/local/etc/dovecot/dovecot-trash.conf
}
...
protocol imap {
  mail_plugins = $mail_plugins antispam imap_acl imap_quota
  imap_client_workarounds = delay-newmail tb-extra-mailbox-sep
}
...

Здесь в значении параметра mail_plugins включаются плагины quota, который, собственно, и реализует сам механизм, а также плагин trash, который по определённому алгоритму очищает папки в случае достижения размера ограничений с тем, чтобы не терять в этом случае входящую почту. В тех же целях к размеру квоты дополнительно даётся по 10% для папок Junk (спам) и Trash (удалённые) — см. quota_rule. Также для целей информирования пользователей о приближении к исчерпанию квоты используется специальный механизм информирования с интервалами, определёнными в параметрах quota_warning путём рассылки специального сообщения.

Запросы к учётным записям пользователей в базе данных MySQL оформлены в дополнительном конфигурационном файле dovecot-sql.conf. При этом устанавливаются размеры квот для данного пользователя и / или домена.

root@beta:/usr/local/etc/dovecot # cat dovecot-sql.conf
driver = mysql
connect = host=localhost dbname=exim user=user password=password
default_pass_scheme = BLF-CRYPT
password_query    = SELECT `username` AS `user`, `password` \
        FROM `mailbox` WHERE `username` = LCASE('%u') \
        AND `active` = '1'
user_query    = SELECT CONCAT('/var/mail/', LCASE(maildir)) AS home, \
        CONCAT('*:bytes=', mailbox.quota) AS quota_rule, \
        CONCAT('*:bytes=', domain.quota, 'M') AS quota2_rule \
        FROM mailbox, domain \
        WHERE username = LCASE('%u') AND mailbox.active = '1' \
        AND domain.domain = '%d' AND domain.active = '1'

Словари квот также представлены в отдельных файлах конфигурации.

root@beta:/usr/local/etc/dovecot # cat dovecot-dict-sql-domain.conf 
connect = host=localhost dbname=exim user=user password=password
map {
    pattern = priv/quota/storage
    table = quota2
    username_field = username
    value_field = bytes
}
map {
    pattern = priv/quota/messages
    table = quota2
    username_field = username
    value_field = messages
}
root@beta:/usr/local/etc/dovecot # cat dovecot-dict-sql-user.conf
connect = host=localhost dbname=exim user=user password=password
map {
  pattern = priv/quota/storage
  table = quota2
  username_field = username
  value_field = bytes
}
map {
  pattern = priv/quota/messages
  table = quota2
  username_field = username
  value_field = messages
}

Они, как видно, идентичны.

Скрипт отправки сообщения о приближении к исчерпанию установленной квоты quota_warning.sh.

root@beta:/usr/local/etc/dovecot # ll quota_warning.sh 
-rwxr-xr-x  1 mailnull  mail  302  4 дек 23:16 quota_warning.sh*
root@beta:/usr/local/etc/dovecot # cat quota_warning.sh 
PERCENT=$1
USER=$2
cat << EOF | /usr/local/libexec/dovecot/dovecot-lda -d $USER -o "plugin/quota=maildir:User quota:noenforcing"
From: postmaster@my.server
Subject: Quota warning
X-Priority: 2

Your mailbox is now $PERCENT% full.
Please remove unnecessary information to avoid new mail loss.
EOF

И, наконец, конфигурация плагина автоочистки папок почтового ящика Trash в случае исчерпания квоты содержится в файле dovecot-trash.conf. Он определяет порядок освобождения дискового пространства.

root@beta:/usr/local/etc/dovecot # cat dovecot-trash.conf 
1 Trash
2 Junk
3 Draft

Теперь достаточно лишь перезапустит службу Dovecot 2 и наслаждаться результатом.

root@beta:~ # service dovecot restart
Stopping dovecot.
Waiting for PIDS: 74642.
Starting dovecot.

Имейте ввиду, что если вы подключаете мехнизм квотирования для уже работающих почтовых ящиков, то для начальной установки значения квот в словаре вам потребуется вручную пересчитать квоты каждого пользователя при помощи утилиты администрирования Dovecot doveadm quota recalc. При этом следует учитывать, что в настоящее время она некорректно обрабатывает квоту для домена, поэтому начальное значение её для данного случая нужно будет скорректировать непосредственно в базе данных на основании сумм актуальных значений принадлежащих ему почтовых ящиков.

3. PROFIT!

Статья была полезной? Тогда прошу не стесняться и поддерживать деньгами через PayPal или Яндекс.Деньги.


dovecot  MySQL