Методика детектирования SPAM в e-mail сообщениях

в 14:52, , рубрики: dkim, email, postfix, razor, smtp, spf, Серверное администрирование, Спам (и антиспам), хостинг, электронная почта, метки: , , , , , , , ,

Доброго времени суток!
Я работаю системным администратором в небольшой телекоммуникационной компании. В 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.

Основные моменты в конфиге Postfix (main.cf)

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

Основные моменты в syslog-ng

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); };

Основные моменты в конфиге Postfix (master.cf)

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

Скрипт maildrop

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"

Скрипт взаимодействия с DCC /usr/local/dcc/dccif.pl

#!/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;

Скрипт проверки RBL-списков /usr/local/dnsbl/rbl-check.pl

#!/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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js