17 Июнь 2017

Автоматизация удаления неиспользуемых почтовых ящиков совместно с Dovecot

Automate inactive mailboxes removal with Dovecot

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

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

Рассмотрим реализацию системы автоматического удаления неиспользуемых почтовых ящиков совместно с популярным сервером IMAP4/POP3 Dovecot. В данном случае используется его последняя актуальная версия в среде FreeBSD.

root@beta:~ # uname -v
FreeBSD 11.0-RELEASE-p9 #0: Tue Apr 11 08:48:40 UTC 2017     root@amd64-builder.daemonology.net:/usr/obj/usr/src/sys/GENERIC
root@beta:~ # dovecot --version
2.2.30.2 (c0c463e)

1. База данных

Хранение учётных записей пользователей осуществляется в базе данных формата Postfixadmin в MySQL. В данном случае она носит имя exim.

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

Copyright (c) 2000, 2017, 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)]> USE exim;
Database changed
root@localhost [exim]> SHOW TABLES;
+-----------------------+
| 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)
...

Основные учётные данные хранятся в таблице mailbox которая имеет следующую структуру.

...
root@localhost [exim]> DESCRIBE mailbox;
+------------+--------------+------+-----+---------------------+-------+
| 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   |     | CURRENT_TIMESTAMP   |       |
| modified   | datetime     | NO   |     | 2000-01-01 00:00:00 |       |
| active     | tinyint(1)   | NO   |     | 1                   |       |
+------------+--------------+------+-----+---------------------+-------+
10 rows in set (0.00 sec)
...

Как видно, в ней присутствуют все основные данные, за исключением нужной нам для контроля даты последнего использования поля, информацию в которое будет помещаться через специальное расширение Dovecot под названием lastlogin в формате UNIX time.

Не откладывая дело на потом, добавим новое поле lastlogin в таблицу mailbox. Для хранения времени в таком формате нам будет достаточно поля типа INT(11). Для уже дейcтвующих почтовых систем также разумно будет заполнить его по умолчанию текущим временем в том же UNIX time.

...
root@localhost [exim]> ALTER TABLE `mailbox` ADD COLUMN `lastlogin` INT(11) DEFAULT NULL AFTER `modified`;
Query OK, nnn rows affected (0.15 sec)
Records: nnn  Duplicates: 0  Warnings: 0

root@localhost [exim]> UPDATE `mailbox` SET `lastlogin` = UNIX_TIMESTAMP();
Query OK, nnn rows affected (0.15 sec)
Records: nnn  Duplicates: 0  Warnings: 0

root@localhost [exim]> SELECT `lastlogin` FROM `mailbox` LIMIT 3;
+------------+
| lastlogin  |
+------------+
| 1497724320 |
| 1497724320 |
| 1497724320 |
+------------+
3 rows in set (0.00 sec)

root@localhost [exim]> QUIT
Bye

Как видно, колонка была добавлена и заполнена в качестве значения по умолчанию текущим значением UNIX time.

2. Конфигурация Dovecot

Теперь перейдём к настройке самого раcширения lastlogin для Dovecot.

root@beta:~ # cd /usr/local/etc/dovecot/
root@beta:/usr/local/etc/dovecot # cat dovecot.conf
...
dict {
...
  lastlogin = mysql:/usr/local/etc/dovecot/dovecot-dict-sql-lastlogin.conf
...
}
...
protocol imap {
  mail_plugins = $mail_plugins antispam imap_acl imap_quota last_login
...
}
protocol pop3 {
  mail_plugins = $mail_plugins last_login
...
}
...
plugin {
  last_login_dict = proxy::lastlogin
}
...

Во-первых, в блок dict была добавлена ссылка на файл-описание словаря с именем dovecot-dict-sql-lastlogin.conf для работы с таблицей, хранимой в MySQL. Во-вторых, в протоколах доступа к почтовым ящикам imap и pop3 был активирован сам плагин lastlogin. Также можно описать его единожды в глобальной секции и тогда значения времени доступа будут обновляться также и при доставке почты по протоколу LMTP или через LDA. Однако, для нашей задачи такой подход будет неприменимым. И, наконец, в собственно настройке расширения был указан способ доступа к словарю через прокси.

Содержимое файла-описания словаря.

root@beta:/usr/local/etc/dovecot # cat dovecot-dict-sql-lastlogin.conf
connect = host=localhost dbname=exim user=user password=pass
map {
   pattern = shared/last-login/$user
   table = mailbox
   username_field = username
   value_field = lastlogin
   value_type = uint
   fields {
      username = $user
   }
}

Обновление поля lastlogin будет производиться по ключевому полю username.

Перезапустив Dovecot можно пронаблюдать работу плагина по обновлению значений поля lastlogin при каждом доступе пользователя к своему почтовому ящику посредством протоколов IMAP4 или POP3.

root@beta:/usr/local/etc/dovecot # service dovecot restart
Stopping dovecot.
Waiting for PIDS: 67651.
Starting dovecot.
root@beta:/usr/local/etc/dovecot # mysql -u root -p -D exim -e "SELECT username, lastlogin FROM mailbox WHERE lastlogin > 0 LIMIT 3"
Enter password:
+-----------------+------------+
| username        | lastlogin  |
+-----------------+------------+
| boo@my.domain   | 1497726124 |
| foo@my.domain   | 1497705698 |
| moo@my.domain   | 1497705653 |
+-----------------+------------+

3. Скрипт удаления неиспользуемых ящиков

Небольшой shell-скрипт под названием mboxcleanup.sh для автоматизации контроля использования почтовых ящиков на основании собираемой информации от расширения lastlogin и запускаемый по cron позволит решить поставленную задачу.

root@beta:/usr/local/etc/dovecot # ll mboxcleanup.sh
-rwxr-xr-x  1 mailnull  mail  1969 17 июня  17:35 mboxcleanup.sh*
root@beta:/usr/local/etc/dovecot # cat mboxcleanup.sh
#!/bin/sh

# Remove inactive users from Postfixadmin database and free disk space
# v.20170616 (c)2017 by Max Kostikov http://kostikov.co e-mail: max@kostikov.co
#
# Requires MySQL and Postfixadmin installed
#

## Settings
# db credentials
DBUSER="user"
DBPASS="pass"
DBNAME="exim"

# binaries and scripts
mysql=`which mysql`
SQL="$mysql -u $DBUSER -p$DBPASS -D $DBNAME -s --disable-column-names -e"
MYNAME=`basename $0`

# domains to check
DOM="my.domain"

# pathes
MDIR="/var/mail"

# logging
LOG="/var/log/mboxcleanup.log"

# timings in sec
exp="15552000"          # 180 days
cur=`date +'%s'`        # current
DEL=`expr $cur - $exp`

## Write to log function
#
logwrite ()
{
        echo "`date '+%Y-%m-%d %H:%M:%S'` $1" >> $LOG
}

## Main action
#
logwrite "Starting mailboxes cleanup"

for i in $DOM
do
        # find expired mailboxes
        req=`$SQL "SELECT local_part FROM mailbox WHERE lastlogin < $DEL AND domain = '$i'" 2>/dev/null`
        if [ -n "$req" ]; then
                logwrite "  Found `echo $req | wc -w | tr -d ' '` expired mailbox(es) for $i"
                for j in $req
                do
                        # get aliases list
                        rem=`$SQL "SELECT address FROM alias WHERE goto = '$j@$i'" 2>/dev/null`
                        for k in $rem
                        do
                                $SQL "DELETE FROM alias WHERE address = '$k' AND goto ='$j@$i'" 2>/dev/null
                                $SQL "INSERT INTO log (timestamp, username, domain, action, data) VALUES (NOW(), '`whoami` ($MYNAME)', '`echo $k | cut -d@ -f2`', 'delete_alias', '$k')" 2>/dev/null
                        done
                        # get mail location
                        dir=`$SQL "SELECT maildir FROM mailbox WHERE username = '$j@$i'" 2>/dev/null`
                        $SQL "DELETE FROM mailbox WHERE username = '$j@$i'" 2>/dev/null
                        $SQL "INSERT INTO log (timestamp, username, domain, action, data) VALUES (NOW(), '`whoami` ($MYNAME)', '$i', 'delete_mailbox', '$j@$i')" 2>/dev/null
                        tar -czf $MDIR/$(dirname $dir)/$j@$i.tar.gz $MDIR/$dir 2>/dev/null
                        rm -rf $MDIR/$dir
                        logwrite "    mailbox $j@$i removed, backup saved to $MDIR/$(dirname $dir)/$j@$i.tar.gz"
                done
        else
                logwrite "  Nothing to do for $i yet"
        fi
done

logwrite "Cleanup finished"

Вкратце, данный скрипт для списка доменов DOM проверяет дату последнего посещения пользователем своего ящика и если она была более чем 180 дней назад (см. переменную exp) то производится:

  • удаление ссылающихся на такой почтовый ящик псевдонимов;
  • удаляется запись о самом почтовом ящике получив предварительно путь к месту хранения почты;
  • содержимое ящика архивируется в файл с именем адреcа электронной почты пользователя и расширением tar.gz после чего содержимое также удаляется.

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

При этом, во-первых, ведётся лог-файл по указанному в переменной LOG пути, а, во-вторых, о проделанных действиях делаются запись в SQL-лог Postfixadmin с текстом user (mboxcleanup.sh) в поле пользователя, выполнившего данные операции. Если скрипт будет запускаться от имени mailnull, под которым работают все почтовые сервисы, что автор и рекомендовал бы, то предварительно следует создать пустой файл для записи протокола с соответствующими правами.

root@beta:/usr/local/etc/dovecot # touch /var/log/mboxcleanup.log
root@beta:/usr/local/etc/dovecot # chown mailnull:mail /var/log/mboxcleanup.log

Также начиная с версии 3.0 Postfixadmin можно заменить прямое обращение к таблицам базы данных из скрипта на использование специального PHP-модуля Postfixadmin-CLI, который позволяет выполнить базовые операции с базой данных пользователей через модули самой системы управления. Однако, на момент написания статьи он не обладает в полной мере требуемым функционалом, поэтому автор предпочёл использовать непосредственный доступ через интерфейс командной строки MySQL.

В случае, если вы используете на данной почтовой системе Spamassassin, то, вероятно, хорошей идеей при удалении почтового ящика будет и очистка базы данных bayes-сигнатур для данного пользователя по аналогичной методике.

Теперь осталось добавить скрипт в cron.

root@beta:/usr/local/etc/dovecot # cat /etc/crontab | grep mboxcleanup
0       6       *       *       *       mailnull /usr/local/etc/dovecot/mboxcleanup.sh >/dev/null 2>&1

Проблема решена!

4. PROFIT!

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


dev  dovecot  MySQL  shell