#!/bin/sh -
set -a
PATH=/usr/ucb:/bin:/usr/bin:/usr/sbin:/sbin:/usr/lib
#### reports sent to email addresses listed, one per line, in this file
USERLIST=/etc/postfix/reportrecipient
#### MAILFROM abuse by default
MAILFROM=abuse
#### filter out garbage in
IGNORE='(:.hold:|Policy Rejection|Greylisted| warning|postfix.smtp\[|status=bounced|postfix/cleanup.*relay=none|DISCARD|Address verification in progress|450 4.7.1 Client host rejected: cannot find your hostname|: filter: |/qmgr\[|action=pass,|connect from|broken.example.com)'
#### maximum number of lines per section
#### don't make this too large or mailed reports could bounce (postconf message_size_limit)
SECTIONMAX=5000
#### maximum archive levels (i.e, maillog.25)
#### tune this to 2 x max# mail log files created daily
MAXARCHIVES=4
#### where to look for logs
LOGDIRS="/var/log /var/adm /var/log/OLD"
LOGFILES="maillog messages mail.log mail mail.messages"
#### customizable end of report
SIGNATURE="www.postconf.com/docs/spamrep"
#### use: MAILCMD="sendmail -oem -oi -t -f $MAILFROM" with sendmail >V8.8
#### "-t" should work with Postfix versions Snapshot-20030611 or newer
MAILCMD="sendmail -oem -oi -f $MAILFROM"
#### redefine $host only if localhost is not the mailhost
host=`uname -n|awk -F. '{print $1}'`
DEBUG=NO #[Y/y/else N]
DEBUG_MAILTO=${MAILFROM}
######################################################################
# spamrep_today_byuser spamrep_yesterday_byuser (Postfix version)
#
#
#
# License terms at:
# HTML version available with PostConf
######################################################################
# Usage A: spamrep_today_byuser
# (prints report to screen)
# Usage B: spamrep_today_byuser mail
# (mails report to users listed in $USERLIST)
# NOTE1: May fail under Redhat 9 due to a bug in its 'sort' command.
# To fix install GNU coreutils-5.0 or later.
# NOTE2: 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
#
######################################################################
# $Id: spamrep_today_byuser,v 1.130 2012/03/28 04:24:13 marquis Exp marquis $
######################################################################
LANG=C
_POSIX2_VERSION=199209
TMP="/tmp/.spamrepu.$$"
REPDAYS=3
umask 077
LESS=-ce
TOTALFILTERED=0
#### test minlines when changing the report
MINLINES=7
cleantmp () {
rm -f $TMP $TMP.spam $TMP.rep $TMP.sum $TMP.rbl $TMP.pre
}
cleanexit () {
cleantmp
rm -f $TMP.today
exit
}
trap "cleanexit" 0 1 2 3 15
printSecHead () {
SS="`wc -l $TMP.spam 2>/dev/null | awk '{print $1}'`"
TOTALFILTERED=`expr ${SS} + ${TOTALFILTERED}`
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=`sed -e 's/^.*blocked using //' -e 's/, reason.*$//' -e 's/;.*$//' $TMP.spam | 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 -gt 1 ]; then
echo " $rblhits - $rbl [`expr 100 \* $rblhits / $SS`%]"
#fi
done
}
rblCalc | sort -nr | \
$AWK '{ printf "%15i %s %s %s %s %s %s %s \n", $1, $2, $3, $4, $5, $6, $7, $8 }' > $TMP.rbl
fi
}
barheader () {
echo "------[ $1 ]----------------------------------------------------------------" | \
awk -F"\/\/" '{ 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 -v "$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
## probably not running as root
elif [ "`echo ${1} | grep '.gz'$`" != "" ]; then
zcat ${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
}
report_on_account () {
account=$1
#################### list by unique sender ###########################
grep -i "to=.${account}" $TMP.today 2>/dev/null | \
grep -v " reject_warning: " | grep 'from=<' | \
sed -e 's/^.* from= ' -e 's/> to=<.*$/>/' -e 's/>, to=<.*$/>/' | sort -t\< +1f | \
sort -u 2>/dev/null > $TMP.spam
UNIQUE="`wc -l $TMP.spam 2>/dev/null | awk '{print $1}'`"
if [ -s $TMP.spam ]; then
sectionTitleS='unique sender'
sectionTitleM="$sectionTitleS"s
printSecHead
printSecFoot
fi
#### correct double counting
TOTALFILTERED=0
grep -i "to=.${account}" $TMP.today 2>/dev/null | \
egrep -i '(reject: header|discard:|essage.size.exceeds|access.denied|Recipient.address.rejected|Message.content.rejected)' | \
egrep -iv '(user.unknown|discard: header X-Amavis.*: INFECTED|discard: header X-Spam| reject_warning: |helo.command.rejected|domain.not.found|need.fully-qualified)' | \
sed 's/: Access denied//' > $TMP.spam
if [ -s $TMP.spam ]; then
sectionTitleS='rejected by localhost'
sectionTitleM="$sectionTitleS"
printSecHead
printSecFoot
fi
grep -i "to=.${account}" $TMP.today 2>/dev/null | \
egrep -i '(helo.command.rejected|domain.not.found|need.fully-qualified)' | \
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
grep -i "to=.${account}" $TMP.today 2>/dev/null | \
grep " reject_warning: " | \
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 "to=.${account}" $TMP.today 2>/dev/null | \
grep -i "discard: header X-Spam" | \
grep -i " to=.${account}" | \
sed -e 's/ proto=ESMTP 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 ####
grep -i "to=.${account}" $TMP.today 2>/dev/null | \
egrep '(discarded, .* - VIRUS|discard: header X-Amavis.*: INFECTED)' | \
sed -e 's/'$host' .*: to/'$TIMEZONE' virus: to/' \
-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 "to=.${account}" $TMP.today 2>/dev/null | \
grep -i "blocked.using" | grep -v " reject_warning: " | \
sed -e 's/ Service unavailable//' \
-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 mail ##################
echo "" >> $TMP
if [ "`wc -l $TMP 2>/dev/null | awk '{print $1 }'`" -lt $MINLINES ]; then
cleantmp
continue
else
#### build header
if [ $MAIL = y ]; then
#### "To: ${account}" is required but may be ignored if recipient is
#### specified on the command line.
echo "From: ${MAILFROM}
To: ${account}
Reply-To: ${MAILFROM}
Subject: $DAY mailstats for $account
" >> $TMP.pre
fi
echo "" >> $TMP.pre
barheader "$DAY mailstats for $account" >> $TMP.pre
echo "" >> $TMP.pre
echo "" >> $TMP.pre
SENDERS="sender"
if [ "$UNIQUE" -ne 1 ]; then
SENDERS="${SENDERS}s"
fi
echo "$TOTALFILTERED total filtered from $UNIQUE $SENDERS" | \
$AWK '{ printf "%9i %s %s %s %s %s %s %s %s \n", $1, $2, $3, $4, $5, $6, $7, $8, $9 }' >> $TMP.pre
sort -nr < $TMP.sum | \
$AWK '{ printf "%12i %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 | grep -v "unique sender" > $TMP.rep
cat $TMP.rbl >> $TMP.rep
tail +`expr $REPLINE + 1` $TMP.pre | grep -v "unique sender" >> $TMP.rep
else
grep -v "unique sender" $TMP.pre > $TMP.rep
fi
#### add note
echo " " >> $TMP.rep
echo " Reply to this email or forward to $MAILFROM if" >> $TMP.rep
echo " you find any false-positives or have questions about" >> $TMP.rep
echo " spam filtering." >> $TMP.rep
echo " " >> $TMP.rep
echo " Important reminder: please do not reply to unsolicited " >> $TMP.rep
echo " email." >> $TMP.rep
#### 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
if [ $MAIL = y ]; then
if [ "$DEBUG" = y ] || [ "$DEBUG" = Y ]; then
$MAILCMD $DEBUG_MAILTO < $TMP.rep
else
$MAILCMD $account < $TMP.rep
fi
elif [ -s $TMP.rep ]; then
clear
less $TMP.rep
echo "Press [RETURN] to continue"
read x
echo "processing, please wait..."
fi
cleantmp
fi
}
#################### 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
############### parse the command line #####################
case $1 in
h|-h|--h|help|-help|--help)
echo " USAGE: `basename $0` [ mail ] [ user1 ... ]"
exit 0
;;
mail)
if [ "$2" = "" ]; then
#### list -> mail ####
MAIL=y
LIST=y
else
#### cmnd line userlist -> mail ####
MAIL=y
LIST=n
shift
fi
;;
"")
#### list -> display ####
MAIL=n
LIST=y
;;
*)
#### cmnd line userlist -> display ####
MAIL=n
LIST=n
;;
esac
############### run the reports ############################
if [ $LIST = n ]; then
for account in $* ; do
report_on_account $account
done
else
if [ ! -s $USERLIST ]; then
report_on_account `whoami`
else
for account in `grep -v '^#' $USERLIST | awk '{ print $1 }'` ; do
report_on_account $account
done
fi
fi
cleanexit