#!/users2/local/bin/perl -w
#!/users2/local/bin/perl

##
# smreject.pl - Processes rejected message entries from sendmail logfile.
##

# Ver 1.2  by Ralf Hildebrandt <R.Hildebrandt@tu-bs.de>
#          May 30, 1998

#
# usage:  smreject [-t top#] [-f logfile] [-j] [-m addr]
#
# -t n        list top n hosts or addresses for each ruleset.
#            n = 0 for no detail by host or email address.
#
# -f logfile  use the given logfile instead of the default one.
#
# -j         use the "junk file" to provide further information.
#
# -m addr    mail the output to the given address
#
# -d         enable debug mode (this is just for me)



# Ver 1.2    Ralf Hildebrandt <R.Hildebrandt@tu-bs.de>
#
#            * Have completely rewritten the following (30/05/1998):
#               - Logfileparser (some sort of FA now)
#               - Output routine (made that a function, gives us the From: now)
#               - Regexp'es for matching the lines of the logfile (now match correctly)
#               - other_reasons() renamed to find_reason() and cleaned/sped up.
#
# Ver 1.13    Ralf Hildebrandt <R.Hildebrandt@tu-bs.de>
#            * Sendmail 8.9 has come...
#            * Added support for all new rulesets.
#            * Wrote the findinjunk() routine -- it replaces findhostname(), 
#              findemail() and findIP() (which was not used anyway...)
#            * Fixed the usage line.
#            * Made use of junk file (/etc/mail/access) default.
#              Important: Only entries NOT ending with OK or RELAY are junk entries! 
#            * Changed the routines for counting junk file entries
#              by splitting them into two counters, one found_in_junk
#              and one find_reason (e.g. RBL, no DNS etc.)
#            * Added debug mode

# Ver 1.12a (unofficial, Anne Bennett <anne@alcor.concordia.ca>)
#            Make "junk" optional; turn on iff opt_j. (Oct 22, 1997)
#            Initialize variables to avoid perl complaints if
#            certain counters are never incremented. (Oct 22, 1997)
#            Comment out never-used assignments. (Oct 22, 1997)
#            Highlight "configuration section". (Oct 22, 1997)
#            Initialize $top if not set with "-t". (Oct 22, 1997)
#            Added an option to change the logfile used. (Oct 22, 1997)
#            Print nothing if no rejects were found. (Oct 22, 1997)
#            Added option to mail output. (Oct 23, 1997)
#
# Ver 1.12 by Ted George  (tgeorge@kcnet.com)  
#            Count for junk file rejections included check_mail
#            errors.  (Sep 24, 1997)
#
# Ver 1.11   Check_mail rejections from domain names were
#            not properly matched.  (Sep 18, 1997)
#
# Ver 1.1    Cleaned up output slightly.  Minor fixes including
#            the unknown category for entries that don't match
#            any items in junk file.  (Sep 10, 1997)
#
# Ver 1.0    Original release  (Sep 5, 1997)
#

# =======================================================
# change these locations to match your system
#
# location of sendmail log file
$logfile = "/usr/adm/maillog";

# location of the plaintext access file (not the .db/.dbm/.hash !)
$junk = "/etc/mail/access";

# mail program which accepts "-s subject", address on command line,
# and message on standard input.
$mailprog = "mutt";

# path, used to call "cut" and the mail program
$ENV{PATH} = "/users2/local/bin:/usr/local/bin:/usr/bin:/bin";
# =======================================================

# initialize variables
$check_rcpt = 0;
$check_from = 0;
$check_received = 0;
$check_message_id = 0;

$found_in_junk = 0;
$other_reasons = 0;
$mail_errors = 0;

# parse command-line options
require "getopts.pl";
&Getopts("f:jm:t:d");
$logfile = "$opt_f" if $opt_f;
$dojunk  = 1;
$dojunk  = "$opt_j" if $opt_j;
$mailto  = "$opt_m" if $opt_m;
$top     = 10;
$top     = "$opt_t" if $opt_t;
$debug   = "$opt_d" if $opt_d;

if ($mailto) {
    # Sanitize "mailto" to satisfy "-T":  address should contain
    # nothing but "word" characters (alphabetics, numerics, and
    # underscores), a hyphen, an at sign, a comma, or a dot.
    if ($mailto =~ /^([-\@\w,.]+)$/) {
	$mailto = $1;
    }
    else {
	die "Bad characters in mail address: $mailto";
    }
    $real_stdout = select();
    open(MAILIT, "| $mailprog -s 'smreject results' $mailto")
	or die("Cannot fork $mailprog");
    select(MAILIT);
}

if ($dojunk) {
    @junk = `grep -E -v "(OK|RELAY)\$" $junk | cut -d" " -f1`;
    # now works with custom error messages; Thanks to Jan Krueger <Jan.Krueger@stud.uni-hannover.de>
    chop(@junk);
}
open(INPUT, "$logfile") || die "Can't open $logfile.";

$id = 0;
$old_id = 1;

while (<INPUT>) {

    $first = $_ if ! $first;
    $last  = $_;

    $id    = $1 if (/sendmail\[\d+\]: (\D{3}\d{5})/);

    if (/ruleset=/) {
	$old_id  = $id;
        # Changed this! Now we match everything!
	$ruleset = $1 if (/ruleset=(.+), arg1=/);
	$arg1    = $1 if (/arg1=(.+), relay=/);
	$relay   = $1 if (/relay=(.+), reject=/);
	$reject  = $1 if (/reject=([^,]+)$/);
	if ($debug) {
	    print "Ruleset:\t",$ruleset,"\n", 
	    "Arg1:\t\t",$arg1,"\n", 
	    "Relay:\t\t",$relay,"\n", 
	    "Reject:\t\t",$reject; 
	}
        $next    = 1;
    } elsif (($old_id eq $id) && $next) {
        $from    = $1 if (/from=([^,]+),/);
	if ($debug) {
	    print "From:\t\t",$from,"\n\n";
	}
	$next = 0;
        # Now we have gathered all the data we need.

        if ($dojunk) {
	    $$ruleset++;
            # I wouldn't have believed that this works...
	    if ($ruleset =~ /check_(received|message_id|from)/) {
		# Look for $arg1 in junk 
		$relevant = $arg1;
	    } elsif ($ruleset =~ /check_rcpt/) {
		# Look for $from in junk
		$relevant = $from;
	    } else {
		die "Unknown ruleset $ruleset found.";		
	    }

	    find_reason($relevant,$reject);

	    $name = "\$count_" . $ruleset;
	    $$name{$relevant."\n\tFrom: ".$from}++;
	    $name = "\$count_relay_" . $ruleset;
	    $$name{$relay}++;
        }

    } else {
	$ruleset = 0;
	$old_id  = 0;
	$relay   = 0;
	$arg1    = 0;
	$next    = 0;
	next;
    }
}

$totreject = $check_from + $check_rcpt + $check_received + $check_message_id;
if ( $totreject > 0 ) {
    $from = "unknown"; $to = "unknown";
    $from = $1 if $first =~ /^(.{7}\d\d:\d\d:\d\d )/;
    $to = $1 if $last =~ /^(.{7}\d\d:\d\d:\d\d )/;
    print "\nSendmail Rejected Messages for $from - $to\n";
    print "-----------------------------------------------------------------\n";
    print "$totreject rejections processed\n";

    if ($dojunk) {
	if ($found_in_junk > 0) {
	    print "\n\n*** $found_in_junk Rejections due to junk data file entry ***\n\n";
	    foreach (@junk) {
		&prjunk_entry($_);
	    }
	    &prjunk_entry("unknown");
	}
    }	

    if ($other_reasons > 0) {
	print "\n\n*** $other_reasons Rejections due to other reasons ***\n\n";
	&prjunk_entry("Bad Message-Id");
	&prjunk_entry("DNS failure. Client must resolve");
	&prjunk_entry("Client must resolve");
	&prjunk_entry("RBL Entry");
    }
    
    print_stats("check_message_id");
    print_stats("check_from");
    print_stats("check_received");
    print_stats("check_rcpt");
    
    if ($mailto) {
	select($real_stdout);
	close(MAILIT);
    }
}


sub sortbyvalues {
    local(@data);
    local(@datakeys);
    
    while (($key, $value) = each(%tmp)) {
	push(@data, sprintf("%6d %-s\n", $value, $key));
	push(@datakeys, sprintf("%6d", $value));
    }
    
    sub bydatakeys { 
	$datakeys[$a] <=> $datakeys[$b]; 
    }
@data = @data[reverse sort bydatakeys $[..$#data];
$#data = $top - 1 if ($#data >= $top);
return @data;
}

sub findinjunk {
    local($_) = @_;

    foreach $junk_addr (@junk) {
	return $junk_addr if /$junk_addr/;
    }
}

sub prjunk_entry {
    local($_) = @_;
    if ($count_junk{$_}) {
	printf("%6d %-s\n", $count_junk{$_}, $_) if ($count_junk{$_} > 0);
    }
}

sub print_stats {
    local $ruleset = $_[0];
    local $name;
    local %tmp;

    if ($$ruleset > 0) {
	print "\n\n*** $$ruleset rejections by ruleset $ruleset ***\n" ;
	if ($top > 0) {
	    print "\nTop $top $ruleset rejections by relevant argument and From: :\n\n";
	    $name = "\$count_" . $ruleset;
	    %tmp = %$name;
	    print &sortbyvalues();
	    print "\nTop $top $ruleset rejections by mail relay host:\n\n";
	    $name = "\$count_relay_" . $ruleset;
	    %tmp = %$name;
	    print &sortbyvalues();
	}
    }
}

sub find_reason {
    local $key = $_[0];
    local $reject = $_[1];

    local $junk_entry;

    $junk_entry = "Bad Message-Id" if ($reject =~ /Bad Message-Id/);
    $junk_entry = "DNS failure. Client must resolve" if ($reject =~ /451 DNS failure/);
    $junk_entry = "Client must resolve" if ($reject =~ /550 Client must resolve/);
    $junk_entry = "blackholed" if ($reject =~ /http:maps.vix.com/);

    if ($junk_entry) {
	$other_reasons++; 
    } else {
	$junk_entry = &findinjunk($key);
	if ($junk_entry) {
	    $found_in_junk++; 
	} else {
	    $junk_entry = "unknown";
	    if ($debug) {
		print "Key: ",$key," Reject: ", $reject,"\n";
	    }
	    $mail_errors++;
	}
    }
    $count_junk{$junk_entry}++;
    if ($debug) {
	print "Reason:\t\t",$junk_entry,"\n\n";
    }
}

