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