#!/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 # 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() { # 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() { chomp(); # Check for mailer equals smtp, esmtp, or local # The "to" field may be "to=", # "to=,", # "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: # ... 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/^,$//; # If the message was sent to more than one recipient, # the "to" field will look like ", # ". Remove occurences of "<" # and ">" $recipients =~ s/>,... User unknown" # Get rid of the ">..." at the end of the recipient's address. $recipients =~ s/>\.\.\.//; return ($recipients); } sub print_help { print <