package Net::Peep::Notifier;

require 5.005;
use strict;
use Carp;
use Sys::Hostname;
use Data::Dumper;
use Net::Peep::Log;
use Net::Peep::Mail;

require Exporter;

use vars qw{ @ISA %EXPORT_TAGS @EXPORT_OK @EXPORT $VERSION $LOGGER 
		 $NOTIFICATIONS $NOTIFICATION_INTERVAL 
		     %NOTIFICATION_LEVEL %NOTIFICATION_RECIPIENTS
			 %NOTIFICATION_HOSTS
			     @SMTP_RELAYS $HOSTNAME $USER $FROM };

@ISA = qw(Exporter);
%EXPORT_TAGS = ( );
@EXPORT_OK = ( );
@EXPORT = qw( );
$VERSION = do { my @r = (q$Revision: 1.1 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r };

$LOGGER = Net::Peep::Log->new();

$NOTIFICATIONS = {}; # used for caching failsafe information
$NOTIFICATION_INTERVAL = 1800; # seconds
%NOTIFICATION_LEVEL = ( ); # see below for more information
%NOTIFICATION_RECIPIENTS = ( );
%NOTIFICATION_HOSTS = ( );
@SMTP_RELAYS = ( 'localhost' );

$HOSTNAME = hostname() ? hostname() : 'localhost';
$USER = $ENV{'USER'} ? $ENV{'USER'} : 'peep';

$FROM = "Peep Notification <$USER\@$HOSTNAME>";

sub new {

    my $self = shift;
    my $class = ref($self) || $self;
    my $this = {};
    bless $this, $class;

} # end sub new

sub _timeToSend {

    # answers the question: based on the time that the last
    # notification was sent, is it time to send another notification

    my $self = shift;
    my $client = shift;

    my $last_send_time;
    if ($self->_sendTimeExists($client)) {
	$last_send_time = $self->_getSendTime($client);
    } else {
	return 1; # if there is no send time, it's time!
    }

    my $time = time();

    if ($time - $last_send_time > $NOTIFICATION_INTERVAL) {
	return 1;
    } else {
	return 0;
    }

} # end sub _timeToSend

sub _notificationExists {

    # returns 1 if a notification has been sent for a client of the
    # type stored in the generator object, 0 otherwise

    my $self = shift;
    my $client = shift || confess "client not found";

    if (exists $NOTIFICATIONS->{$client}) {
	return 1;
    } else {
	return 0;
    }

} # end sub _notificationExists

sub _getNotificationRecipients {

    my $self = shift;
    my $client = shift || confess "client not found";

    unless (exists $NOTIFICATION_RECIPIENTS{$client}) {
	return wantarray ? () : [];
    }

    return wantarray ? @{$NOTIFICATION_RECIPIENTS{$client}} : $NOTIFICATION_RECIPIENTS{$client};

} # end sub _getNotificationRecipients

sub _getNotificationHosts {

    my $self = shift;
    my $client = shift || confess "client not found";

    unless (exists $NOTIFICATION_HOSTS{$client}) {
	return wantarray ? () : [];
    }

    return wantarray ? @{$NOTIFICATION_HOSTS{$client}} : $NOTIFICATION_HOSTS{$client};

} # end sub _getNotificationHosts

sub notify {

    my $self = shift;
    my $notification = shift || confess "notification not found";
    my $force = shift;

    my $client = $notification->client() || confess "client not found";

    if (exists $NOTIFICATION_HOSTS{$client} &&
	exists $NOTIFICATION_RECIPIENTS{$client} &&
	exists $NOTIFICATION_LEVEL{$client}) {
	unless (scalar($self->_getNotificationRecipients($client))) {
	    $LOGGER->debug(1,"Cannot notify recipients for client [$client]:  ".
			   "No recipients were specified.");
	    return 0;
	}

	my @hosts = $self->_getNotificationHosts($client);

	unless (scalar(@hosts)) {
	    $LOGGER->debug(1,"Cannot notify recipients for client [$client]:  ".
			   "No acceptable notification hosts [@hosts] were specified.");
	    return 0;
	}

	my $ok = 0;
	
	for my $host (@hosts) {
	    $LOGGER->debug(8,"Checking host [$host] against hostname [$HOSTNAME] ...\n");
	    $ok++ if $host eq $HOSTNAME;
	    $ok++ if $host =~ /^(all|localhost|127\.0\.0\.1)$/;
	    $ok = 0, last if $host eq 'none';
	}
	
	my $status = $notification->status();
	if ($NOTIFICATION_LEVEL{$client} eq 'crit' && $status ne 'crit') {
	    $LOGGER->debug(1,"Notification ignored:  Notification level is [$status].  ".
			   "Notifications are only sent at [crit].");
	    return 1;
	} elsif ($NOTIFICATION_LEVEL{$client} eq 'warn' && $status !~ /^(warn|crit)$/) {
	    $LOGGER->debug(1,"Notification ignored:  Notification level is [$status].  ".
			   "Notifications are only sent at [crit] or [warn].");
	    return 1;
	} else {
	    # do nothing
	}
	
	my $return;
	
	if ($ok) {
	    
	    eval {
		
		confess "Cannot notify recipients:  No client was specified."
		    unless $client;
		confess "Cannot notify recipients:  No mail relays were specified."
		    unless scalar(@SMTP_RELAYS);
		
		$self->store($notification);
		
		if ($self->_notificationExists($client)) {
		    if ($self->_timeToSend($client) || $force) {
			$return = $self->_notify($client);
		    } else {
			$return = 1;
		    }
		} else {
		    $return = $self->_notify($client);
		}
		
	    };

	    if ($@ || ! $return) {
		$LOGGER->log(ref($self),":  Error generating notification:  $@");
	    }
	    
	    return $return;
	    
	} else {
	    
	    $LOGGER->debug(1,"Notification ignored:  [$HOSTNAME] was not among the list of acceptable notification hosts.");
	    return 1;
	    
	}

    } else {

	$LOGGER->debug(1,"Notification ignored:  No notification information is available for client [$client].\n".
		         "\t(Possibly because no notification block was defined for the client in the Peep \n".
		         "\tconfiguration file.)");
	return 1;

    }
	
} # end sub notify

sub _notify {

    # notify the recipients of the notification
    my $self = shift;
    my $client = shift || confess "client not found";
    
    if (scalar($self->_getNotifications($client))) {

	my $mailer = Net::Peep::Mail->new();
	
	$mailer->smtp_server(@SMTP_RELAYS);
	$mailer->from("Peep Notification <$USER\@$HOSTNAME>");
	$mailer->to($self->_getNotificationRecipients($client));
	$mailer->subject("[Peep Notification] $client on $HOSTNAME");
	$mailer->body($self->_getBody($client));
	if ($mailer->send()) {
	    $self->_setSendTime($client);
	    $self->_garbageCollect($client);
	    return 1;
	} else {
	    return 0;
	}

    }

} # end sub _notify

sub _getNotifications {

    # Return only those notifications exceeding the notification
    # threshhold defined in the Peep configuration file for the
    # specified client

    my $self = shift;
    my $client = shift || confess "client not found";

    return () unless $self->_notificationExists($client);

    my $level = $NOTIFICATION_LEVEL{$client} 
        || confess "Notification level for client [$client] not found.";

    if ($level eq 'crit') {
	return grep $_->status() eq 'crit', @{$NOTIFICATIONS->{$client}->{'NOTIFICATIONS'}};
    } elsif ($level eq 'warn') {
	return grep $_->status() =~ /warn|crit/, @{$NOTIFICATIONS->{$client}->{'NOTIFICATIONS'}};
    } else {
	return @{$NOTIFICATIONS->{$client}->{'NOTIFICATIONS'}};
    }

} # end sub _getNotifications

sub _getBody {

    my $self = shift;
    my $client = shift;

    return undef unless $self->_notificationExists($client);

    my @notifications = $self->_getNotifications($client);

    my $n_notifications = scalar(@notifications);

    my $time = scalar(localtime(time()));

    my $body = <<"eop";
User:           $USER
Host:           $HOSTNAME
Client:         $client
Time:           $time
Notifications:  $n_notifications

eop

    ;

    my $i = 1;

    # reverse the order of notifications to get the most recent first
    for my $notification (reverse @notifications) {

	my $status = ucfirst($notification->status());
	my $message = $notification->message();
	my $time = scalar(localtime($notification->datetime()));

	$body .= <<"eop";
\[Notification $i:  $status at $time\]
$message

eop

    ;

	$i++;

    }

    return $body;

} # end sub _getBody

sub store {

    # store failsafe information for future reference
    my $self = shift;
    my $notification = shift; # a Net::Peep::Notification object

    my $client = $notification->client() || confess "client not found";
    push @{$NOTIFICATIONS->{$client}->{'NOTIFICATIONS'}}, $notification;

    return 1;

} # end sub store

sub _sendTimeExists {

    my $self = shift;
    my $client = shift || confess "client not found";
    
    if (exists $NOTIFICATIONS->{$client} &&
	exists $NOTIFICATIONS->{$client}->{'SEND_TIME'}) {
	return 1;
    } else {
	return 0;
    }

} # end sub _sendTimeExists

sub _setSendTime {

    my $self = shift;
    my $client = shift || confess "client not found";
    $NOTIFICATIONS->{$client}->{'SEND_TIME'} = time();

} # end sub _setSendTime

sub _getSendTime {

    my $self = shift;
    my $client = shift || confess "client not found";

    if (exists $NOTIFICATIONS->{$client}) {
	if (exists $NOTIFICATIONS->{$client}->{'SEND_TIME'}) {
	    return $NOTIFICATIONS->{$client}->{'SEND_TIME'};
	} else {
	    confess "Cannot find SEND_TIME key for client [$client]";
	}
    } else {
	return undef;
    }

} # end sub _getSendTime

sub _garbageCollect {

    # clear out old notification information

    my $self = shift;

    my $client = shift || confess "client not found";
    delete $NOTIFICATIONS->{$client}->{NOTIFICATIONS}
	if exists $NOTIFICATIONS->{$client} &&
	    exists $NOTIFICATIONS->{$client}->{NOTIFICATIONS};
    $NOTIFICATIONS->{$client}->{NOTIFICATIONS} = [];

    return 1;

} # end sub _garbageCollect

sub force {

    # clear out any unsent notifications whether or not they're ready
    # to be sent
    my $self = shift;

    my $force = 1;

    my $n;

    for my $client (keys %$NOTIFICATIONS) {
	if (exists $NOTIFICATIONS->{$client}->{'NOTIFICATIONS'} && 
	    exists $NOTIFICATIONS->{$client}->{'SEND_TIME'}) {
	    my @notifications = @{$NOTIFICATIONS->{$client}->{'NOTIFICATIONS'}};
	    $n = @notifications;
	    $self->notify($notifications[0],$force) if @notifications;
	}
    }

    return $n;

} # end sub force

sub flush {

    # clear out any unsent notifications that are ready to be sent
    my $notifier = Net::Peep::Notifier->new();

    for my $client (keys %$NOTIFICATIONS) {
	if (exists $NOTIFICATIONS->{$client}->{'NOTIFICATIONS'} && 
	    exists $NOTIFICATIONS->{$client}->{'SEND_TIME'}) {
	    my @notifications = @{$NOTIFICATIONS->{$client}->{'NOTIFICATIONS'}};
	    $notifier->notify($client);
	}
    }

} # end sub flush

1;

__END__

=head1 NAME

Net::Peep::Notifier - Utility object for Peep client event or state
e-mail notifications

=head1 SYNOPSIS

  use Net::Peep::Notifier;
  $notifier = new Net::Peep::Notifier;
  $notifier->client('logparser'); # identify who is generating notifications
  $notifier->from($from); # identify who is sending the e-mail
  $notifier->recipients(@recipients); # identify some failsafe recipients
  $notifier->relays(@relays); # identify some SMTP relays
  $notifier->notify(); # sends an e-mail if it is time, otherwise caches
                       # the information for sending later

  $john = new Net::Peep::Notifier;
  $john->flush(); # flush (send) outstanding messages as necessary

=head1 DESCRIPTION

    Utility object for notifications; for example, when logparser
    matches a root login in /var/log/messages.

    When a notification is created, the object will first check
    whether a notification has been sent for that client (based on the
    value of $NOTIFICATION_INTERVAL).

    If so, the notification information is merely noted and no
    notification is sent out.  If not, the notification information,
    as well as any previously unsent notification information, is sent
    to the recipients specified by return value of the recipients()
    method.

    The object also supports a flush method, which can be used to
    periodically flush (send out) any failsafe messages which are due
    to be sent.

    The basic idea is to populate the object attributes with pertinant
    information (e.g., a a list of recipients, a set of SMTP relays, a
    'From:' address, etc.), then call the notify method.  If a
    notification should be sent, it will be.  If not, the information
    will be cached for later use (e.g., a call to the flush() method.)

    To flush out unsent messages, simply instantiate a new
    Net::Peep::Notifier object and call the flush method.

=head2 EXPORT

None by default.

=head1 ATTRIBUTES

    $LOGGER - A Net::Peep::Log object

    $NOTIFICATIONS - Used for caching failsafe information

    $NOTIFICATION_INTERVAL - Number of seconds between each
    notification

    $NOTIFICATION_LEVEL - A hash whose keys are client names (e.g.,
    'logparser') and whose values are the level ('info', 'warn', or
    'crit') at which to issue notifications for that client.  Defaults
    to 'info'.  

    %NOTIFICATION_RECIPIENTS - A hash whose keys are client names
    (e.g., 'logparser') and whose values are arrays of e-mail
    addresses to which notifications will be sent for that client.
    Will be set to whatever is specified in the Peep configuration
    file.

    @SMTP_RELAYS - An array of SMTP servers from which e-mail
    notifications may be relayed.  Defaults to localhost.  Will we set
    to whatever is specified in the Peep configuration file.

=head1 METHODS

    new() - The constructor

    from() - A get/set method to store a 'From:' address for e-mail
    notification.  Must be set prior to calling the notify() method.

    interval() - A get/set method to store the notification interval
    (in seconds).  Defaults to $NOTIFICATION_INTERVAL.  

    recipients() - A get/set method to store an array of e-mail
    recipients.  Must be set prior to calling the notify() method.

    relays() - A get/set method to store an array of SMTP relays.
    Must be set prior to calling the notify() method.

    notify() - If applicable, notify recipients of an event or state
    via e-mail based on attributes such as generator() etc. (see
    above).  Otherwise, store the information for later reference.

    flush() - Clear out any unsent failsafe messages which have not
    been sent in at least $NOTIFICATION_INTERVAL seconds

    force() - Forcibly clear out any unsent failsafe messages whether
    or not they have been sent in at least $NOTIFICATION_INTERVAL
    seconds

=head1 AUTHOR

Collin Starkweather <collin.starkweather@colorado.edu> Copyright (C) 2001

=head1 SEE ALSO

perl(1), Net::Peep, Net::Peep::Mail

=cut
