РОМАН СУЗИ
Почтовый фильтр или Milter = Mail + Filter
В версии Sendmail 8.11.6 появилось нововведение, которое приоткрывает дверцу над таинственными процессами по обработке почтовых сообщений. И эта дверца – программный интерфейс фильтров по содержанию (content filter API, или, коротко, Milter) – очень существенный элемент для организации обработки почты в масштабах всего почтового сервера.
Если для индивидуальной настройки доставки почты в UNIX-системах обычно применяется procmail, то для обработки всей проходящей через сервер почты Milter подходит как нельзя кстати. Для большей эффективности рекомендуется применять версию Sendmail 8.12.
Главными применениями Milter можно считать:
- отклонение писем со спамом;
- организация проверки на вирусы;
- обезвреживание опасных вложений;
- фильтрация по содержимому;
- организация нестандартной обработки и сортировки почты.
Еще раз отметим, что Milter дает доступ не только ко входящим письмам, но и к исходящим. Это позволяет предотвращать злоупотребления почтовым сервером со стороны местных пользователей.
Одной из очень полезных особенностей Milter является его большая эффективность по сравнению с другими средствами обработки почты. Во-первых, Milter дает приложению-фильтру доступ к информации о сообщении на очень ранних стадиях его получения Sendmail: с помощью Milter приложение-фильтр получает атрибуты SMTP-команд вместе с Sendmail, что позволяет отклонять сообщения, еще не получив их в полном объеме. Во-вторых, Milter поддерживает многопоточность, что позволяет иметь всего один экземпляр приложения-фильтра, а не запускать приложение для каждого сообщения (как это делает procmail). В-третьих, подключение Milter-приложения к Sendmail можно сделать достаточно безопасным, определив требуемую реакцию Sendmail на тот случай, если приложение-фильтр не отвечает. Наконец, приложение-фильтр может находиться на другой машине, общаясь с почтовым сервером по TCP/IP. Кстати, фильтр может быть не один. Крючки (hooks) к Milter API имеются для многих известных антивирусных и антиспамовых программ.
Есть, конечно, у Milter и минусы. Во-первых, он работает только с Sendmail. Во-вторых, в нем не очень удобно (хотя и возможно) реализовать фильтрацию на основе индивидуальных настроек пользователей, как это имеет место с procmail.
Sendmail и приложение-фильтр общаются через сокет. Это может быть сокет в файловой системе, IP-сокет (или IPv6-сокет). Первый более эффективен, если обработка почты происходит на локальной машине, а второй позволяет вынести фильтрацию почты на другую машину (или машины). Например, один хост может быть целиком задействован для фильтрации спама, другой – проверять сообщения на вирусы, а третий – анализировать соответствие содержимого почтовых сообщений принятой политике (скажем, образовательное учреждение может фильтровать письма сомнительного содержания). На любом этапе Milter может пропустить (ACCEPT), отклонить (REJECT) или выбросить (DROP) обрабатываемое сообщение. Milter также может менять список получателей письма, изменять заголовочную часть и тело. При отклонении письма можно указать его детальную причину.
Sendmail написан на C, и поэтому Milter API ориентирован на C-приложения. Однако пример, который мы рассмотрим ниже, использует Python Milter – обертку Milter API для языка Python. Эта обертка – полноценный объектный интерфейс к обрабатываемому SMTP-соединению. Отметим, что использование интерпретатора скриптового языка существенно не влияет на производительность приложения, так как приложение работает в режиме демона. Зато мы получаем большой выигрыш в выражении логики приложения-фильтра и доступ к богатым стандартным библиотекам Python.
Чтобы использовать приложения-фильтры, нужно скомпилировать Sendmail с поддержкой Milter API. Как это сделать, описано в документации к Sendmail. В частности, необходимо указать в файле devtools/Site/site.config.m4 опцию:
APPENDDEF(“conf_sendmail_ENVDEF”, “-DMILTER”)
Для нашего примера понадобится Python 2.x и Python Milter. Первый можно взять на www.python.org, а второй – на http://www.bmsi.com/python/milter.html. Его необходимо скомпилировать, использовав библиотеки соответствующей версии Sendmail. Подробнее об этом написано в документации к Python Milter. Возможно, потребуется дополнительно установить заголовочные файлы и библиотеки libmilter и smutil – конкретнее об этом сказано на указанной выше www-странице. [Мне даже приходилось переименовывать libsmutil в libsm – прим. автора]. В результате компиляции Python Milter должен получиться файл milter.so, который вместе с mime.py и Milter.py нужно поместить в каталог с другими модулями Python.
Python Milter имеет в своем составе два законченных примера, однако подходящий именно вам фильтр лучше составить самому. Приведенный ниже пример (листинг 1) Milter поможет вам разобраться в Milter API. Конечно, это не законченный продукт, а скорее конструктор, который нужно доработать, добавив требуемую вам логику.
Листинг 1
#!/usr/bin/python2
"""
Пример Milter, на основе которого можно написать собственный фильтр для почты.
"""
import sys, os, Milter, tempfile, re, traceback
from time import strftime, time, localtime
def dbg_except():
"""Функция, которую можно ставить в части except: оператора try-except для отладки"""
sys.stderr.write(strftime(“%Y%m%d%H%M%S “) +
"".join(apply(traceback.format_exception, sys.exc_info())))
class ExampleMilter(Milter.Milter):
"""Класс, каждый объект которого отвечает за одно соединение."""
def log(self, *msg):
print "%s [%d] %s" % (strftime(“%Y%m%d%H%M%S”), self.id, " ".join(map(str, msg)))
def __init__(self):
self.tempname=self.mailfrom=self.connfrom=self.fp=None
self.id = Milter.uniqueID()
def connect(self, hostname, unused, hostaddr):
"""Вызывается при установке SMTP-соединения"""
self.log("connect: %s, %s" % (hostname, hostaddr))
# Здесь можно сразу проверить и отклонить:
# return Milter.REJECT
return Milter.CONTINUE
def hello(self,hostname):
"""Вызывается после команды HELO"""
self.log("hello from %s" % hostname)
return Milter.CONTINUE
def envfrom(self, f, *s):
"""Вызывается после команды MAIL FROM (их может быть несколько в рамках одного соединения). Отмечает начало сообщения."""
self.log("mail from", f, s)
self.tempname = None
self.mailfrom = f
self.headers = []
self.bodysize = 0
return Milter.CONTINUE
def envrcpt(self, to, *s):
"""Вызывается после команды RCPT TO (их может быть несколько для каждого сообщения)."""
self.log("rcpt to", to, s)
return Milter.CONTINUE
def header(self, name, val):
"""Вызывается для каждого поля заголовка сообщения"""
# Записать поле в список
self.headers.append("%s: %s" % (name, val))
lname = name.lower()
if lname in (“subject”, “from”, “to”):
self.log(“%s: %s” % (lname, val))
return Milter.CONTINUE
def eoh(self):
"""Вызывается по окончании обработки заголовка"""
# Начинаем записывать сообщение во временный файл. Сначала заголовки
self.tempname = tempfile.mktemp(".milter.tmp")
self.fp = open(self.tempname, "w+b")
self.fp.write("\n".join(self.headers) + "\n\n")
return Milter.CONTINUE
def body(self, chunk):
"""Вызывается для каждого фрагмента тела сообщения"""
# Тело сообщения по кусочкам пишется в тот же файл
if self.fp:
self.fp.write(chunk)
self.bodysize += len(chunk)
return Milter.CONTINUE
def eom(self):
"""Вызывается в конце каждого сообщения"""
# Закрываем временный файл (если он есть)
if self.fp:
self.fp.close()
else:
return Milter.TEMPFAIL
# Здесь можно делать всевозможные проверки и вызывать следующие методы:
# Удалить получателя:
# self.delrcpt("<user1@host.ru>")
# Добавить получателя:
# self.addrcpt("<user2@host.ru>")
# Добавить поле в заголовок:
# self.addheader("X-Processed-By", "Milter")
# ...и другое (см. документацию к Milter)
# Заменить тело сообщения (покусочно):
# for c in chunks:
# self.replacebody(c)
# Отклонить сообщение (с указанием причины):
# self.setreply(“550”, “5.1.1”, “SPAM”)
# return Milter.REJECT
# Отбросить сообщение:
# return Milter.DROP
# Принять сообщение:
self.log(“msg accepted: size=%s” % self.bodysize)
return Milter.ACCEPT
def abort(self):
"""Вызывается в случае ненормального завершения соединения"""
self.log("abort. Size=%d" % self.bodysize)
return Milter.CONTINUE
def close(self):
"""Вызывается в конце соединения, даже если оно было прервано"""
self.log("connection closed.")
sys.stdout.flush()
if self.fp:
self.fp.close()
self.fp = None
if self.tempname:
os.remove(self.tempname)
self.tempname = None
return Milter.CONTINUE
if __name__ == "__main__":
os.chdir("/home/milter")
tempfile.tempdir = "/var/tmp"
socketname = "inet:2525@milter.host.ru"
timeout = 240 # секунд
Milter.factory = ExampleMilter
Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS + Milter.DELRCPT + Milter.ADDRCPT)
print """Example Milter start"""
sys.stdout.flush()
Milter.runmilter("mainfilter", socketname, timeout)
print """Example Milter shutdown"""
В этой небольшой программе описывается класс ExampleMilter, основанный на классе Milter из модуля Milter. Для каждого SMTP-соединения создается новый объект класса ExampleMilter. Отдельные методы этого объекта отвечают за обработку определенных событий (см. комментарии в тексте программы). Вся информация, касающаяся определенного соединения, должна храниться в атрибутах объекта. В нашем примере так накапливается и хранится bodysize (размер тела сообщения) и некоторые вспомогательные объекты. (В Python сам объект передается в метод в качестве первого аргумента и традиционно называется self, поэтому для работы с атрибутами внутри метода нужно использовать self.имя_атрибута. Кстати, в Python новые атрибуты могут появляться в объекте в любой удобный момент.) Здесь следует заметить, что методы вызываются в определенной последовательности, и нет гарантии, что для данного соединения будет вызван тот или иной метод. Например, следом за hello() может сразу последовать abort(). Это обстоятельство необходимо учитывать при написании своего фильтра, как и то, что в рамках одного соединения может быть обработано несколько сообщений. В силу чего требуется правильно инициализировать имена: те, что относятся ко всему соединению, нужно инициализировать в connect(), а относящиеся к конкретному сообщению – в envfrom(). Соответственно, конечной точкой использования таких имен должны быть методы close() и eom().
На любом этапе работы фильтра можно решить судьбу сообщения, возвратив Milter.REJECT (отклонить), Milter.ACCEPT (принять), Milter.DROP (выбросить) или продолжить обработку – Milter.CONTINUE. В методе eom() можно менять некоторые свойства обрабатываемого сообщения (см. комментарии в листинге 1). Напомним, что состав получателей не обязательно соответствует содержимому полей To, Cc, Bcc, так как эти данные передаются отдельными командами протокола SMTP.
Для включения фильтра необходимо добавить примерно следующие две строки к файлу sendmail.mc:
MAIL_FILTER(`mainfilter", `S=inet:2525@milter.host.ru, T=C:10m;S:30s;R:30s;E:10m")
define(`confINPUT_MAIL_FILTERS", `mainfilter")
Здесь milter.host.ru и 2525 – хост, на котором запущен Milter, и порт (номер выбран произвольно).
Если вы привыкли напрямую править sendmail.cf (что очень не рекомендуется), то в него нужно добавить следующее:
O InputMailFilters=mainfilter
#O Milter.LogLevel O Milter.macros.connect=j, _, {daemon_name},
{if_name}, {if_addr}
O Milter.macros.helo={tls_version}, {cipher}, {cipher_bits},
{cert_subject}, {cert_issuer}
O Milter.macros.envfrom=i, {auth_type}, {auth_authen},
{auth_ssf}, {auth_author}, {mail_mailer}, {mail_host},
{mail_addr}
O Milter.macros.envrcpt={rcpt_mailer}, {rcpt_host}, {rcpt_addr}
Xfilteronegoru, S=inet:2525@milter.host.ru,
T=C:10m;S:30s;R:30s;E:10m
Описание каждого фильтра в файле конфигурации Sendmail может сопровождаться тремя опциями: F, S и T. Если опция F не задана, то проблемы с фильтром безболезненны для доставляемого Sendmail сообщения. Если F=T, неработоспособность фильтра приводит к временной неудаче (temporary fail) доставки сообщения. Если F=R и фильтр недоступен, сообщение отвергается (reject). В опции S указывается адрес фильтра, имеющий один из приведенных ниже форматов:
S=local:путь
S=inet:порт@хост
S=inet6:порт@хост
Здесь путь – путь к UNIX-сокету в локальной файловой системе, остальные – IP-сокеты на некотором хосте.
Опции Т задают таймауты. C – таймаут соединения с фильтром; S – таймаут при передаче информации от Sendmail фильтру; R – таймаут при ожидании ответа фильтра; E – таймаут ожидания окончательного подтверждения от момента передачи конца сообщения фильтру.
В случае использования двух фильтров фрагмент исходного файла конфигурации Sendmail будет выглядеть так:
MAIL_FILTER(`mainfilter", `S=inet:2525@milter.host.ru, T=C:10m;S:30s;R:30s;E:10m")
MAIL_FILTER(`filter2", `S=inet:2626@milter.host.ru, T=C:10m;S:30s;R:30s;E:10m")
define(`confINPUT_MAIL_FILTERS", `mainfilter,filter2")
Информация на фильтры посылается в порядке их описания. Следующий алгоритм взаимодействия Sendmail и Milter приведен в документации к Sendmail:
Для каждого соединения:
Для каждого фильтра:
Вызвать connect()
Вызвать hello()
Для каждого сообщения (последовательно):
Для каждого фильтра:
Вызвать envfrom()
Для каждого получателя:
Для каждого фильтра:
Вызвать envrcpt()
Для каждого фильтра:
Для каждого поля заголовка:
Вызвать header()
Вызвать eoh()
Для каждого фрагмента тела:
Вызвать body()
Вызвать eom()
Для каждого фильтра:
Вызвать close()
Примечание: при обрыве соединения на любой стадии и по инициативе любого агента вызывается abort() и close().
В листинге 2 приведен вариант метода eom(), с помощью которого можно вызывать из Milter произвольные программы для проверки сообщения (например, на спам и вирусы).
Листинг 2
def eom(self):
"""Обработка сообщения: проверка на вирусы с помощью антивируса ClamAV"""
# Закрываем временный файл (если он есть)
if self.fp:
self.fp.close()
else:
return Milter.TEMPFAIL
try:
clam=os.popen("clamscanm <%s"%self.tempname,"r").read()
if clam.find("FOUND") != -1:
self.log(“virus rejected: %s” % clam)
self.setreply(“550”,’5.1.1',’VIRUS FOUND %s’ % clam)
return Milter.REJECT
except:
dbg_except()
self.log(“msg accepted: size=%s” % self.bodysize)
return Milter.ACCEPT
В этом методе вызывается программа clamscanm, принимающая файл на стандартный ввод и выводящая результат на стандартный вывод. Результат читается методом read() целиком и анализируется на присутствие подстроки «FOUND». Если есть такая строка, Milter устанавливает причину отказа методом setreply, пишет в лог и отклоняет сообщение. Так как все это происходит при установленном SMTP-соединении, отправитель получит «отлуп» сразу. Аналогично можно проверять почту на спам.
Следует отметить, что в процессе эксплуатации неаккуратно запрограммированный Milter может оставлять за собой временные файлы. Следующая небольшая программа на Python стирает оставленные Milter устаревшие файлы из каталога /var/tmp:
#!/usr/bin/python
import os, glob, time, stat
recent = time.time() - 60*20 # 20 min
for fl in glob.glob("/var/tmp/*.tmp"):
try:
if os.stat(fl)[stat.ST_MTIME] < recent:
os.unlink(fl)
except:
pass
Итак, Milter дает нам полный контроль над передачей сообщений на самом раннем этапе – этапе входящего SMTP-соединения. Фильтры можно писать на C/C++, Perl и других языках, тем не менее Python Milter отлично справляется с задачей.