#!/bin/sh -
#### default output if not on an interactive terminal
MAILTO=postmaster
#### filter logging noise
IGNORE='(: hold: |: replace: |: filter: |: Operation timed out|: cannot find your hostname|greylist| warning| postfix.smtp\[|status=bounced|postfix/cleanup.*relay=none|DISCARD|Address verification in progress|/qmgr\[|action=pass,|connect from|broken.example.com)'
#### customizable end of report
SIGNATURE="www.postconf.com/docs/spamrep"
#### maximum number of lines per section
#### don't make this too large or mailed reports could bounce (postconf message_size_limit)
SECTIONMAX=100
#### where to look for logs
LOGDIRS="/var/log /var/adm /var/log/OLD /var/log/archives"
LOGFILES="maillog messages mail.log mail mail.messages"
#### maximum archive levels (i.e, maillog.25)
#### tune this to 2 x max# mail log files created daily
MAXARCHIVES=25
#### bsd-compatible mail required for "-s"
#MAILCMD=mailx
MAILCMD=mail
#### redefine $host only if this is not the mailhost
host=`uname -n|awk -F. '{print $1}'`
######################################################################
# spamrep_today spamrep_yesterday (Postfix version)
#
#
#
# License terms at:
# HTML version available with PostConf
######################################################################
# Usage: spamrep_today
# (prints report to screen)
# or from crontab: spamrep_today mail
# (sends report to account/s in $MAILTO)
#
# Bug reports and feature requests to: support@postconf.com
#
# NOTE: If the server's header_checks and body_checks are blocking
# reports try this modifyication to /etc/postfix/master.cf,
# from:
# pickup fifo n - n 60 1 pickup
# to:
# pickup fifo n - n 60 1 pickup -o receive_override_options=no_header_body_checks
######################################################################
set -a
PATH=/usr/ucb:/bin:/usr/bin:/usr/sbin:/sbin
if [ "$#" -gt 1 ] || [ "$#" -eq 1 -a "$1" != "mail" ]; then
echo " USAGE: `basename $0` [mail]"
exit 1
else
LANG=C
_POSIX2_VERSION=199209
TMP="/tmp/.spamrep.$$"
TOTALFILTERED=0
REPDAYS=365
umask 077
LESS=-ce
#### test minlines after changing the header format
MINLINES=2
fi
cleantmp () {
rm -f $TMP $TMP.spam $TMP.rep $TMP.sum $TMP.today $TMP.rbl $TMP.pre
}
cleanexit () {
cleantmp
exit
}
trap "cleanexit" 0 1 2 3 15
printSecHead () {
SS="`wc -l $TMP.spam 2>/dev/null | awk '{print $1}'`"
#TOTALFILTERED=`expr ${SS} + ${TOTALFILTERED}`
TOTALFILTERED=`echo ${SS} ${TOTALFILTERED} | awk '{ printf "%i", $1 + $2 }'`
echo "" >> $TMP
if [ $SS -eq 1 ]; then
barheader "$SS $sectionTitleS" >> $TMP
echo " $SS $sectionTitleS" >> $TMP.sum
elif [ $SS -gt $SECTIONMAX ]; then
barheader "$SS ${sectionTitleM}, ${SECTIONMAX} shown" >> $TMP
echo " $SS ${sectionTitleM}, ${SECTIONMAX} shown" >> $TMP.sum
else
barheader "$SS $sectionTitleM" >> $TMP
echo " $SS $sectionTitleM" >> $TMP.sum
fi
}
printSecFoot () {
echo "" >> $TMP
head -${SECTIONMAX} $TMP.spam >> $TMP
rm -f $TMP.spam
}
printRBLdetail () {
RBLS=`grep blocked.using $TMP.spam | awk -F\; '{ print $2 }' | awk '{ print $NF }' | sort -u`
if [ "`echo $RBLS | wc -w`" -gt 1 ]; then
rblCalc () {
for rbl in $RBLS ; do
rblhits="`grep $rbl $TMP.spam 2>/dev/null | wc -l 2>/dev/null | sed 's/ *//g'`"
if [ $rblhits -ge 1 ]; then
echo " $rblhits - $rbl [`expr 100 \* $rblhits / $SS`%]"
fi
done
}
rblCalc | sort -rn | \
$AWK '{ printf "%17i %s %s %s %s %s %s %s \n", $1, $2, $3, $4, $5, $6, $7, $8 }' > $TMP.rbl
fi
}
barheader () {
echo "------[ $1 ]----------------------------------------------------------------" | \
$AWK -F"\n" '{ printf "%-.75s", $1 }' 2>/dev/null
}
parselog () {
#### customize sed flags only in case of non-standard syslog format
grep "^$LOGDATE" 2>/dev/null | grep postfix | egrep -iv "$IGNORE" | sort -Mr | \
sed -e 's/ NOQUEUE://' \
-e 's/ proto=ESMTP / /' \
-e 's/ proto=SMTP / /' \
-e 's/ postfix.* reject:/ reject:/' \
-e 's/ postfix.* discard:/ discard:/' \
-e 's/ postfix.* error:/ error:/' \
-e 's/ postfix.* fatal:/ fatal:/' \
-e 's/ postfix.* panic:/ panic:/' \
-e 's/ postfix.* reject_warning:/ reject_warning:/' \
-e 's/ postfix.* warning:/ warning:/' \
-e 's/ postfix.*smtp.*mail.info]//' \
-e 's,'" $host "'postfix\/,'" $TIMEZONE "',' \
-e 's,'" $host "'postfix.\/,'" $TIMEZONE "',' \
-e 's/'" $host "'/ '$TIMEZONE' /' \
>> $TMP.today
}
catlog () {
if [ ! -r ${1} ]; then
continue
elif [ "`echo ${1} | grep '\.gz'$`" != "" ]; then
zcat ${1} | parselog
elif [ "`echo ${1} | egrep '\.(bz|bz2)'$`" != "" ]; then
bzcat ${1} | parselog
else
cat ${1} | parselog
fi
#logger " DEBUG: `basename $0` parsing ${1}"
}
findlogs () {
if [ -s ${1} ]; then
# current log
catlog ${1}
elif [ -s ${1}.gz ]; then
# compressed archive
catlog ${1}.gz
fi
i=0
while [ $i -lt $MAXARCHIVES ]; do
j=${i}
i=`expr $i + 1`
presize=1
if [ -s ${1}.${j} ] && [ "`find ${1}.${j} -mtime -${REPDAYS}`" != "" ]; then
# numbered archive
presize=`wc -c $TMP.today 2>/dev/null | awk '{ print $1 }'`
catlog ${1}.${j}
elif [ -s ${1}.${j}.gz ] && [ "`find ${1}.${j}.gz -mtime -${REPDAYS}`" != "" ]; then
# numbered and compressed archive
presize=`wc -c $TMP.today 2>/dev/null | awk '{ print $1 }'`
catlog ${1}.${j}.gz
fi
postsize=`wc -c $TMP.today 2>/dev/null | awk '{ print $1 }'`
if [ $REPDAYS = 1 ] && [ $presize = $postsize ]; then
## don't search older logs
break
fi
done
}
#################### main ####################
if [ "`uname -s`" = SunOS ]; then
AWK=nawk
else
AWK=awk
fi
TIMESTAMP="`date`"
TIMEZONE="`echo $TIMESTAMP | awk '{print $5}'`"
if [ "`basename $0 | grep -i yesterday`" != "" ]; then
#### basename = *yesterday* ####
REPDAYS=2
if [ "`echo $TIMESTAMP | awk '{ print $3 }'`" = 1 ]; then
#### unreliable cross-platform hack for 1st of the month
TZ="`echo $TIMEZONE`+31"
TIMESTAMP="`date`"
LOGDATE="`echo $TIMESTAMP | $AWK '{printf "%3s%3s", $2, $3}'`"
DAY="`echo $TIMESTAMP | awk '{print $2, $3, $NF}'`"
else
LOGDATE="`echo $TIMESTAMP | $AWK '{printf "%3s%3s", $2, $3-1}'`"
DAY="`echo $TIMESTAMP | awk '{print $2, $3-1, $NF}'`"
fi
else
#### basename = *today* ####
REPDAYS=1
LOGDATE="`echo $TIMESTAMP | $AWK '{printf "%3s%3s", $2, $3}'`"
DAY="`echo $TIMESTAMP | awk '{print $1, $2, $3, $NF}'`"
fi
cleantmp
cp -f /dev/null $TMP.today
for dir in $LOGDIRS ; do
if [ -d $dir ]; then
for log in $LOGFILES ; do
#### parse logs into $TMP.today
findlogs ${dir}/${log}
done
fi
done
################### list by filter type ###################
egrep -i '(reject: header|discard:|message.size.exceeds|access.denied|Message.content.rejected)' $TMP.today | \
egrep -iv '(user.unknown|discard: header X-Amavis.*: INFECTED|discard: header X-Spam| reject_warning: |Recipient.address.rejected|helo.command.rejected|domain.not.found|need.fully-qualified|Relay access denied)' | \
sed 's/: Access denied//' > $TMP.spam
if [ -s $TMP.spam ]; then
sectionTitleS='rejected by localhost'
sectionTitleM="$sectionTitleS"
printSecHead
printSecFoot
fi
grep -i "Recipient.address.rejected" $TMP.today | \
sed -e 's/Recipient address rejected: //' -e 's/ from local;//' > $TMP.spam
if [ -s $TMP.spam ]; then
sectionTitleS='invalid recipient'
sectionTitleM="$sectionTitleS"s
printSecHead
printSecFoot
fi
grep 'Relay access denied' $TMP.today > $TMP.spam
if [ -s $TMP.spam ]; then
sectionTitleS='relay attempt'
sectionTitleM="$sectionTitleS"s
printSecHead
printSecFoot
fi
egrep -i '(helo.command.rejected|domain.not.found|need.fully-qualified)' $TMP.today | \
egrep -iv '( reject_warning: |reject: header|discard:|message.size.exceeds|access.denied|Recipient.address.rejected|Message.content.rejected)' > $TMP.spam
if [ -s $TMP.spam ]; then
sectionTitleS='protocol error'
sectionTitleM="$sectionTitleS"s
printSecHead
printSecFoot
fi
egrep -i user.unknown $TMP.today | \
egrep -v '(Recipient address rejected| reject_warning: )' | \
sed 's/ in local recipient table//' > $TMP.spam
if [ -s $TMP.spam ]; then
sectionTitleS='user unknown'
sectionTitleM="$sectionTitleS"s
printSecHead
printSecFoot
fi
egrep -i '( warning: | error: | fatal: | panic: )' $TMP.today | \
egrep -v '( reject_warning: |older than source)' > $TMP.spam
if [ -s $TMP.spam ]; then
sectionTitleS="notice"
sectionTitleM="$sectionTitleS"s
printSecHead
printSecFoot
fi
grep " reject_warning: " $TMP.today | sed -e 's/blocked using/listed by/' \
-e 's/reject_warning: /warn_only: /' -e 's/ ... Service unavailable; //' > $TMP.spam
if [ -s $TMP.spam ]; then
sectionTitleS='warn-only notice'
sectionTitleM="$sectionTitleS"s
printSecHead
printSecFoot
fi
#### assumes "/^X-Spam-Level: \*\*\*\*\*\*/ DISCARD" or equivalent header_checks
grep -i "discard: header X-Spam" $TMP.today | \
sed -e 's/ helo=.localhost.//' -e 's/from localhost.*127.0.0.1.; //' \
-e 's/ from local;//' > $TMP.spam
if [ -s $TMP.spam ]; then
sectionTitleS='discarded by spamassassin'
sectionTitleM="$sectionTitleS"
printSecHead
printSecFoot
fi
#######################################################################################
## For optimal AV logging A) configure amavis to "$final_virus_destiny = D_PASS;",
## and "$policy_bank{'MYNETS'} = { bypass_banned_checks_maps => [1], };", then
## configure Postfix to both B) send XFORWARD "smtp_send_xforward_command = yes",
## C) and receive it "smtpd_authorized_xforward_hosts = localhost", finally
## D) discarding the results via header_checks "/^X-Amavis-Alert: INFECTED/ DISCARD".
#######################################################################################
#### assumes clam-av, amavis discarded by header_check ####
egrep '(discarded, .* - VIRUS|discard: header X-Amavis.*: INFECTED)' $TMP.today | \
sed -e 's/ '$TIMEZONE' .*: to/'$TIMEZONE' virus: to/' \
-e 's/ helo=.*$//' \
-e 's/ from localhos.*\[127.0.0.1\]//' \
-e 's/, relay=127.0.0.1\[127.0.0.1\].*status=sent//' > $TMP.spam
if [ -s $TMP.spam ]; then
sectionTitleS='discarded virus'
sectionTitleM="$sectionTitleS"es
printSecHead
printSecFoot
fi
grep -i "blocked.using" $TMP.today | grep -v " reject_warning: " | \
sed -e 's/ Service unavailable//' \
-e 's/Barracuda Reputation, see/b.barracudacentral.org;/' \
-e 's/ Client host .* blocked using / blocked using /' > $TMP.spam
if [ -s $TMP.spam ]; then
sectionTitleS='rejected by subscription'
sectionTitleM="$sectionTitleS"
printSecHead
printRBLdetail
printSecFoot
fi
################### finish header and view or email ###################
echo "" >> $TMP
if [ "`wc -l $TMP 2>/dev/null | awk '{print $1}'`" -lt $MINLINES ]; then
echo " No filter activity logged for $LOGDATE"
cleanexit
else
#### build header
echo "" > $TMP.pre
barheader "$host mailstats for $DAY" >> $TMP.pre
echo "" >> $TMP.pre
echo "" >> $TMP.pre
echo "$TOTALFILTERED total filtered" | $AWK '{ printf "%11i %s %s \n", $1, $2, $3}' >> $TMP.pre
sort -nr < $TMP.sum | \
$AWK '{ printf "%14i %s %s %s %s %s %s %s %s \n", $1, $2, $3, $4, $5, $6, $7, $8, $9 }' >> $TMP.pre
#### add rbl detail
if [ -s $TMP.rbl ] && [ "`grep 'rejected by subscription' $TMP.pre`" != "" ]; then
REPLINE=`grep -n 'rejected by subscription' $TMP.pre | awk -F: '{print $1}'`
head -${REPLINE} $TMP.pre > $TMP.rep
cat $TMP.rbl >> $TMP.rep
tail +`expr $REPLINE + 1` $TMP.pre >> $TMP.rep
else
mv -f $TMP.pre $TMP.rep
fi
#### add report body
cat $TMP >> $TMP.rep
#### add footer
if [ "$SIGNATURE" ]; then
barheader "end of report - ${SIGNATURE}" >> $TMP.rep
else
barheader "end of report - www.postconf.com/docs/spamrep" >> $TMP.rep
fi
echo "" >> $TMP.rep
fi
if [ "$1" = "mail" ]; then
$MAILCMD -s "$host mailstats for $DAY" $MAILTO < $TMP.rep
else
######## which viewer ########
if [ "`which less 2>/dev/null`" != "" ]; then
PAGER=less
else
PAGER=${PAGER:=more}
fi
$PAGER $TMP.rep
fi
cleanexit