S25Rスパム対策方式によって正当なメールサーバが誤って拒絶されているのを発見するのに有用なシェルスクリプトを紹介します。メールサーバがウェブサーバを兼ねているなら、このスクリプトをcgi-binディレクトリ配下のディレクトリにパスワード付きで置くことにより、ウェブブラウザで拒絶記録を容易に監視できます。コマンドとして実行することもできます。
このスクリプトは、クライアント制限によって応答コード「450」(「後で再試行せよ」の意味)で拒絶されたアクセスの記録をPostfixのメールログから抽出し(ほかの理由による拒絶は抽出されません)、再試行アクセスが連続して並ぶようにソーティングして表示します。すなわち、クライアントIPアドレス、送信者アドレス、および受信者アドレスとも同じであるアクセスは、連続した行で表示されます。それらのいずれかでも異なるアクセスは、空白行で分断して表示されます。
正当なメールサーバは、応答コード「450」による拒絶に対して、必ず適度な時間間隔を置きながら送信を再試行します。その拒絶の記録は、このスクリプトによって連続して表示されます。したがって、ホワイトリストに登録すべき正当なメールサーバからのアクセスを見つけるのに助けになります。
クライアントが正当か不正か、いずれとも判断しにくい場合は、ひとまずホワイトリストに登録して、もし受信者からスパムの苦情が来たら登録を取り消すのがよいでしょう。
#!/bin/sh
echo "Content-Type: text/plain"
echo
echo "Mail rejection log (450 Client host rejected) - sorted"
echo
#
# (1) Input mail log.
#
cat /var/log/maillog.1 /var/log/maillog | \
#
# (2) Extract records indicating "450 Client host rejected".
#
egrep 'reject:.+ 450 .*Client host rejected:' | \
#
# (3) Extract essential items.
#
gawk '
{
client=substr($0, match($0, /from [^]]+\]/)+5, RLENGTH-5)
sub(/\[/, " [", client)
sender=substr($0, match($0, /from=<[^>]*>/), RLENGTH)
rcpt=substr($0, match($0, /to=<[^>]*>/), RLENGTH)
helo=substr($0, match($0, /helo=<[^>]*>/), RLENGTH)
printf "%s %2d %s %s %s %s %s\n", $1, $2, $3, client, sender, rcpt, helo
}
' | \
#
# (4) Convert month names into month numbers.
#
gawk '
BEGIN {
month_num["Jan"]=1
month_num["Feb"]=2
month_num["Mar"]=3
month_num["Apr"]=4
month_num["May"]=5
month_num["Jun"]=6
month_num["Jul"]=7
month_num["Aug"]=8
month_num["Sep"]=9
month_num["Oct"]=10
month_num["Nov"]=11
month_num["Dec"]=12
max_month_num=0
}
{
$1=month_num[$1]
if ($1>max_month_num)
max_month_num=$1
else if ($1<max_month_num)
$1+=12
printf "%3d %2d %s %s %s %s %s %s\n", $1, $2, $3, $4, $5, $6, $7, $8
}
' | \
#
# (5) Sort according to IP address, sender address and recipient address.
#
sort -k 5,7 | \
#
# (6) Insert a blank line between records with a different triplet.
#
gawk '
BEGIN {
prev_triplet=""
}
{
if (prev_triplet!="") {
if (prev_triplet!=$5 $6 $7)
print ""
}
print
prev_triplet=$5 $6 $7
}
' | \
#
# (7) Convert retry records in a sequence into one line.
#
gawk '
BEGIN {
RS=""
}
{
gsub(/\n/, "\036")
print
}
' | \
#
# (8) Sort according to date and time.
#
sort -k 1,3 | \
#
# (9) Reconvert retry records in a sequence into multiple lines.
#
gawk '
{
gsub(/\036/, "\n")
print
print ""
}
' | \
#
# (10) Reconvert month numbers into month names.
#
gawk '
BEGIN {
month_name[1]="Jan"
month_name[2]="Feb"
month_name[3]="Mar"
month_name[4]="Apr"
month_name[5]="May"
month_name[6]="Jun"
month_name[7]="Jul"
month_name[8]="Aug"
month_name[9]="Sep"
month_name[10]="Oct"
month_name[11]="Nov"
month_name[12]="Dec"
}
{
if ($0!="") {
$1=month_name[($1-1)%12+1]
printf "%s %2d %s %s %s %s %s %s\n", $1, $2, $3, $4, $5, $6, $7, $8
}
else
print ""
}
' | \
#
# (11) Output sorted records with counting.
#
gawk '
BEGIN {
Suppress_single_access_records=0
RS=""
acc_count=0
host_and_rcpt=""
msg_count=0
seq_count=0
}
{
retry_count=gsub(/\n/, "\n")
acc_count+=1+retry_count
if (index(host_and_rcpt, $5 $7)==0) {
++msg_count
host_and_rcpt=$5 $7 host_and_rcpt
}
if (retry_count>0)
++seq_count
if (!(retry_count==0 && Suppress_single_access_records)) {
print
print ""
}
}
END {
print "access count =", acc_count, \
", estimated message count =", msg_count, \
", retry sequence count =", seq_count
}
'
|