#!/usr/bin/perl -w

# Name: find-recipients.pl
# Purpose: Checks a given sendmail maillog file for a specified
# sender email address and returns a list of recipients email addresses
# to which email was sent.

# Written By:    Jim Cameron <scripts (at) moonpoint.com>
# Created:       March 31, 2006
# Last Modified: March 31, 2009
# Version:       1.3

# Download URL: 
# http://support.moonpoint.com/downloads/computer_languages/perl/find-recipients.pl

# Usage: find-recipients [--status status] [--to recipient] sender [maillog]
#
# Unless specified, maillog is assumed to be $maillog.
#
# Optional arguments:
#
# A count will be provided of all messages from the sender, but only those which
# match the specified criteria will be displayed. The number listed for
# the count and the number of message ids displayed may not always match.
# Sometimes a message will have a "from" entry in the log file, but no
# "to" entries.
# 
# --status status - the status is as specified, e.g. "Sent" or "Deferred" 
# --to recipient  - the specified recipient is in the "to" field.

# Note: This script is based on a script by David Miller named findrecpt.pl and
# also named checkrecpt.pl, which he created to check a postfix maillog file.
# This script is designed to parse a sendmail log file and has modifications
# and addtions I made to meet my own needs.
#
# Mr. Miller's script, which was last modified on July 25, 2002 can be found
# at http://www.irbs.net/internet/postfix/0208/att-0794/findrecpt.pl. 
# His comments on the script as well as a link to his findsender.pl
# script can be found at http://www.irbs.net/internet/postfix/0208/0794.html

# Anyone who finds any use for this script is free to modify it to suit his or
# her own needs without restriction, though I would appreciate notification
# of any bug fixes or additions made, so that I can incorporate them in 
# my own script. 

use strict;

# Getopt::Long allows one to parse arguments on the command line
# See http://aplawrence.com/Unix/perlgetopts.html and
# http://www.unix.org.ua/orelly/perl/prog/ch07_035.htm for further
# information.
use Getopt::Long;

#--------------
# variables
#--------------

my @fields;
my $title = "";  # Unless the title is specified, none will be displayed
my $help;
my $maillog = "/var/log/maillog"; # Assume /var/log/maillog unless specified
my @msgids;
my $heading;  # Controls whether a heading is displayed when resutls are shown
my $recipient;
# If you use blocklists to stop spam,you can set rejectmsg to be some
# string that you specify in sendmail.mc when messages are rejected
# based on a blocklist lookup
my $rejectmsg = "reject=550"; 
my $searchstatus;
my $sender;
my $status;
my $timestamp;
my $to;
my $version;
my $current_version = "1.2a";

# The entries will be searched based on the specified sender, which can
# be specified with or without a "--from" bfore the email address.
#
# The displayed entries can be restricted to those where the entry has
# a give status, e.g. "Sent" or "Deferred" ot to those with a "to" field
# matching the one specified.
#
# If the "--version" argument appears on the command line, the current
# version of the program will be displayed.
#
# If "--help" is included on the command line, usage information for the
# script will be displayed.

&GetOptions("from=s" => \$sender,
  "noheading"        => \$heading,
  "title=s"          => \$title,
  "status=s"         => \$searchstatus,
  "to=s"             => \$to,          
  "version"          => \$version,
  "help"             => \$help);

#---------------------------
# Main Thread of Execution
#---------------------------
# process command line arguments


if (defined($version))
{
 print "find-recipients version $current_version\n";
 exit;
}

# The sender should appear on the command line either by itself or as
# "--from sender". If no arguments are provided on the command line, print 
# usage information.
unless (($#ARGV >= 0) || defined($sender)) {
 &print_help;
 exit;
}

if (defined($help))
{
 &print_help;
 exit;
}

# If "--from someone@somewhere.com" is used on the command line,
# then the maillog file to be used should be $ARGV[0], if a maillog
# file is specified on the command line. Otherwise, the default value
# for the maillog file will be used.
elsif (defined($sender) && ($#ARGV == 0)) {
   $maillog = $ARGV[0];
}

if (!defined($sender)) {
  $sender = $ARGV[0]; 
}

if ($#ARGV == 1) {
 $maillog = $ARGV[1];
}

# check sender name for basically valid form
if (! ($sender =~ /.*@.*/)) {
    print "Sender address $sender does not appear to be valid! ";
    print "An \"@\" is expected.\n";
    exit 1;
}

# test for existance of maillog
if(! -r $maillog) {
    print "Can not find mail log $maillog\n";
    exit 1;
}

# get all message ids associated with the sender in temporary array
my @temp;
# The case of the letters in the sender email address should be irrelevant
open(LOG, "<$maillog") || die "Open of $maillog failed\n";
while(<LOG>) {
    # Ignore the case of letters in the email address
    if(/from=<$sender>/i) {
        @fields = split " ";
	$fields[5] =~ s/://;
	$temp[$#temp + 1] = $fields[5];
    }
}
close(LOG);
# optimize list by sorting and removing duplicates
@temp = sort @temp;
foreach my $msg (@temp) {
    if($#msgids < 0) {
	$msgids[0] = $msg;
    } 
    elsif ($msgids[$#msgids] ne $msg) {
	$msgids[$#msgids + 1] = $msg;
    }
}

if (!defined($heading)) {
   # Heading = "true"; display a heading 
   $heading = 1;
}
else {
   #Heading = "false"; don't display a heading
   $heading = 0;
}
# Display header information
printMessageHeader($#msgids + 1, $sender, $maillog, $title, $heading);

# Lookup recipients for each message id
open(LOG, "<$maillog") || die "Open of $maillog failed\n";
foreach my $msg (@msgids) {
    while(<LOG>) {
        chomp();
        # Check for mailer equals smtp, esmtp, or local
        # The "to" field may be "to=<name@somwhere.com>", 
        # "to=<name1@somewhere.com>,<name2@somewhere.com>", 
        # "to=name", or, if the recipient address is a local address on the 
        # system and there is a .forward file for the recipient that points
        # to other local addresses on the system, then the "to" address
        # could be "to=\\name3".
        # 
        # E.g. suppose a message is addressed to "Mary" and Mary has
        # a .forward file in her home directory with the following lines:
        #
        # \mary, \cindy
        #
        # In this case Mary wants any email addressed to her to go to
        # her own address, but also to be forwarded to Cindy. The
        # maillog file will have two "to=" entries, one will be
        # "to=\\mary" and one will be "to=\\cindy". This script
        # will count the message only once, but will print two lines,
        # one will show the message going to \\cindy and the other will
        # show it going to \\mary.
        if (/$msg/ && (/mailer=smtp/ || /mailer=esmtp/ || /mailer=local/ ||
            /mailer=relay/) && (/to=/)) {
	    @fields = split " ", $_;
	    # recipient address
            $fields[6] =~ s/^to=//;
            $recipient = &tidyRecipients($fields[6]);
            $timestamp = $fields[0]." ".$fields[1]." ".$fields[2];
	    # status
            # The "stat=" information can be in field 12, 14, or 16
            # the status.
	    if ($fields[12] =~ /^stat=/) {
	        $status = $fields[12];
	    }
	    elsif ($fields[14] =~ /^stat=/) {
	        $status = $fields[14];
                # If the status is "I/O error" or "User unknown", or
                # "Service unavailable" then the second word will be in field 15
                if (($status =~ "I/O") || ($status =~ "User") ||
                   ($status =~ "Service")) {
                   $status = $status." ".$fields[15];
                }
	    }
            # "stat=Deferred" can occur in field 15
	    elsif ($fields[15] =~ /^stat=/) {
	        $status = $fields[15];
            }
            # If the message was not a local message field 16 contains
            # the status.
	    elsif ($fields[16] =~ /^stat=/) {
	        $status = $fields[16];
                # If the status is "I/O error" or "User unknown", or
                # "Service unavailable" then the second word will be in field 15
                if (($status =~ "I/O") || ($status =~ "User") ||
                   ($status =~ "Service")) {
                   $status = $status." ".$fields[17];
                }
	    }
            $status =~ s/^stat=//;
            if ($status eq "Deferred:") {
                chop($status);
            }
            if (!&filtered) {
               &printMessageInfo($timestamp, $msg, $status, $recipient);
            }
	} 
        # Handle rejected messages
        elsif (/$msg/ && /$rejectmsg/) { 
	    @fields = split " ";
            $timestamp = $fields[0]." ".$fields[1]." ".$fields[2];
	    $status = "Rejected";           
	    # recipient address
            $fields[7] =~ s/^arg1=//;
	    $recipient = &tidyRecipients($fields[7]);
            if (!&filtered) {
               &printMessageInfo($timestamp, $msg, $status, $recipient);
            }
	}
        # A message entry can have the following form where there is no
        # "stat=" field:
        #
        # Jan  3 03:23:13 myserver sendmail[27580]: k038NBTG027580: 
        # <nosuchaddress@myserver.com>... User unknown
        #
        # In these cases, set the value of status to "User unknown".
        elsif (/$msg/ && /\.\.\. User unknown/) {
	   @fields = split " ";
           $fields[6] = &tidyRecipients($fields[6]);
           $timestamp = $fields[0]." ".$fields[1]." ".$fields[2];
           $status = "User unknown";           
           if (!&filtered) {
              &printMessageInfo($timestamp, $msg, $status, $fields[6]);
           }
        }
    }
    # If the line counter variable, "$." is not reset, if the script fails 
    # because of a problem parsing a line the line counter wonot reflect the
    # the actual line in the file causing the problem, because it just keeps 
    # incrementing with each seek.
    $.=0;
    seek LOG, 0, 0;
}
close(LOG);
exit 0;

#----------------
# Sub Routines
#----------------

sub filtered

# Determine if an entry should be displayed or filtered out based on
# arguments passed on the command line, such as "to" or "status".

{
   # If "to" has been assigned a value only display those
   # entries where the recipient is the one specified,
   # otherwise display all recipients.
   if (((defined($to)) && (lc($recipient) eq lc($to))) || 
      (!defined($to))) {
      # If "searchstatus" is defined, only print those entreis
      # with a matching status, e.g. "sent" or "deferred"
      if (((defined($searchstatus)) && 
         (lc($status) eq lc($searchstatus))) ||
         (!defined($searchstatus))) {
         # display the message info, i.e. filtereed will be "false"
         return 0;
      }
      else {
         # set "filtered" to be "true", i.e. don't display the entry
         return 1;
      }
    }
    else {
        # set "filtered" to be "true", i.e. don't display the entry
        return 1;
    }
}

sub printMessageHeader
{
   my ($messages, $sender, $maillog, $title) = @_;

   if ($title ne "") {
      print "$title\n\n";
   }
   if ($heading) {
      # Display count of messages and display header if there are messages
      # If no messages we are done
      print "Found $messages messages from $sender in $maillog\n";
      if ($messages == 0) { exit 0; }

      print "\nMessage recipients\n\n";
      printf "%-15s %-14s %-13s %s\n", "Time", "Message ID", "Status", 
             "Recipient";
      print "----------------------------------------------------------------\n";
   }
}

sub printMessageInfo
{
   my ($timestamp, $msgid, $status, $recipient) = @_;
   printf "%-15s %14s %-13s %s\n", $timestamp, $msgid, $status, $recipient;
}

sub tidyRecipients
{
   my ($recipients) = @_;
   $recipients =~ s/^<//;
   $recipients =~ s/>,$//;
   # If the message was sent to more than one recipient,
   # the "to" field will look like "<email1@somewhere.com>,
   # <email2@somewhere.com>". Remove occurences of "<"
   # and ">"
   $recipients =~ s/>,</, /g;
   # Remove the "," from the end of lines where forwarding has
   # occurred to a local address, e.g. lines that will be "\\name,"
   # or where the message has been sent to a local address, such as
   # "to=amy,"
   $recipients =~ s/,//g;
   # In cases where someone is attempting to send a message to
   # a nonexistent user on the system the log may contain an
   # entry such as <nosuchuser@myserver.com>... User unknown"
   # Get rid of the ">..." at the end of the recipient's address.
   $recipients =~ s/>\.\.\.//;
   return ($recipients);
}

sub print_help
{
  print <<EOF;
find-recipients searches a maillog file for all recipients to whom
a given sender has sent email.

Usage: $0 [--noheading] [--title title] [--status status]
[--to recipient] sender [maillog]

Unless specified, maillog is assumed to be $maillog.

Optional arguments:

A count will be provided of all messages from the sender, but only those which
match the specified criteria will be displayed.

--noheading       - don't display any heading information
--title title     - an optional title for the output (use quotes for multiple
                    words)
--status status   - the status is as specified, e.g. "Sent" or "Deferred" 
--to recipient    - the specified recipient is in the "to" field.
--version         - display the current version

You can also use one-letter options for the above arguments, e.g. -n, -t, -s,
-t, or -v

EOF
}

