Доброго времени суток!
Я работаю системным администратором в небольшой телекоммуникационной компании. В 2006 году начал разрабатывать систему фильтрации почтового SPAM'а на базе почтового сервера Postfix и агента Maildrop. На текущий момент создана система фильтрации спама с довольно эффективными методами анализа сообщений. Эту методику я и хотел бы представить ко всеобщему обозрению и критике.
Сразу скажу, что статья не является руководством по настройке почтового сервера и сопутствующих программ.
Ядром системы фильтрации является Maildrop-скрипт, который состоит из множество правил, анализирующих SMTP-соединение, заголовки и тело сообщения. Каждое правило либо повышает, либо уменьшает SPAM-рейтинг, который влияет на итоговый вердикт: спам, возможно спам, не спам. Как именно работают правила можно увидеть ниже в исходном тексте скрипта.
Достоинства системы:
- лёгкость;
- приём почты даже от самых кривых почтовых серверов;
- масштабируемость;
- использование SPF, DKIM
- использование распределённых систем контрольных сумм Razor, Pyzor, DCC;
- использование локальной базы Bayes;
- отсутствие серых списков (greylisting);
- проверка на вирусы;
- лёгкая отладка (лог в стандартном mail.log + header в каждом письме);
- использование ловушек спама;
- вердикт выносится, исходя из множества правил;
- самообучение.
Правила фильтрации:
- client_not_ptr(9) — проверка существования PTR (обратной зоны) для IP адреса отправителя;
- helo_not_fqdn(20) — проверка обратной зоны на соответствие FQDN;
- helo_bareip(9) — проверка на приветствие голым IP вместо домена;
- helo_device(30) — проверка на приветствие от домашних роутеров типа Dlink;
- helo_local(20), helo_arpa(20), helo_localhost(20) — проверка на приветствие от кривых обратных зон домашних сетей или localhost;
- helo_a(9) — проверка на существования домена из HELO;
- subject_empty(5) — проверка темы сообщения на пустоту (а так же пустые RE:, FWD: с цифрами и без);
- subject_hiero(5) — проверка темы сообщения на иероглифы;
- reply(-40) — проверка заголовка In-Reply-To (только, если сообщение является ответом и Message-Id есть в базе);
- from(4) — сравнение отправителя в SMTP сессии (MAIL FROM) с заголовками письма (From);
- from_eqto(9) — — если e-mail отправителя и получателя одинаковые;
- from_nonvowel(4) — проверка на содержание последовательностей только из согласных букв в доменах и e-mail отправителя;
- tocc_rcpt(9) — сравнение получателя в SMTP сессии (RCPT TO) и заголовках письма (To);
- tocc_count(+) — увеличение рейтинга пропорционально количеству получателей в полях To и Cc (если таких больше 4);
- uunknown_stat(+) — проверка количества попыток с данного IP отправить письмо для несуществующих адресатов на нашем сервере;
- protoerror_stat — проверка ошибок протокола для данного IP («550 5.5.1 Protocol error»);
- dkim_pass(-7), dkim_fail(20) — проверка цифровой подписи DKIM;
- dns_bl(+) — проверка в 25 DNSBL списках;
- dns_wl(-) — проверках в 2 DNSWL списках;
- spf_pass(-5), spf_neutral(5), spf_softfail(9), spf_fail(30) — проверка IP отправителя на предмет разрешения отправлять почту данного домена;
- l_unsubscribe(-3) — наличие заголовка List-Unsubscribe;
- size_wl(-) — проверка размера письма. Чем больше, тем меньше итоговый рейтинг;
- bayes(-20… 200) — проверка через bogofilter (Bayes);
- razor(30), pyzor(30) — проверка контрольных сумм через сеть Razor и Pyzor;
- dcc(-9… 40) — проверка контрольных сумм через сеть DCC;
- date_rfc2822(5), date_tz(5), date_tzce(5), date_tab(5) — проверка заголовка Date на RFC2822, правильность указания временной зоны и tab вместо пробела;
- geoip_(+) — проверка на нежелательные страны типа Пакистана, Бразилии, Вьетнама и т.п.;
- local(-10) — при отправке писем от локальных пользователей (включая sendmail).
В скобках указано, сколько баллов добавляет или отнимает данное правило от итогового рейтинга.
Эмпирическим путём были установлены пределы рейтинга:
менее 0 баллов — точно не спам. Срабатывает самообучение и уведомление dcc, pyzor, razor, bogofilter;
менее 10 баллов — не спам;
от 10 баллов — возможно спам;
от 30 баллов — возможно спам. Срабатывает самообучение и уведомление dcc, pyzor, razor, bogofilter;
от 45 баллов — точно спам. Письмо удаляется. Срабатывает самообучение и уведомление dcc, pyzor, razor, bogofilter.
Если письмо «возможно спам», то тема помечается маркером "*****SPAM*****".
Антивирусная проверка осуществляется через Clamav. Если письмо инфицировано, то тема помечается "*****VIRUS-VIRUS-VIRUS*****".
Для более точного определения спама создано ~20 ловушек. Ловушки представляют собой обычные e-mail адреса вида vasy_pupkin3@domain, которые размещены на нескольких популярных сайтах в невидимых div и видны только ботам, собирающим адреса для своих рассылок.
После проверки, к каждому входящему сообщению добавляется заголовок «X-Spam-Status».
Примеры таких заголовков:
«X-Spam-Status: YES, score=78, tests=post, client_not_ptr(9), helo_not_fqdn(20), spf_softfail(9), bayes(40 1.000000), spam»
«X-Spam-Status: YES, score=43, tests=post, client_not_ptr(9), tocc_rcpt(9), uunknown_stat(1), bayes(3 0.500582), pyzor(30), dcc(-9), spam»
«X-Spam-Status: NO, score=-20, tests=post, bayes(-20 0.000000), ham»
Некоторые правила, содержащие сложные регулярные выражения были позаимствованы из SpamAssassin.
Статистика в цифрах
Ежедневно через почтовый сервер проходит ~50 000 писем.
Спам составляет ~98% от общего числа писем.
Выборочный анализ «засвеченных» почтовых ящиков показывает, что некоторый спам всё же проскакивает с пометкой «возможно спам».
Отслеживать ошибочные срабатывания представляется возможным только по отзывам клиентов, но зачастую проблема кроется не в фильтре, а в отправляющей стороне.
Почему было решено написать свою систему?
Ранее было испробовано несколько решений для защиты от спама (SpamAsassin, Спамооборна от Яндекса, Kaspersky Anti-Spam), но ни одно из них не устроило и я ещё я гентушник.
Дальнейшие планы:
- сделать отдельный milter на Perl, включающий в себя весь описанных функционал, для более тесного взаимодействия с почтовым сервером;
- перейти на Bayes-фильтр с поддержкой MySQL;
- увеличение количества ловушек;
- багфикс.
Используемое ПО:
- Linux/BSD;
- Postfix;
- Clamav;
- opendkim (milter);
- Perl;
- Maildrop;
- DCC;
- Razor;
- Pyzor;
- Clamav;
- модифицированные скрипты взаимодействия с DCC и аналаза SPF.
smtpd_recipient_restrictions =
reject_non_fqdn_sender
reject_unknown_sender_domain
reject_unknown_recipient_domain
reject_non_fqdn_recipient
reject_unlisted_sender
reject_unlisted_recipient
permit_sasl_authenticated
reject_unauth_destination
check_policy_service unix:private/policyd-spf
reject_unauth_pipelining
reject_invalid_helo_hostname
smtpd_milters = unix:/var/run/clamav/clamav-milter.sock, unix:/var/run/opendkim/opendkim.sock
non_smtpd_milters = unix:/var/run/opendkim/opendkim.sock
destination mail_protoerror { file("/var/log/mail.protoerror" group(vmail) perm(0660)); };
destination mail_uunknown { file("/var/log/mail.uunknown" group(vmail) perm(0640)); };
destination mail_messageid { file("/var/log/mail.messageid" group(vmail) perm(0640)); };
filter f_mail_protoerror { program("postfix/postscreen") and message("550 5.5.1 Protocol error"); };
filter f_mail_uunknown { program("postfix/smtpd") and message("Recipient address rejected: User unknown"); };
filter f_mail_messageid { program("postfix/cleanup") and message("message-id="); };
log { source(src); filter(f_mail_protoerror); destination(mail_protoerror); };
log { source(src); filter(f_mail_uunknown); destination(mail_uunknown); };
log { source(src); filter(f_mail_messageid); destination(mail_messageid); };
maildrop unix - n n - 40 pipe
flags=DORhu user=vmail:vmail argv=/usr/bin/maildrop -f ${sender} -d ${recipient} ${client_address} ${client_hostname} ${client_helo} ${sasl_username}
policyd-spf unix - n n - 0 spawn
user=spf argv=/usr/local/spf/postfix-policyd-spf-perl.pl
CLIENT_ADDRESS=$1
CLIENT_HOSTNAME=$2
if($CLIENT_HOSTNAME eq "unknown")
{
CLIENT_HOSTNAME=$CLIENT_ADDRESS
}
CLIENT_HELO=$3
if(/^X-Original-To: (.+)/:h)
{
RCPT_TO_ORIG=$MATCH1
}
RCPT_TO=escape($RCPT_TO_ORIG)
MAIL_FROM=escape($FROM)
SASL_USERNAME=$4
QUEUE_ID=""
#QUEUE_ID
if(/^Received:.*by %имя_нашего_почтового_сервера% ([A-Za-z0-9]+) with [A-Za-z0-9]+ id ([A-Za-z0-9]+)/:h)
{
QUEUE_ID=$MATCH1
}
#
# Check for user defined filter file
#
CMD_RAZOR_CHECK='/usr/bin/razor-check'
CMD_RAZOR_REPORT='/usr/bin/razor-report'
CMD_PYZOR_CHECK='/usr/bin/pyzor check'
CMD_PYZOR_REPORT='/usr/bin/pyzor report'
CMD_DCC_CHECK="/usr/local/dcc/dccif.pl -o header -c $CLIENT_ADDRESS -j $CLIENT_HOSTNAME -l $CLIENT_HELO -f $FROM -r $RCPT_TO_ORIG"
CMD_DCC_REPORT="/usr/local/dcc/dccif.pl -o spam -c $CLIENT_ADDRESS -j $CLIENT_HOSTNAME -l $CLIENT_HELO -f $FROM -r $RCPT_TO_ORIG"
CMD_BOGOFILTER_SPAM='/usr/bin/bogofilter --unicode=yes -C -e -p -O /dev/null -s'
CMD_BOGOFILTER_HAM='/usr/bin/bogofilter --unicode=yes -C -e -p -O /dev/null -n'
if($LOGNAME eq "ловушка@домен")
{
exception {
`$CMD_RAZOR_REPORT`
`$CMD_PYZOR_REPORT`
`$CMD_DCC_REPORT`
`$CMD_BOGOFILTER_SPAM`
to "/dev/null"
}
}
elsif($LOGNAME eq "ловушка2@домен2")
{
exception {
`$CMD_RAZOR_REPORT`
`$CMD_PYZOR_REPORT`
`$CMD_DCC_REPORT`
`$CMD_BOGOFILTER_SPAM`
to "/dev/null"
}
}
#
# Mailfilter
#
RET_SPAM=0
RET_SPAM_STATUS="NO"
if($SASL_USERNAME eq "" && $CLIENT_ADDRESS ne "")
{
RET_SPAM_TESTS="post"
#PTR
if($CLIENT_HOSTNAME eq $CLIENT_ADDRESS)
{
RET_SPAM=$RET_SPAM + 9
RET_SPAM_TESTS="$RET_SPAM_TESTS, client_not_ptr(9)"
}
#HELO fqdn
if($CLIENT_HELO =~ /^[a-zA-Z0-9_-]+$/)
{
RET_SPAM=$RET_SPAM + 20
RET_SPAM_TESTS="$RET_SPAM_TESTS, helo_not_fqdn(20)"
}
else
{
#HELO checks
if($CLIENT_HELO =~ /^[*d+.d+.d+.d+]*$/)
{
RET_SPAM=$RET_SPAM + 9
RET_SPAM_TESTS="$RET_SPAM_TESTS, helo_bareip(9)"
}
elsif($CLIENT_HELO =~ /^(dsl)?(device|speedtouch).lan$/)
{
#HELO device
RET_SPAM=$RET_SPAM + 30
RET_SPAM_TESTS="$RET_SPAM_TESTS, helo_device(30)"
}
elsif($CLIENT_HELO =~ /.(lan|local|home|localdomain)$/)
{
RET_SPAM=$RET_SPAM + 20
RET_SPAM_TESTS="$RET_SPAM_TESTS, helo_local(20)"
}
elsif($CLIENT_HELO =~ /.in-addr.arpa$/)
{
RET_SPAM=$RET_SPAM + 20
RET_SPAM_TESTS="$RET_SPAM_TESTS, helo_arpa(20)"
}
elsif($CLIENT_HELO =~ /localhost$/)
{
RET_SPAM=$RET_SPAM + 20
RET_SPAM_TESTS="$RET_SPAM_TESTS, helo_localhost(20)"
}
#HELO to CLIENT_ADDRESS lookup
`nslookup -type=a "$CLIENT_HELO" | grep -q "$CLIENT_ADDRESS$"`
if($RETURNCODE == 1)
{
RET_SPAM=$RET_SPAM + 9
RET_SPAM_TESTS="$RET_SPAM_TESTS, helo_a(9)"
}
}
#SUBJECT exists
if(! /^Subject:/:h)
{
xfilter 'reformail -A"Subject:"'
}
#SUBJECT empty
if(/^Subject:([rR][eE]|F[wW][dD]?|:|s|[[]0-9])*$/:h)
{
RET_SPAM=$RET_SPAM + 5
RET_SPAM_TESTS="$RET_SPAM_TESTS, subject_empty(5)"
}
elsif(/^Subject: e$B.*(?:L$>5Bz|EE;R%a!<%k)(?:8x|9-)9p/:h || /^Subject: [({[<][. ]*(?-i:xbcxba[. ]*xc0xce[. ]*)?(?-i:xb1xa4(?:[. ]*|[x00-x7f]{0,3})xb0xed|xc1xa4[. ]*xbaxb8|xc8xab[. ]*xbaxb8)[. ]*[)}]>]/:h)
{
RET_SPAM=$RET_SPAM + 5
RET_SPAM_TESTS="$RET_SPAM_TESTS, subject_hiero(5)"
}
#In-Reply-To
if(/^In-Reply-To: (.*)/:h)
{
IN_REPLY_TO_ID=$MATCH1
if($IN_REPLY_TO_ID ne "")
{
`grep -iqm 1 "$IN_REPLY_TO_ID" /var/log/mail.messageid`
if($RETURNCODE != 0)
{
OLD_FILE=`ls -1rv /var/log/old/mail.messageid* | head -1`
if($RETURNCODE == 0)
{
`grep -iqm 1 "$IN_REPLY_TO_ID" $OLD_FILE`
}
}
if($RETURNCODE == 0)
{
RET_SPAM=$RET_SPAM - 40
RET_SPAM_TESTS="$RET_SPAM_TESTS, reply(-40)"
}
}
}
#FROM
#if(! /^From:.*$MAIL_FROM/:h)
#{
#RET_SPAM=$RET_SPAM + 4
#RET_SPAM_TESTS="$RET_SPAM_TESTS, from(4)"
#}
#FROM=TO
if(/^From:.*$RCPT_TO/:h)
{
RET_SPAM=$RET_SPAM + 9
RET_SPAM_TESTS="$RET_SPAM_TESTS, from_eqto(9)"
}
#FROM non-vowel letters
if(/^From:.*[bcdfgjklmnpqrstvwxzBCDFGJKLMNPQRSTVWXZ]{7}S*@/:h || /^From:.*@S*[bcdfgjklmnpqrstvwxzBCDFGJKLMNPQRSTVWXZ]{7}/:h)
{
RET_SPAM=$RET_SPAM + 4
RET_SPAM_TESTS="$RET_SPAM_TESTS, from_nonvowel(4)"
}
#TO,CC rcpt
if(! /^(To|Cc):.*$RCPT_TO/:h)
{
RET_SPAM=$RET_SPAM + 9
RET_SPAM_TESTS="$RET_SPAM_TESTS, tocc_rcpt(9)"
}
#TO,CC undisclosed
# if(/^(To|Cc):.*undisclosed.*recipient/:h)
# {
# RET_SPAM=$RET_SPAM + 4
# RET_SPAM_TESTS="$RET_SPAM_TESTS, tocc_undisclosed(9)"
# }
TOCCADDCOUNT=-1
foreach /^(To|Cc):.*/
{
foreach (getaddr $MATCH) =~ /.+/
{
TOCCADDCOUNT=$TOCCADDCOUNT+1
}
}
if($TOCCADDCOUNT > 4)
{
RET_SPAM=$RET_SPAM + $TOCCADDCOUNT
RET_SPAM_TESTS="$RET_SPAM_TESTS, tocc_count($TOCCADDCOUNT)"
}
#User unknown stat for IP
UNKNOWN_SCORE=`grep -oE "RCPT from.*[$CLIENT_ADDRESS].*to=<.*>" /var/log/mail.uunknown | sort | uniq | wc -l`
if($UNKNOWN_SCORE > 0)
{
UNKNOWN_SCORE=$UNKNOWN_SCORE
RET_SPAM=$RET_SPAM + $UNKNOWN_SCORE
RET_SPAM_TESTS="$RET_SPAM_TESTS, uunknown_stat($UNKNOWN_SCORE)"
}
#Protocol error stat for IP
PROTOERROR_SCORE=`grep -oE "RCPT from.*[$CLIENT_ADDRESS].*to=<.*>" /var/log/mail.protoerror | sort | uniq | wc -l`
if($PROTOERROR_SCORE > 0)
{
PROTOERROR_SCORE=$PROTOERROR_SCORE * 4
RET_SPAM=$RET_SPAM + $PROTOERROR_SCORE
RET_SPAM_TESTS="$RET_SPAM_TESTS, protoerror_stat($PROTOERROR_SCORE)"
}
#DKIM
DNSBL_CHECK=1
if(/^Authentication-Results:.*dkim=([a-z]+)/:h)
{
if($MATCH1 eq "pass")
{
RET_SPAM=$RET_SPAM - 7
RET_SPAM_TESTS="$RET_SPAM_TESTS, dkim_pass(-7)"
DNSBL_CHECK=0
}
elsif($MATCH1 eq "fail")
{
RET_SPAM=$RET_SPAM + 20
RET_SPAM_TESTS="$RET_SPAM_TESTS, dkim_fail(20)"
}
}
#DNSBL
DNSBL_SCORE=0
if($DNSBL_CHECK == 1)
{
DNSBL_SCORE=`/usr/local/dnsbl/rbl-check.pl BL $CLIENT_ADDRESS`
}
if($DNSBL_SCORE > 0)
{
DNSBL_SCORE=$DNSBL_SCORE * 9
RET_SPAM=$RET_SPAM + $DNSBL_SCORE
RET_SPAM_TESTS="$RET_SPAM_TESTS, dns_bl($DNSBL_SCORE)"
}
DNSWL_SCORE=`/usr/local/dnsbl/rbl-check.pl WL $CLIENT_ADDRESS`
if($DNSWL_SCORE > 0)
{
DNSWL_SCORE = $DNSWL_SCORE * 5
RET_SPAM=$RET_SPAM - $DNSWL_SCORE
RET_SPAM_TESTS="$RET_SPAM_TESTS, dns_wl(-$DNSWL_SCORE)"
}
#SPF pass
if(/^Received-SPF: pass/:h)
{
RET_SPAM=$RET_SPAM - 5
RET_SPAM_TESTS="$RET_SPAM_TESTS, spf_pass(-5)"
}
#SPF fail
if(/^Received-SPF: fail/:h)
{
RET_SPAM=$RET_SPAM + 30
RET_SPAM_TESTS="$RET_SPAM_TESTS, spf_fail(30)"
}
#SPF softfail
if(/^Received-SPF: softfail/:h)
{
RET_SPAM=$RET_SPAM + 9
RET_SPAM_TESTS="$RET_SPAM_TESTS, spf_softfail(9)"
}
#SPF neutral
if(/^Received-SPF: neutral/:h)
{
RET_SPAM=$RET_SPAM + 5
RET_SPAM_TESTS="$RET_SPAM_TESTS, spf_neutral(5)"
}
#List-Unsubscribe
if(/^List-Unsubscribe:/:h)
{
RET_SPAM=$RET_SPAM - 3
RET_SPAM_TESTS="$RET_SPAM_TESTS, l_unsubscribe(-3)"
}
#SIZE
WEIGHT_SIZE=$SIZE / 100000
WEIGHT_SIZE=`printf "%.0f" "$WEIGHT_SIZE"`
WEIGHT_SIZE=$WEIGHT_SIZE + 1
if($WEIGHT_SIZE > 40)
{
WEIGHT_SIZE=40
}
elsif($WEIGHT_SIZE >= 3)
{
RET_SPAM=$RET_SPAM - $WEIGHT_SIZE
RET_SPAM_TESTS="$RET_SPAM_TESTS, size_wl(-$WEIGHT_SIZE)"
}
#BOGOFILTER
BOGO_STATUS="Unsure"
if($WEIGHT_SIZE != 40)
{
BOGOFILTER=`/usr/bin/bogofilter --unicode=yes -C -e -p --header-format="X-Bogosity-1ffg55676: %c, %p" | grep -m 1 "X-Bogosity-1ffg55676"`
if($BOGOFILTER =~ /^X-Bogosity-1ffg55676: (Spam|Ham|Unsure), (.+)$/)
{
BOGO_STATUS=$MATCH1
BOGO_BALL=$MATCH2 * 40
BOGO_BALL=`printf "%.0f" "$BOGO_BALL"`
if($BOGO_STATUS eq "Spam")
{
RET_SPAM=$RET_SPAM + $BOGO_BALL
RET_SPAM_TESTS="$RET_SPAM_TESTS, bayes($BOGO_BALL $MATCH2)"
}
elsif($BOGO_STATUS eq "Ham")
{
#full white
if($MATCH2 eq "0.000000")
{
RET_SPAM=$RET_SPAM - 20
RET_SPAM_TESTS="$RET_SPAM_TESTS, bayes(-20 $MATCH2)"
BOGO_STATUS="White"
}
}
elsif($BOGO_STATUS eq "Unsure")
{
BOGO_BALL=`echo "3 + ($MATCH2 - 0.5) * 71.4" | bc`
BOGO_BALL=`printf "%.0f" "$BOGO_BALL"`
RET_SPAM=$RET_SPAM + $BOGO_BALL
RET_SPAM_TESTS="$RET_SPAM_TESTS, bayes($BOGO_BALL $MATCH2)"
}
}
}
#RAZOR
if($BOGO_STATUS ne "White")
{
`$CMD_RAZOR_CHECK`
if($RETURNCODE == 0)
{
RET_SPAM=$RET_SPAM + 30
RET_SPAM_TESTS="$RET_SPAM_TESTS, razor(30)"
}
}
#PYZOR
if($BOGO_STATUS ne "White")
{
`$CMD_PYZOR_CHECK`
if($RETURNCODE == 0)
{
RET_SPAM=$RET_SPAM + 30
RET_SPAM_TESTS="$RET_SPAM_TESTS, pyzor(30)"
}
}
#DCC
if($BOGO_STATUS eq "Unsure")
{
`$CMD_DCC_CHECK`
#many
if($RETURNCODE != 0 && $RETURNCODE != 1 && $RETURNCODE != 127)
{
RET_SPAM=$RET_SPAM + $RETURNCODE
RET_SPAM_TESTS="$RET_SPAM_TESTS, dcc($RETURNCODE)"
}
elsif($RETURNCODE == 1)
{
RET_SPAM=$RET_SPAM - 9
RET_SPAM_TESTS="$RET_SPAM_TESTS, dcc(-9)"
}
}
#Date RFC2822
if(! /^Date:s*(?:(?i:Mon|Tue|Wed|Thu|Fri|Sat|Sun),s)?s*(?:[12]d|3[01]|0?[1-9])s+(?i:Jan|Feb|Ma[ry]|Apr|Ju[nl]|Aug|Sep|Oct|Nov|Dec)s+(?:19[7-9]d|2d{3})s+(?:[01]?d|2[0-3]):[0-5]d(?::(?:[0-5]d|60))?(?:s+[AP]M)?(?:s+(?:[+-][0-9]{4}|UT|[A-Z]{2,3}T|0000 GMT|"GMT"))?(?:s*(.*))?s*$/:h)
{
RET_SPAM=$RET_SPAM + 5
RET_SPAM_TESTS="$RET_SPAM_TESTS, date_rfc2822(5)"
}
#Date timezone does not exist
if(/^Date:.*[-+](?!(?:0d|1[0-4])(?:[03]0|[14]5))d{4}$/:h)
{
RET_SPAM=$RET_SPAM + 5
RET_SPAM_TESTS="$RET_SPAM_TESTS, date_tz(5)"
}
#Date timezone CST, EST
if(/[+-]dd[30]0(?<!-0600|-0500|+0800|+0930|+1030)s+(?:bCSTb|(CST))/:h || /[+-]dd[30]0(?<!-0500|-0300|+1000|+1100)s+(?:bESTb|(EST))/:h) #-->
{
RET_SPAM=$RET_SPAM + 5
RET_SPAM_TESTS="$RET_SPAM_TESTS, date_tzce(5)"
}
#Date tab
if(/^Date:s*t/:h)
{
RET_SPAM=$RET_SPAM + 5
RET_SPAM_TESTS="$RET_SPAM_TESTS, date_tab(5)"
}
#GeoIP
GEOIP_RET=`geoiplookup "$CLIENT_ADDRESS"`
GEOIP_COUNTRY=`expr substr "$GEOIP_RET" 24 2`
if($GEOIP_COUNTRY eq "IN" || $GEOIP_COUNTRY eq "ID" || $GEOIP_COUNTRY eq "BR" || $GEOIP_COUNTRY eq "KR" || $GEOIP_COUNTRY eq "CN")
{
#India, Indonesia, Brazil, Korea (S), China
RET_SPAM=$RET_SPAM + 20
RET_SPAM_TESTS="$RET_SPAM_TESTS, geoip_$GEOIP_COUNTRY(20)"
}
elsif($GEOIP_COUNTRY eq "IT" || $GEOIP_COUNTRY eq "PE" || $GEOIP_COUNTRY eq "CO" || $GEOIP_COUNTRY eq "VN" || $GEOIP_COUNTRY eq "TW" || $GEOIP_COUNTRY eq "PL" || $GEOIP_COUNTRY eq "GB" || $GEOIP_COUNTRY eq "ES" || $GEOIP_COUNTRY eq "AR" || $GEOIP_COUNTRY eq "PK" || $GEOIP_COUNTRY eq "BG" || $GEOIP_COUNTRY eq "RO" || $GEOIP_COUNTRY eq "KZ" || $GEOIP_COUNTRY eq "TR")
{
#Italy, Peru, Colombia, Vietnam, Taiwan, Poland, United Kingdom, Spain, Argentina, Pakistan, Bulgaria, Romania, Kazakhstan, Turkey
RET_SPAM=$RET_SPAM + 10
RET_SPAM_TESTS="$RET_SPAM_TESTS, geoip_$GEOIP_COUNTRY(10)"
}
elsif($GEOIP_COUNTRY eq "US" || $GEOIP_COUNTRY eq "AP"|| $GEOIP_COUNTRY eq "CA"|| $GEOIP_COUNTRY eq "FR")
{
#United States, Страны Азии( не вошедшие в список), Canada, France
RET_SPAM=$RET_SPAM + 4
RET_SPAM_TESTS="$RET_SPAM_TESTS, geoip_$GEOIP_COUNTRY(4)"
}
##############
if($RET_SPAM >= 10)
{
RET_SPAM_STATUS="YES"
}
}
else
{
RET_SPAM=$RET_SPAM - 10
RET_SPAM_TESTS="local(-10)"
}
if(/^X-Virus-Status: Infected/:h)
{
`$CMD_RAZOR_REPORT`
`$CMD_DCC_REPORT`
xfilter 'reformail -R "Subject:" "Subject: *****VIRUS-VIRUS-VIRUS*****"'
}
if($RET_SPAM < 0)
{
if($BOGO_STATUS ne "Spam" || /^Subject: *****NOT555SPAM*/:h)
{
RET_SPAM_TESTS="$RET_SPAM_TESTS, ham"
`$CMD_BOGOFILTER_HAM`
}
}
elsif($RET_SPAM >= 30 || /^Subject: *****555SPAM*/:h)
{
RET_SPAM_TESTS="$RET_SPAM_TESTS, spam"
`$CMD_RAZOR_REPORT`
`$CMD_DCC_REPORT`
`$CMD_BOGOFILTER_SPAM`
}
`logger -t maildrop -pmail.info "$QUEUE_ID: X-Spam-Status: $RET_SPAM_STATUS, score=$RET_SPAM, tests=$RET_SPAM_TESTS, debug: $CLIENT_ADDRESS[$CLIENT_HOSTNAME]: helo: $CLIENT_HELO, from: $FROM, to: $RCPT_TO_ORIG"`
if($RET_SPAM >= 45)
{
exception {
to "/dev/null"
}
}
REFORMAIL_ADD=""
if($RET_SPAM_STATUS eq "YES")
{
REFORMAIL_ADD="-R "Subject:" "Subject: *****SPAM*****""
}
xfilter "reformail -I"X-Spam-Checker-Version: Spam Checker v.3.1" -I"X-Spam-Status: $RET_SPAM_STATUS, score=$RET_SPAM, tests=$RET_SPAM_TESTS" -I"X-Spam-Flag: $RET_SPAM_STATUS" -I"X-Virus-Scanned:" $REFORMAIL_ADD"
#!/usr/bin/perl -w
# check this file by running it separately
use strict 'subs', 'vars';
use Getopt::Std;
use Socket;
sub dccif {
my($out, # write X-DCC header or entire body to this
$opts, # blank separated string of "spam", ... options
$clnt_addr, # SMTP client IP address as a string
$clnt_name, # null or SMTP client hostname
$helo, # value of SMTP HELO command
$env_from, # envelope Mail_From value
$env_tos, # array of "addressrname" env_To strings
$in, # read body from this
$homedir) = @_; # DCC home directory
my($env_to, $result, $body, $oks, $i);
$homedir = "/var/dcc"
if (! $homedir);
if ($clnt_addr) {
inet_aton($clnt_addr)
|| return ("", "inet_aton($clnt_addr) failed: $!n");
} else {
$clnt_name = '';
}
socket(SOCK, AF_UNIX, SOCK_STREAM, 0)
|| return("", "socket(AF_UNIX): $!n");
connect(SOCK, sockaddr_un("$homedir/dccifd"))
|| return("", "connect($homedir/dccifd): $!n");
# send the options and other parameters to the daemon
$result = dccif_write($opts . "12"
. $clnt_addr . "15" . $clnt_name . "12"
. $helo . "12"
. $env_from . "12",
"opts helo clnt");
return $result if ($result);
foreach $env_to (@$env_tos) {
$result = dccif_write($env_to . "12", "rcpt");
return $result if ($result);
}
$result = dccif_write("12", "end rcpts");
return $result if ($result);
# send the body of the message to the daemon
if (! open(IFH, $in)) {
$result = "?nopen($in): $!n";
close(SOCK);
return $result;
}
for (;;) {
$i = sysread(IFH, $body, 8192);
if (!defined($i)) {
$result = "?nsysread(body): $!n";
close(SOCK);
close(IFH);
return $result;
}
if ($i == 0) {
close(IFH);
last;
}
$result = dccif_write($body, "body");
if ($result) {
close(IFH);
return $result;
}
}
# tell the daemon it has all of the message
if (!shutdown(SOCK, 1)) {
$result = "shutdown($homedir/dccifd): $!n";
close(SOCK);
return $result;
}
# get the result from the daemon
$result = <SOCK>;
if (!defined $result) {
$result = "read($homedir/dccifd): $!n";
close(SOCK);
return $result;
}
$oks = <SOCK>;
if (!defined $oks) {
$result = "read($homedir/dccifd): $!n";
close(SOCK);
return $result;
}
# copy the header or body from the daemon
if (! open(OFH, ">" . $out)) {
#$result = "?nopen($in): $!n";
close(SOCK);
return $result;
}
for (;;) {
$i = read(SOCK, $body, 8192);
return $body;
if (!defined $i) {
$result = "?nread(body): $!n";
close(SOCK);
close(OFH);
return $result;
}
if ($i == 0) {
close(SOCK);
close(OFH);
return $result . $oks;
}
if (! syswrite(OFH, $body)) {
$result = "?nsyswrite($out): $!n";
close(SOCK);
close(OFH);
return $result;
}
}
}
sub dccif_write {
my($buf, $emsg) = @_;
my $result;
if (! syswrite(SOCK, $buf)) {
$result = ("?nsyswrite($emsg): $!n");
close(SOCK);
return $result
}
return "";
}
my($usage);
my(%opts, $clnt_addr, $clnt_name, $rcpt, @rcpts, $body, $result);
$usage=("usage: [-h homedir] [-o opts] [-c clnt-name] [-j host-name] [-l heLo] [-f env_from]n"
. " [-I infile] [-O outfile]"
. " [-r rcpt1[:user1][,rcpt2[:user2],...]]n");
$opts{I} = "-";
$opts{O} = "-";
if (!getopts('h:o:j:c:l:f:I:O:t:r:', %opts) || @ARGV) {
print STDERR $usage;
exit 1;
}
if (! $opts{c}) {
$clnt_addr = "";
$clnt_name = "";
} else {
$clnt_name = $opts{j};
$clnt_addr = $opts{c};
if (!$clnt_addr) {
$clnt_addr = "";
}
else
{
$clnt_addr = $clnt_addr;
$clnt_name = "" if ($clnt_addr eq $clnt_name);
}
}
# make array of env_To recipients
# With no recipients, dccifd will understand a target count of 0. The
# result is like `dccifd -Q`.
@rcpts = ();
if ($opts{r}) {
my(@raw_rcpts, @raw_rcpt);
@raw_rcpts = split(/,/, $opts{r});
while (@raw_rcpts) {
$rcpt = shift(@raw_rcpts);
$rcpt =~ s/;/r/;
push @rcpts, $rcpt;
}
}
$result = dccif($opts{O},
$opts{o} ? $opts{o} : "",
$clnt_addr, $clnt_name,
$opts{l} ? $opts{l} : "",
$opts{f} ? $opts{f} : "",
@rcpts,
$opts{I},
$opts{h} ? $opts{h} : "");
$result =~ s/nn//;
print "$resultn";
$result =~ s/many/999999/ig;
$result =~ s/okd?/0/ig;
my %count = (body => 0, fuz1 => 0, fuz2 => 0);
if ($result =~ /bBody=(d+)/) { $count{body} = $1+0; }
if ($result =~ /bFuz1=(d+)/) { $count{fuz1} = $1+0; }
if ($result =~ /bFuz2=(d+)/) { $count{fuz2} = $1+0; }
#black
if ($count{body} >= 999999 || $count{fuz1} >= 999999) { exit 40; }
if ($count{body} >= 500000 || $count{fuz1} >= 500000) { exit 35; }
if ($count{body} >= 100000 || $count{fuz1} >= 100000) { exit 25; }
if ($count{body} >= 50000 || $count{fuz1} >= 50000) { exit 15; }
if ($count{body} >= 10000 || $count{fuz1} >= 10000) { exit 7; }
if ($count{body} >= 1000 || $count{fuz1} >= 1000) { exit 5; }
if ($count{body} >= 100 || $count{fuz1} >= 100 || $count{fuz2} >= 500) { exit 3; }
if ($count{body} >= 10 || $count{fuz1} >= 10) { exit 2; }
#white
if ($count{body} <= 1 && $count{fuz1} <= 1 && $count{fuz2} <= 2 ) { exit 1; }
#neutral
exit 0;
#!/usr/bin/perl
use Socket;
#DNSBL
%dnsBls = (
'hostkarma.junkemailfilter.com' => '^127.0.0.2$',
'bl.spamcop.net' => '',
'dnsbl.sorbs.net' => '',
'zombie.dnsbl.sorbs.net' => '',
'combined.njabl.org' => '',
'cbl.abuseat.org' => '',
'zen.spamhaus.org' => '',
'dnsbl-1.uceprotect.net' => '',
'ips.backscatterer.org' => '',
'bl.nszones.com' => '^127.0.0.[23]$',
'all.spamrats.com' => '^127.0.0.(36|38)$',
'dnsbl.ahbl.org' => '',
'aspews.ext.sorbs.net' => '',
'dnsbl.dronebl.org' => '',
'b.barracudacentral.org' => '',
'bl.score.senderscore.com' => '',
'psbl.surriel.com' => '',
'dnsbl.abuse.ch' => '',
'list.blogspambl.com' => '',
'dnsbl.dronebl.org' => '',
'abuse.rfc-ignorant.org' => '',
'bogusmx.rfc-ignorant.org' => '',
);
%dnsWls = (
'list.dnswl.org' => '^127.0.[0-9]+.[123]$',
'hostkarma.junkemailfilter.com' => '^127.0.0.1$'
);
sub rbl_ckeck
{
my($ip, %list) = @_;
my $i=0;
while (@dnsbl_serv = each %list)
{
my $check = join(".", reverse(split(/./, $ip))) .".".$dnsbl_serv[0];
my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname($check);
foreach my $item1 (@addrs)
{
my $result = inet_ntoa($item1);
my $def_preg = '^127.d+.d+.d+$';
if($dnsbl_serv[1] ne '') { $def_preg = $dnsbl_serv[1]; }
if($result =~ /${def_preg}/)
{
#print "$ip _ $dnsbl_serv[0] _ $result _ ${def_preg}n";
$i++;
}
}
}
return $i;
}
my $ret = 0;
my $ip = '127.0.0.1';
$ip = $ARGV[1];
if($ARGV[0] eq 'BL') { $ret = rbl_ckeck($ip, %dnsBls); }
elsif($ARGV[0] eq 'WL') { $ret = rbl_ckeck($ip, %dnsWls); }
print "$retn";
Автор: howeal