S25Rスパム対策方式によって正当なメールサーバが誤って拒絶されているのを発見するのに有用なシェルスクリプトを紹介します。メールサーバがウェブサーバを兼ねているなら、このスクリプトをcgi-binディレクトリ配下のディレクトリにパスワード付きで置くことにより、ウェブブラウザで拒絶記録を容易に監視できます。コマンドとして実行することもできます。
このスクリプトは、応答コード「4XX」(「後で再試行せよ」の意味;「XX」は数字の組)で拒絶されたアクセスの記録をPostfixのメールログから抽出し(S25Rによるもの以外も抽出されます)、再試行アクセスが連続して並ぶようにソーティングして表示します。すなわち、クライアントIPアドレス、送信者アドレス、および受信者アドレスとも同じであるアクセスは、連続した行で表示されます。それらのいずれかでも異なるアクセスは、空白行で分断して表示されます。
正当なメールサーバは、応答コード「4XX」による拒絶に対して、必ず適度な時間間隔を置きながら送信を再試行します。その拒絶の記録は、このスクリプトによって連続して表示されます。したがって、ホワイトリストに登録すべき正当なメールサーバからのアクセスを見つけるのに助けになります(理由コードが「C」か「B」でない場合は、S25Rのホワイトリストにホストを登録しても受信できませんので、ご注意ください)。
クライアントが正当か不正か、いずれとも判断しにくい場合は、ひとまずホワイトリストに登録して、もし受信者からスパムの苦情が来たら登録を取り消すのがよいでしょう。
#!/bin/sh
echo "Content-Type: text/plain"
echo
echo "Mail rejection log (4XX) - sorted;" \
"Reason code: C=Client, B=DNSBL, S=Sender, R=Recipient, H=HELO, O=Other"
echo
#
# (1) Input mail log.
#
cat /var/log/maillog.1 /var/log/maillog | \
#
# (2) Extract records indicating "4XX".
#
egrep 'reject: RCPT from [^]]+\]: 4[0-9][0-9] ' | \
#
# (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)
if (match($0, /Client host rejected/))
reason="C"
else if (match($0, /Client host .+ blocked/))
reason="B"
else if (match($0, /Sender address rejected/))
reason="S"
else if (match($0, /Recipient address rejected/))
reason="R"
else if (match($0, /Helo command rejected/))
reason="H"
else
reason="O"
printf "%s %2d %s %s %s %s %s %s\n", \
$1, $2, $3, reason, 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 %s\n", $1, $2, $3, $4, $5, $6, $7, $8, $9
}
' | \
#
# (5) Sort according to IP address, sender address and recipient address.
#
sort -k 6,8 | \
#
# (6) Insert a blank line between records with a different triplet.
#
gawk '
BEGIN {
prev_triplet=""
}
{
if (prev_triplet!="") {
if (prev_triplet!=$6 $7 $8)
print ""
}
print
prev_triplet=$6 $7 $8
}
' | \
#
# (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 %s\n", $1, $2, $3, $4, $5, $6, $7, $8, $9
}
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, $6 $8)==0) {
++msg_count
host_and_rcpt=$6 $8 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
}
'
|
#!/bin/sh
echo "Content-Type: text/html"
echo
echo "<html><body><pre>"
echo "Mail rejection log (4XX) - sorted;" \
"Reason code: C=Client, B=DNSBL, S=Sender, R=Recipient, H=HELO, O=Other"
echo
#
# (1) Input mail log.
#
cat /var/log/maillog.1 /var/log/maillog | \
#
# (2) Extract records indicating "4XX".
#
egrep 'reject: RCPT from [^]]+\]: 4[0-9][0-9] ' | \
#
# (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)
if (match($0, /Client host rejected/))
reason="C"
else if (match($0, /Client host .+ blocked/))
reason="B"
else if (match($0, /Sender address rejected/))
reason="S"
else if (match($0, /Recipient address rejected/))
reason="R"
else if (match($0, /Helo command rejected/))
reason="H"
else
reason="O"
printf "%s %2d %s %s %s %s %s %s\n", \
$1, $2, $3, reason, 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 %s\n", $1, $2, $3, $4, $5, $6, $7, $8, $9
}
' | \
#
# (5) Sort according to IP address, sender address and recipient address.
#
sort -k 6,8 | \
#
# (6) Insert a blank line between records with a different triplet.
#
gawk '
BEGIN {
prev_triplet=""
}
{
if (prev_triplet!="") {
if (prev_triplet!=$6 $7 $8)
print ""
}
print
prev_triplet=$6 $7 $8
}
' | \
#
# (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 %s\n", $1, $2, $3, $4, $5, $6, $7, $8, $9
}
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, $6 $8)==0) {
++msg_count
host_and_rcpt=$6 $8 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
}
' | \
#
# (12) Convert "<" and ">" into entities.
#
gawk '
{
gsub(/</, "\\<")
gsub(/>/, "\\>")
print
}
'
echo "</pre></body></html>"
|