#! /usr/bin/perl -w

# Copyright (C) 2016-2025 Arne Wichmann
#
# This little thing is distributed under the GNU General Public License,
# version 2. If you need the license, ask your distributor for it.

use strict;

# this makes sure that max 1 mail per user and hour will be sent by icinga.
# if any further mails would be sent they will be appended to
# notifications.<emailadress> and all mails of last hour will be sent in
# one mail. it needs a working mail system and at-functionality.

# in the beginning I envisioned a second mail per hour to be sent if all
# pending notifications are finished (with UP or OK), but this proved too
# much of a hassle for the gain

use Fcntl qw(:flock SEEK_END);
use POSIX;

# I use Unix-environment quite a bit. Most variables come from Icinga
# configuration state - they should be about as secure as Icinga. The
# exception is the comment field - this is as secure as the users giving
# comments. Perhaps I should sanitize this. If you think so, tell me. ;)
# chdir("/var/lib/icingamail")|| die "cannot chdir to work dir";
our($email)=$ENV{USEREMAIL};
our($lastmailfile)="lastmail.$email";
our($lastfile)="laststate.$email";
our($notificationfile)="notifications.$email";
our($delayedmailcommand)="/etc/icinga2/scripts/delayed-mail";
our($shortdelay)=5;
our($maildelay)=3600;
our(@prio)=("DOWN",'CRITICAL','WARNING','UNKNOWN', 'OK','UP');
our($lognotifications)="notifications.log";
# form: https://echelon.orga.linuxhotel.de/thruk/cgi-bin/extinfo.cgi?type=
our($url)=$ENV{USERURL} if $ENV{USERURL};
our($mailfrom)=$ENV{USERFROM} if $ENV{USERFROM};
our($service)=$ENV{SERVICEDESC} if $ENV{SERVICEDESC};
our($host)=$ENV{HOSTALIAS} if $ENV{HOSTALIAS};
# TODO - env-handling hierher

our($newnotification)='';
our($mailsubject)='';

our($verbose)=1;
sub logmsg($;$);
sub logdie($);

if ($ENV{RENOTIFY}) {
  $mailsubject="DELAYED NOTIFY";
} elsif ($service) {
  my($state)=$ENV{SERVICESTATE};

  # I decided to handle duplicate OK-messages here instead of fixing
  # icinga. They happen because I use separate objects for warnings and
  # criticals to delay warnings by (by default) 30 min
  if ($state eq "OK" and $lastfile) {
    my($servathost)="$service\@$host";
    if (open(LAST,"<$lastfile")) {
      $_=<LAST>;
      chomp;
      if ($_ eq $servathost) {
        logmsg("Ignoring duplicate $servathost");
	exit(0);
      }
    } else { # if it is not there we assume we are used first time
      $_=$!+0; # numeric
      ($_==ENOENT) or logmsg("Error opening $lastfile: ".strerror($_));
    }
    if (open(LAST,">$lastfile")) {
      print LAST $servathost;
      close(LAST) || logmsg "Close failed: $!";
  } }
 
  $newnotification="***** Icinga  *****\n".
    "\nNotification Type: ".$ENV{NOTIFICATIONTYPE}.
    "\n\nService: $service\nHost: $host\nAddress: ".$ENV{HOSTADDRESS}.
    "\nState: $state\n\nDate/Time: ".$ENV{LONGDATETIME}.
    "\n\nAdditional Info: ".$ENV{SERVICEOUTPUT}.
    "\n\nComment: [".$ENV{NOTIFICATIONAUTHORNAME}.
    "] ".$ENV{NOTIFICATIONCOMMENT}."\n";
  $newnotification.="URL: ".$url."2&host=".$ENV{HOSTALIAS}."&service=$service\n"
    if $url;
  $mailsubject=$ENV{NOTIFICATIONTYPE}.": ".$ENV{HOSTDISPLAYNAME}." - ".
    $ENV{SERVICEDISPLAYNAME}." is $state";
} elsif ($ENV{HOSTSTATE}) {
  $newnotification="***** Icinga  *****\n".
    "\nNotification Type: ".$ENV{NOTIFICATIONTYPE}."\n\nHost: ".$ENV{HOSTALIAS}.
    "\nAddress: ".$ENV{HOSTADDRESS}."\nState: ".$ENV{HOSTSTATE}."\n".
    "\nDate/Time: ".$ENV{LONGDATETIME}."\n".
    "\nAdditional Info: ".$ENV{HOSTOUTPUT}."\n".
    "\nComment: [".$ENV{NOTIFICATIONAUTHORNAME}."] ".$ENV{NOTIFICATIONCOMMENT};
  $newnotification.="URL: ".$url."1&host=".$ENV{HOSTALIAS}."\n" if $url;
  $mailsubject=$ENV{NOTIFICATIONTYPE}.": ".$ENV{HOSTDISPLAYNAME}." is ".
    $ENV{HOSTSTATE};
}

if (!$ENV{RENOTIFY} and $lognotifications) {
  open(LOG,">>$lognotifications")
    || logmsg "cannot write logfile $lognotifications: $!";
  flock(LOG,LOCK_EX) || logdie "cannot lock logfile $lognotifications: $!";
  print LOG $newnotification;
  flock(LOG,LOCK_UN) || logdie "cannot unlock $lognotifications: $!";
  close(LOG) || logmsg "Close failed: $!";
}

my($lastmail);
unless (-f $lastmailfile) { 
  open(_,"+>$lastmailfile")|| logdie "cannot write $lastmailfile: $!";
  flock(_,LOCK_EX) || logdie "cannot lock $lastmailfile: $!";
  $lastmail=0;
} else {
  open(_,"+<$lastmailfile") || logdie "cannot write $lastmailfile: $!";
  flock(_,LOCK_EX) || logdie "cannot lock $lastmailfile: $!";
  $lastmail=<_>;
  $lastmail>0 or logmsg "$lastmailfile contained junk\n";
}

# - ging in der letzten stunde eine mail raus?
my($time)=time();
if ($time<$maildelay) { logdie("Unix-timestamp < $maildelay: $time"); }
logmsg("Mailing? $time $lastmail",1);
if ($time-$lastmail>$maildelay) { # send a mail
  logmsg(" - yes",1);
  truncate(_,0) || logdie "cannot truncate $lastmailfile: $!";
  seek(_,0,0) || logdie "cannot seek to position 0 in $lastmailfile: $!";
  print _ ($time+$shortdelay);
  flock(_,LOCK_UN) || logdie "cannot unlock $lastmailfile: $!";
  close(_);

  #     - warte 5s ob noch jemand was sagen will
  # TODO: Das scheint nicht wirklich zu funktionieren, alle folgenden
  # kommen 5s spaeter - serialisiert icinga? das rauszufinden ist viel
  # Arbeit fuer wenig Erfolg
  # sleep $shortdelay;

  #     - schicke mail mit allem raus was wartet
  # create if nonexistent, else just open RW
  open(_,(-f $notificationfile?"+<":"+>").$notificationfile) ||
    logdie "cannot write $notificationfile: $!";
  flock(_,LOCK_EX) || logdie "cannot writelock $notificationfile: $!";
  my(@input)=<_>;
  truncate(_,0) || logdie "cannot truncate $notificationfile: $!";
  flock(_,LOCK_UN) || logdie "cannot unlock $notificationfile: $!";
  close(_) || logmsg "Close failed: $!";
  push(@input,$newnotification);

  # schoenes subject
  my($p,$count);
  for my $pp (@prio) {
    # $count=grep { /Notification Type: $pp/ } @input;
    $count=grep { /State: $pp/ } @input;
    $p=$pp;	# $pp ist leider local 
    last if $count;
  }
  # if (1<(grep { /Notification Type: / } @input)) {
  if (1<(grep { /State: / } @input)) {
    if ($count>1) { $mailsubject="Summary: $p in multiple cases" }
    else { $mailsubject="Summary: $p and less urgent items" }
  } elsif ($mailsubject eq 'DELAYED NOTIFY') { $mailsubject="Delayed: $p" }

  my(@mc)=("mail","-s",$mailsubject);
  push(@mc,"-r",$mailfrom) if $mailfrom;
  push(@mc,$email);
  open(_,"|-",@mc) ||logdie "pipe failed: $!";
  print _ @input;
  close(_)
    or logmsg "mail failed or similar problem (exit $?)".($!?": $!\n":"\n");
} else { # append to $notificationfile
  logmsg(" - no",1);
  flock(_,LOCK_UN) || logdie "cannot unlock $lastmailfile: $!";
  close(_) || logmsg "Close failed: $!";

#     - wenn noch nichts gescheduled ist: schedule eine vermailung
  if (-z $notificationfile and not $ENV{RENOTIFY}) {
    my($attime)="now + ".(int(($lastmail+$maildelay-$time)/60)+2)." minutes";
    logmsg("delay mailing to: $attime",1);
    open(_,"|-","at",$attime) or logmsg "pipe to at filed: $!";
    print _ "RENOTIFY=1 $delayedmailcommand";
    close(_)
      or logmsg "at failed or similar problem (exit $?)".($!?": $!\n":"\n");
  }

#     - haeng was auch immer du zu sagen hast an die liste
  open(_,">>$notificationfile") || logdie "cannot write $notificationfile: $!";
  flock(_,LOCK_EX) || logdie "cannot writelock $notificationfile: $!";
  seek(_,0,SEEK_END) or logdie "cannot seek $notificationfile: $!\n";
  print _ $newnotification;
  flock(_,LOCK_UN) || logdie "cannot unlock $notificationfile: $!";
  close(_) || logmsg "Close failed: $!";

}

exit 0;

sub logmsg($;$) {
  my $what=shift;
  my $prio=$#_>=0?shift:0;
  return unless $verbose>=$prio;
  open(LOG,">>log") || die "Cannot write logfile log: $!";
  print LOG localtime(time)." $what\n";
  close(LOG);
}

sub logdie($) {
  my $what=shift;
  logmsg("DYING: $what",-1);
  die $what;
}
