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

#### filter out garbage in
IGNORE='(: hold: |: replace: |: filter: |broken.example.com|: Operation timed out|: cannot find your hostname|greylist| warning| postfix.smtp\[|status=bounced|cleanup.*relay=none|DISCARD|Address verification in progress|connect from|/qmgr\[|action=pass,)'

#### 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)
#
#              <http://www.postconf.com/docs/spamrep/> 
#
# License terms at: <http://www.roble.com/docs/RobleBSDlicense.html>
# HTML version available with PostConf <http://www.postconf.com/>
######################################################################
# 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
#
######################################################################
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=`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 -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"\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' /' \
	    -e 's/[[:space:]][[:space:]]*/ /g' \
	>> $TMP.today
}

catlog () {
	if [ ! -r ${1} ]; then
		continue
		## probably not running as root
	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
}

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/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 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 <user2> ... ]"
		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
