Решение проблемы

Очистка и обслуживание почтовой базы postfix


Мне приходится много работать с почтовыми серверами на базе postfix с базой почты формата maildir. За несколько лет работы накопилось множество различных приемов по оптимизации работы и настройке. Сегодня решил собрать в кучу все более ли менее универсальные и полезные скрипты по автоматической очистке почтовой базы в postfix.

Содержание:

  • 1 Введение
  • 2 Простое удаление старых писем из ящика
  • 3 Массовая очистка корзин в почтовой базе postfix
  • 4 Удаление писем на основе содержимого письма
  • 5 Фильтрация писем на основе темы письма
  • 6 Еще несколько примеров работы с почтовой базой
  • 7 Заключение

Введение

Очистка и обслуживание почтовой базы postfix

Данная статья будет актуальна для тех, кто сам выполнил установку и настройку postfix или воспользовался готовой сборкой на базе iredmail. Это что касается материалов моего сайта. А в целом все описанное ниже будет актуально для любого почтового сервера, который хранит почту в формате maildir.

Скажу пару слов, почему именно maildir. Лично я этот формат использую за его удобство. В нем каждое письмо это отдельный файл, который можно посмотреть любым текстовым редактором. Эти файлы удобно бэкапить, анализировать содержимое, сортировать по каким-то признакам. В общем, с ними можно работать как с обычными текстовыми файлами. На основе этих плюсов и выполняется вся дальнейшая работа в статье. Из минусов вижу только один — огромное количество мелких файлов создают большую нагрузку на дисковую подсистему.

Приведу для наглядности пример, который позволит оценить нагрузку на диски. Для синхронизации с помощью rsync почтовой базы объемом примерно 1 терабайт, расположенной на raid10 обычных 3.5 sata дисков, на одиночный такой же диск для бэкапа, уходит где-то пару часов в основном на сравнение файлов между источником и приемником. Само копирование файлов проходит быстро, но чтобы сравнить изменения за день, приходится выполнять длительную операцию. При этом в целом работа пользователей (~30-40 человек) с этой базой вполне комфортна, каких-то тормозов не наблюдается.

То есть по сути, для такого количества пользователей, сервером может быть обычный десктопный компьютер с 2-4 обычными sata дисками. Хватит производительности любого процессора и примерно 2-4 гигабайта оперативной памяти. Отдельный вопрос, конечно, к надежности обычного системника. Я сервера на них не рекомендую собирать, но при большом желании можно.

Приведенные далее скрипты для очистки почтовой базы писались в разное время на разных серверах. Иногда может показаться, что все сделано нелогично или как-то сложно. Громоздкие конструкции часто возникали там, где появлялись проблемы с пробелами или спецсимволами в именах папок на русском языке, которые при переводе в UTF-7 (кодировка названия imap папок в dovecot) превращаются в весьма неудобные для обработки строки. Дальше будет понятно, что я имею ввиду.

Перейдем теперь к конкретным примерам.

Простое удаление старых писем из ящика

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

/usr/bin/find /data/mail/virtual/zabbix@mailsrv.ru/Maildir/*/ -type f -mtime +30 -exec rm {} ;
/usr/bin/find Путь до утилиты find. Проверьте его актуальность в своем дистрибутиве.
/data/mail/virtual/reports@eme.ru/Maildir/*/ Путь до конкретного ящика. Конструкция /*/ позволяет сразу проверить обе папки new и cur.
-type f Говорим find, что ищем только файлы.
-mtime +30 Указываем срок более 30 дней с последнего изменения файла. То есть все файлы старше 30-ти дней.
-exec rm {} ; Выполняем удаление.

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

/usr/bin/find /data/mail/virtual/zabbix@mailsrv.ru/Maildir/*/ -type f -mtime +30 >> /root/dellist.txt

После этого смотрите файл /root/dellist.txt и проверяйте, что собираетесь удалить. После того, как проверили, не обязательно заново выполнять поиск по базе и лишний раз нагружать диски. Можно удалить все указанные в dellist.txt письма следующим скриптом.

#!/bin/bash

cat /root/dellist.txt | while read i ; do
rm -f "$i"
done

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

Массовая очистка корзин в почтовой базе postfix

Очистка и обслуживание почтовой базы postfix

Рассмотрим более сложный пример. Нам нужно автоматически очистить все корзины пользователей от писем, старше 30-ти дней. Я рекомендую всегда настраивать такую очистку. Дело в том, что если сервер сильно нагружен, то он не всегда корректно удаляет содержимое корзины. Например, пользователь отправил в корзину очень много писем (десятки тысяч), нажал «очистить корзину» и закрыл почтовый imap клиент. Есть вероятность, что реально письма не удалятся, а так и будут висеть в корзине. Imap сервер dovecot не удаляет мгновенно письма, а ставит их в очередь и потихоньку удаляет. Иногда этот процесс прерывается и удаление не происходит. Можно попытаться это сделать снова и рано или поздно они таки удалятся.

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

Сложность с очисткой корзин в том, что названий папок для удаленной почты может быть много. Каждый почтовый клиент создает какую-то свою папку, имя которой может не совпадать с уже существующими. Сейчас могут использоваться одновременно 3 вида почтовых клиентов: web, десктопная или мобильная программа. Каждая из них создает свой набор папок. Ко всему прочему, русские имена imap папок хранятся в кодировке UTF-7, что осложняет работу скриптов. Необходимо экранировать спецсимволы и пробелы.

Вот мой список возможных названий папок для удаленной почты.

Удаленные .&BCMENAQwBDsENQQ9BD0ESwQ1-
Удаленные элементы .&BCMENAQwBDsENQQ9BD0ESwQ1- &BE0EOwQ1BDwENQQ9BEIESw-
Корзина .&BBoEPgRABDcEOAQ9BDA-
Deleted Messages .Deleted Messages
Deleted Items .Deleted Items
Trash .Trash

Я обычно выполняю поиск по такому набору папок. Если у вас есть дополнение этого списка, прошу поделиться. А вот скрипт для автоматической очистки корзин с указанными именами папок.

#!/bin/bash

# текущая дата в формате Год-месяц-день
data=`date +"%Y-%m-%d"`
# формируем список почтовых ящиков для поиска
mailbox=`ls -l /data/mail/virtual | grep vmail | awk '{print $9}'`
for box1 in $mailbox
do
/usr/bin/find /data/mail/virtual/$box1/Maildir/'.&BCMENAQwBDsENQQ9BD0ESwQ1-'/*/ -type f -mtime +30 | while read a ; do
ls "$a" >> /root/mailclean/trashclean-log/$data.txt
done
done
for box2 in $mailbox
do
/usr/bin/find /data/mail/virtual/$box2/Maildir/'.Deleted Messages'/*/ -type f -mtime +30 | while read b ; do
ls "$b" >> /root/mailclean/trashclean-log/$data.txt
done
done
for box3 in $mailbox
do
/usr/bin/find /data/mail/virtual/$box3/Maildir/.Trash/*/ -type f -mtime +30 | while read c ; do
ls "$c" >> /root/mailclean/trashclean-log/$data.txt
done
done
for box4 in $mailbox
do
/usr/bin/find /data/mail/virtual/$box4/Maildir/'.&BCMENAQwBDsENQQ9BD0ESwQ1- &BE0EOwQ1BDwENQQ9BEIESw-'/*/ -type f -mtime +30 | while read d ; do
ls "$d" >> /root/mailclean/trashclean-log/$data.txt
done
done
for box5 in $mailbox
do
/usr/bin/find /data/mail/virtual/$box5/Maildir/'.&BBoEPgRABDcEOAQ9BDA-'/*/ -type f -mtime +30 | while read e ; do
ls "$e" >> /root/mailclean/trashclean-log/$data.txt
done
done
for box6 in $mailbox
do
/usr/bin/find /data/mail/virtual/$box6/Maildir/'.Deleted Items'/*/ -type f -mtime +30 | while read f ; do
ls "$f" >> /root/mailclean/trashclean-log/$data.txt
done
done
cat /root/mailclean/trashclean-log/$data.txt | while read i ; do
rm -f "$i"
done

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

Конструкция

mailbox=`ls -l /data/mail/virtual | grep vmail | awk '{print $9}'`

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

mailbox=/root/mailclean/mailboxlist.txt

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

Аналогичным образом можно очистить папки со спамом. Вот мой список для таких папок.

Нежелательная почта .&BB0ENQQ2BDUEOwQwBEIENQQ7BEwEPQQwBE8- &BD8EPgRHBEIEMA-
Spam .Spam
Junk E-mail .Junk E-mail
Junk .Junk

Более подробно вопрос автоматического удаления спама рассмотрим отдельно.

Удаление писем на основе содержимого письма

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

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

#!/bin/bash

# формируем список ящиков для очистки
mailbox=`ls -l /data/mail/virtual | grep vmail | awk '{print $9}'`
# дата в формате Год-месяц-день_час-минута-секунда
data_full=`date +"%Y-%m-%d_%H-%M-%S"`
# дата в формате Год-месяц-день
data=`date +"%Y-%m-%d"`
# директория для хранения копий удаленных писем
copydir=/backup/mailclean
# адрес лог файла с информацией о времени работы скрипта
logfile=/backup/mailclean/log.txt
echo "============`date +"%Y-%m-%d"`============" >> $logfile
echo "`date +"%H-%M-%S"` Start mail clean" >> $logfile
for box in $mailbox
do
# создаем директории с именами ящиков и текущей даты для копий удаленных писем
mkdir -p $copydir/$box/mail/$data
# формируем список всех писем в ящике
/usr/bin/find /data/mail/virtual/$box/Maildir -type f -name "*mailsrv*" -mtime +30 -daystart | while read a ; do
# ищем в содержимом письма адрес получателя zabbix@mailsrv.ru и записываем имена таких писем в индивидуальный файл для каждого ящика
grep -E -R -l -I "*for <zabbix@mailsrv.ru>;*" "$a" >> $copydir/$box/copy-$data_full.txt
done
# пишем название ящика в общий лог файл
echo "=========$box=========" >> $copydir/$data_full-all.txt
# записываем в общий лог файл все удаленные письма каждого ящика за конкретную дату очистки
cat $copydir/$box/copy-$data_full.txt >> $copydir/$data_full-all.txt
# формируем список писем ящика на удаление
cat $copydir/$box/copy-$data_full.txt | while read i ; do
# копируем письмо из реального ящика в папку архива (рекомендую использовать во время отладки)
cp -p "$i" $copydir/$box/mail/$data
# перемещаем письмо из реального ящика в архив (использовать после отладки)
# mv "$i" $copydir/$box/mail/$data
done
done
# записываем время завершения работы скрипта
echo "`date +"%H-%M-%S"` End mail clean" >> $logfile
echo "==================================" >> $logfile

Поясню основные моменты. В строке с поиском писем по всему ящику есть маска:

/usr/bin/find /data/mail/virtual/$box/Maildir -type f -name "*mailsrv*" -mtime +30 -daystart | while read a ; do

В данном случае mailsrv это часть имени сервера. В формате maildir в именах файлов писем всегда присутствует имя сервера. Эта маска позволяет найти только файлы почты, отбросив остальные служебные файлы, которые могут там быть. А они там точно будут, например, индексы dovecot, правила sieve и др. Не забудьте поменять эту маску на свою в соответствии с именем сервера.

Строка для поиска получателя письма *for <zabbix@mailsrv.ru>;* именно в таком виде взята из служебных заголовков. Даже если письма различными фильтрами будут перемещаться в другие ящики, первоначальный получатель писем все равно будет зафиксирован этой строкой.

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

Если вам нужно автоматом удалить в почтовом архиве все спам письма, которые ваша антиспам система помечает как спам, то можете взять служебный заголовок, который ставит эта система и использовать его. Например, Касперский антиспам вот так помечает все спамовые письма:

X-KLMS-AntiSpam-Status: spam

Соответственно я ищу все письма с таким служебным заголовком и что-то с ними делаю. Например, перемещаю в папку spam или удаляю. Но тут нужно быть внимательным и делать все аккуратно. Например, если какое-то письмо фильтр по ошибке пометил спамом, у пользователя не будет никакой возможности его сохранить, если вы будете автоматически удалять эти письма. Тут нужно думать в конкретной ситуации как поступить. На текущий момент я не использую подобные правила для удаления или перемещения почты, только для поиска, чтобы оценить объем спамовой почты. Если он слишком велик и его чистка позволит высвободить значительные объемы дискового пространства, то я начинаю что-то делать с ящиками пользователей. Чаще всего напрягать админов на местах, чтобы они стимулировали пользователей к самостоятельной ручной очистке спама. Если же спама не много, то я просто ничего не делаю.

Фильтрация писем на основе темы письма

Очистка и обслуживание почтовой базы postfix

Рассмотрим более сложный вариант предыдущего скрипта. Там мы фильтровали письма на основе содержимого служебных заголовков. Но если мы захотим отфильтровать почту по теме письма, то сходу у нас ничего не получится. С темой письма возникают сложности из-за того, что она закодирована в base64, если в ней используются русский язык. Вот простой пример. У нас есть письмо с темой «Как дела?». Используем base64 декодер и смотрим, как будет выглядеть тема сообщения в исходнике письма.

Как дела? - 0JrQsNC6INC00LXQu9CwPw==

А вот, что вы увидите в заголовке письма со всеми служебными добавлениями:

Subject: =?UTF-8?B?0JrQsNC6INC00LXQu9CwPw==?=

Вам нужно будет отбросить сначала кодировку =?UTF-8?, потом не знаю, что означающие символы B?, затем в конце еще вот это ?=. Так вы получаете искомую фразу. Теперь представьте, что кто-то ответит на это письмо. Тема сообщения станет Re: Как дела?. В base64 эта фраза будет выглядеть совершенно по-иному:

Re: Как дела? UmU6INCa0LDQuiDQtNC10LvQsD8=

И вот как в реальном заголовке:

Subject: =?UTF-8?B?UmU6INCa0LDQuiDQtNC10LvQsD8=?=

Сложность добавляет еще то, что разные почтовые клиенты используют разную кодировку в теме письма. Мне встречались как UTF-8, так и WIN-1251. То есть для того, чтобы нормально раскодировать и читать тему сообщения, вам нужно сделать обработку на декодирование, на отбрасывание служебных символов. Еще в процессе тестирования я заметил, что если вы используете не весь текст темы, а только ее часть, то закодированная строка поиска может немного не совпадать с той, что будет в теме письма. Изменения могут возникнуть из-за заглавных/строчных букв, пробелов, запятых и т.д. В общем, я не осилил эту тему, так как надо очень плотно погрузиться в предмет и написать много различных проверок и условий. У меня просто не хватило терпения все сделать красиво, чтобы скрипт работал надежно.

Поступил я в итоге по-другому, более просто и топорно, зато надежно. Допустим, вам нужно, чтобы какая-то переписка не хранилась на сервере дольше определенного времени. Это может быть конфиденциальная информация. Например, вы сканируете документы с отправкой на почту и вам нужно, чтобы сканы там не хранились бесконечно долго. Настраиваете на МФУ шаблон темы сообщения, добавляя в начало такую строку — !del. Затем переводите его в base64, добавляя еще фразы с Re: и Fwd: на случай, если эти письма могут куда-то пересылаться или писаться ответы. Конечно, сканеру вряд ли кто-то будет отвечать, но, возможно, для вашей темы сообщения это будет актуально.

!del IWRlbA==
Re: !del UmU6ICFkZWw=
Fwd: !del RndkOiAhZGVs

Дальше берете скрипт из предыдущего примера и меняете там строку поиска на новую:

grep -E -R -l -I "Subject:.*IWRlb.*|Subject:.*RndkOiAhZGVs.*|Subject:.*UmU6ICFkZWw.*" "$a" >> $copydir/$box/copy-$data_full.txt

Эта строка найдет во всех письмах, сформированных в список предыдущей командой, темы сообщения !del, Re: !del, Fwd: !del и скопирует пути и имена файлов в список. Потом вы можете на свое усмотрение работать с этим списком.

Еще несколько примеров работы с почтовой базой

Этот пример будет актуален, если вы используете почтовые ящики, куда копируется абсолютно вся переписка вашего сервера. Допустим, у вас есть ящик out@mailsrv.ru, куда сохраняется вся уходящая корреспонденция. Если на сервере идет активная переписка, то писем в ящике будет много и искать с помощью какого-то imap клиента будет неудобно, так как он может либо тормозить на большом списке писем, либо вообще отваливаться по таймауту и поиск или сортировка будут невозможны с его помощью. Тогда на помощь придут простые скрипты в консоли сервера. Найдем в этом ящике все письма, отправленные в период между первым и седьмым сентября 2017 года и скопируем их в отдельный ящик.

find /data/mail/virtual/out@mailsrv.ru/Maildir/*/ -newerBt '2017-09-01 00:00' -and -not -newerBt '2017-09-07 00:00' -and -type f | cpio -pdmv /data/mail/virtual/user@mailsrv.ru/Maildir/new

По сути тут просто использовались ключи к команде find. У нее их так много, что я для всех прикладных задач всегда храню готовые конструкции, чтобы заново не вспоминать все ключи и возможности.

Еще полезно бывает посмотреть размер почтовых ящиков. Некоторые пользователи имеют в разы больший объем ящика, чем все остальные. Есть любители хранить в почте презентации, пересылать разбитые на части архивы и т.д. Иногда приходится вручную проверять размеры ящиков, чтобы отдать команду на очистку заполненных сверх меры. Сделать это можно простой командой в директории с ящиками:

# du --max-depth=1 | sort -n -r

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

# du -h --max-depth=1

Еще удобнее отправить сразу вывод в текстовый файл с датой в имени файла, чтобы потом было удобно сравнить с тем, что получилось после чистки почтовой базы.

# du -h --max-depth=1 >> "dirsize_`date +"%Y-%m-%d_%H:%M"`.txt"

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

Заключение

Очистка и обслуживание почтовой базы postfix

Поделился тем, что использовал последнее время в работе с почтовыми серверами. По большому счету — ничего особенного. Почтовые сервера на postfix + dovecot чаще всего не требуют частого присмотра. Работают надежно, не требуют к себе повышенного внимания. Достаточно настроить мониторинг postfix и следить за свободным местом, периодически очищая почтовую базу, которая представляет из себя набор обычных файлов.

Так же рекомендую обязательно бэкапить почтовую базу тем или иным способом (можно простым rsync по директории с ящиками) и настроить мониторинг размера бэкапа, чтобы беглым взглядом можно было оценить динамику изменения размера почтовой базы. При желании, можно настроить триггер, реагирующий на те или иные значения базы.

Если у вас есть свои наработки, примеры или замечания к тому, что делаю я, прошу поделиться информацией в комментариях. Если есть вопросы по настройке почтового сервера, создавайте тему на форуме.


СМОТРИ ТАКЖЕ