# -*- perl -*-

# Copyright (c) 2002 by Jeff Weisberg
# Author: Jeff Weisberg <argus @ tcp4me.com>
# Date: 2002-Apr-12 20:06 (EDT)
# Function: Tell someone what happened
#
# $Id: Notify.pm,v 1.57 2005/03/20 20:21:51 jaw Exp $

package Notify;
use NotMe;
use Argus::Encode;
use POSIX ('_exit', 'strftime', 'tzset');

use strict;

my %byid     = (); 
my %lastsent = ();	# by dest
my %queue    = ();	# by dest => {not, tag}
my %unacked  = ();	# by id
my %dstparam = ();	# probably a bad idea...

# only send so often, otherwise queue
# can be overridden in user-defined notification methods via 'qtime: 1234'
my $QUEUETIME = 120;

# we keep re-sending, but if it comes up, give up after this long
my $UNACK_TO  = 1800;

# if older than this, and no longer active, throw them away
my $OLD_AGE = 3600 * 24 * 5;

# delay escalation, if no longer down for this long
my $DELAY_ESCALATE = 600;

# fields which get saved in save file (and then re-loaded)
my @SAVE_FIELDS = qw(created msg sentcnt state objstate objovstate
		     priority autoack ackonup period ackedby ackedat
		     escalated lastsent audit detail severity reason);

sub new {
    my $obj   = shift;
    my %param = @_;
    my( $me, $msg, $fmt, $aa, $dst, @t );

    # do not notify if notifys are not wanted
    return unless $obj->{notify}{sendnotify};

    if( $obj->{status} eq 'down' ){
	$msg = $obj->{notify}{messagedn};
	$fmt = $obj->{notify}{message_fmtdn};
	$aa  = $obj->{notify}{autoack};
    }else{
	$msg = $obj->{notify}{messageup};
	$fmt = $obj->{notify}{message_fmtup};
	$aa  = 1;	# up messages never need to be acked
    }

    $fmt ||= $obj->{notify}{message_fmt};
    
    if( $param{audit} ){
	# notify is an audit channel
	$msg = 'audit: %A';
	$aa = 1;
    }
    
    # do not notify if user specified an empty msg
    return unless $msg;
    
    $me = {
	obj        => $obj,
	created    => $^T,
	idno       => newid(),
	sentcnt    => 0,
	escalated  => 0,
	state      => 'active',		# active, supressed, acked
	objstate   => $obj->{status},
	objovstate => $obj->{ovstatus},
	priority   => $obj->{notify}{priority},
	autoack    => $aa,
	period     => $obj->{notify}{renotify},
	ackonup    => $obj->{notify}{ackonup},
	status     => {},
	sendto     => [],
	log        => [],
	audit      => $param{audit},	# bool
	detail     => $param{detail},
	severity   => $obj->{currseverity},
	reason     => $obj->{srvc}{reason},
    };
    bless $me; # gesundheit

    $me->{msg} = $me->expand($fmt, $msg, $obj);
    
    if( $me->can('notify_policy') ){
	return unless $me->notify_policy();
    }
    
    return unless $me->init($obj);
    
    $me->loggit( 'system', 'created' );
    $me->notify();
}

sub init {
    my $me  = shift;
    my $obj = shift;
    my( $notify );

    # use notifyup, notifydn, or notify?
    if( $me->{audit} ){
	$notify = $obj->{notify}{notifyaudit};
    }else{
	$notify = $obj->{notify}{$me->{objstate} eq 'down' ? 'notifydn' : 'notifyup'};
	$notify = $obj->{notify}{notify} unless defined $notify;
	$notify .= ' ' . $obj->{notify}{notifyalso} if $obj->{notify}{notifyalso};
    }

    {
	my $n;
	foreach my $dst ( split /\s+/, $notify ){
	    # QQQ - is this really a good idea?
	    $dst = $dstparam{$dst}{redirect} if $dstparam{$dst} && $dstparam{$dst}{redirect};
	    $n .=  ($n ? ' ' : '') . $dst;
	}
	$notify = $n;
    }
    
    # set status of each rcpt
    $me->{sendto} = [ { n => 0, start => 0, who => [] } ];
    foreach my $dst ( split /\s+/, $notify ){
	next if $dst eq 'none';
	next unless $dst;
	$me->{status}{$dst} ||= 'created';
	push @{$me->{sendto}[0]{who}}, $dst;
    }

    # He that sendeth a message by the hand of a fool cutteth off the feet, and drinketh damage.
    #   -- proverbs 26:6
    # no recipients? abort
    unless( @{$me->{sendto}[0]{who}} ){
	# QQQ this is too noisy, maybe re-enable later
	# $me->loggit( 'system', 'no recipient' );
    	return undef;
    }

    # pre-build escalation table
    # A wicked messenger falleth into mischief: but a faithful ambassador is health.
    #   -- proverbs 13:17
    {
	my $n = 1;
	if( $obj->{notify}{escalate} ){
	    foreach my $esc ( split /\;\s+/, $obj->{notify}{escalate} ){
		# each is of form N dest dest dest ...
		# previously, N was number of pages to escalate
		# currently, it is elapsed-time in minutes
		# QQQ - is there a preference?
		my @a = split /\s+/, $esc;
		next unless @a;
		my $nt = shift @a;
		$nt *= 60;  # minutes -> seconds
		next unless $nt;
		
		$me->{sendto}[$n] = { n => $n, start => $nt, who => [@a] };
		$n ++;
	    }
	}
    }

    push @{$obj->{notify}{list}}, $me
	unless $me->{audit};		# do not list audit msgs in object notify list
    
    $byid{$me->{idno}} = $me;
    
    if( $me->{state} eq 'active' ){
	$unacked{$me->{idno}} = $me;
    }

    # for easy access
    $me->{acl_ntfyack}    = $obj->{acl_ntfyack};
    $me->{acl_ntfydetail} = $obj->{acl_ntfydetail};
    $me->{timezone}       = $obj->{notify}{timezone};
    $me->{mailfrom}       = $obj->{notify}{mail_from};
    
    1;
}

# flush out queued pending messages
END {
    flushqueue();
    foreach my $p (values %byid){
	$p->save();
    }
}

# return a unique id number
sub newid {
    my( $id, $n );

    if( $Conf::Config{_test_mode} ){
	$id = 1;
	while( exists $byid{$id} ){ $id ++ };
    }else{
	if( open(F, "+< $::datadir/notno") ){
	    chop( $id = <F> );
	    $id ++;
	    while( exists $byid{$id} ){ $id = int(rand( 65535 << (++$n/1000) )); }
	    
	    seek F, 0, 0;
	    print F "$id\n";
	    close F;
	}else{
	    ::loggit( "cannot open $::datadir/notno: $!", 1 );
	    $id = $$;
	    while( exists $byid{$id} ){ $id = int(rand( 65535 << (++$n/1000) )); }
	}
    }

    $id;
}

sub save {
    my $me = shift;
    my $f = "$::datadir/notify/$me->{idno}";

    return if $Conf::Config{_test_mode};
    open( N, "> $f" ) || return $me->loggit( 'system', "cannot open save file: $!", 1 );

    print N "idno: $me->{idno}\n";
    print N "object: ", encode( $me->{obj}->unique() ), "\n";
    foreach my $k (@SAVE_FIELDS){
	my $v = $me->{$k};
	next unless defined $v;

	$v = encode( $v );
	print N "$k: $v\n";
    }

    foreach my $dst (keys %{$me->{status}}){
	print N "status: ", encode($dst), " ", encode($me->{status}{$dst}), "\n";
    }
    
    foreach my $s (@{$me->{sendto}}){
	print N "sendto: $s->{start}";
	foreach my $dst (@{$s->{who}}){
	    print N " ", encode($dst);
	}
	print N "\n";
    }

    foreach my $l (@{$me->{log}}){
	print N "log: $l->{time} ", (encode($l->{who}) || '_') , " ", encode($l->{msg}), "\n";
    }
    
    close N;
}

sub load {
    my $obj = shift;
    my $id  = shift;
    my $f = "$::datadir/notify/$id";
    my $me = { obj => $obj };

    open( N, $f ) || return ::loggit( "cannot open save file '$f': $!", 1 );
    while( <N> ){
	chop;

	next if( /^sendto/ );
	if( /^status:\s+([^\s]+) ([^\s]+)/ ){
	    my($dst, $st) = ($1, $2);
	    $me->{status}{ decode($dst) } = decode($st);
	}
	elsif( /^log:\s+([^\s]+) ([^\s]+) ([^\s]+)/ ){
	    my($t, $w, $m) = ($1, $2, $3);
	    push @{$me->{log}}, { time => $t,
				  who  => (($w eq '_') ? '' : decode($w)),
				  msg  => decode($m)
				  };
	}
	else{
	    my( $k, $v ) = split /:\s*/, $_, 2;
	    $me->{$k} = decode($v);
	}
    }
    close N;
    
    # too old and not still active? toss
    if( $me->{state} ne 'active' && $^T - $me->{created} > $OLD_AGE ){
	unlink $f;
	return;
    }
    
    bless $me;
    $me->init($obj);
}

sub notify {
    my $me  = shift;
    my( $l );
    
    foreach my $dst ( @{$me->{sendto}[0]{who}} ){
	$me->sendorqueue($dst);
    }
    
    $me->{sentcnt} ++;
    $me->{lastsent} = $^T;
}

# I say again! repeated the Pigeon
#   -- Alice in Wonderland
sub renotify {
    my $me = shift;
    my( $l );
    
    # auto-ack if aa and it has already been sent
    return $me->ack() if $me->{autoack};

    for ($l=0; $l<=$me->{escalated}; $l++){
	if( $^T - $me->{created} - $me->{sendto}[$l]{start} < $me->{period} ){
	    # too early to resend the escalation
	    last;
	}
	
        foreach my $dst ( @{$me->{sendto}[$l]{who}} ){
            $me->sendorqueue($dst, 'RESENT');
        }
    }
    
    $me->{sentcnt} ++;
    $me->{lastsent} = $^T;
}

sub escalate {
    my $me = shift;
    my( $n );

    # postpone if no longer down for X minutes
    if( $me->{obj}->{status} ne 'down'
	&& $^T - $me->{obj}->{transtime} > $DELAY_ESCALATE ){
	$me->loggit('system', 'delaying escalation');
	return;
    }
    
    $me->{escalated} ++;
    return if $me->{escalated} >= @{$me->{sendto}};
    
    foreach my $dst ( @{$me->{sendto}[$me->{escalated}]{who}} ){
	$me->sendorqueue($dst );  # 'ESCALATED' will be tagged automatically
    }
}

sub sendorqueue {
    my $me  = shift;
    my $dst = shift;
    my $tag = shift;
    my( $qt );

    $qt = NotMe::qtime($dst);
    $qt = $Conf::Config{qtime} unless defined $qt;
    $qt = $QUEUETIME unless defined $qt;
    
    if( $lastsent{$dst} && ($lastsent{$dst} + $qt > $^T) ){
	$me->queue($dst, $tag);
    }else{
	$me->transmit($dst, $tag);
    }
}

sub queue {
    my $me  = shift;
    my $dst = shift;
    my $tag = shift;
    
    push @{$queue{$dst}}, {not => $me, tag => $tag};
    
    $me->{status}{$dst} = 'queued';
    $me->loggit( $dst, 'queued' );
    $me->save();
}

# And Hezekiah received the letter from the hand of the messengers, and read it
#   -- isaiah 37:14
sub ack {
    my $me = shift;
    my $who = shift;
    my( $dst, $aap );
    
    return undef if $me->{state} ne 'active';
    unless($who){
	$who = 'auto-ack';
	$aap = 1;
	# we do it this way, and not by looking at me->autoack
	# as in some cases autoack = 1, but the ack is from an
	# override or timeout in which case we want to de-queue
    }
    
    $me->{state}   = 'acked';
    $me->{ackedat} = $^T;
    $me->{ackedby} = $who;
    $me->loggit( $who, 'acked' );

    # remove from queue unless it is an auto-ack
    unless( $aap ){
	foreach $dst (keys %queue){
	    @{$queue{$dst}} = grep { $_->{not}{idno} != $me->{idno} } @{$queue{$dst}};
	    delete $queue{$dst} unless @{$queue{$dst}};
	}
	foreach $dst (keys %{$me->{status}}){
	    $me->{status}{$dst} = "acked by $who";
	}
    }
    
    # remove from unacked
    delete $unacked{ $me->{idno} };
    
    # stats, ...

    
    $me->save();
    
    1;
}



sub supress {
    my $me = shift;
    
    # RSN - supress...
}

sub loggit {
    my $me = shift;
    my $who = shift;
    my $msg = shift;
    my $loudly = shift;
    
    push @{$me->{log}}, { time => $^T, who => $who, msg => $msg };
    $msg  = "<$me->{priority}> $msg" if $me->{priority};
    $msg .= " - $who" if $who;
    $me->{obj}->loggit( msg => $msg,
			tag => "NOTIFY-$me->{idno}",
			lpf => $loudly );
}

sub flushqueue {

    foreach my $dst (keys %queue){
	my @p = map { $_->{not} } @{$queue{$dst}};
	my $p = shift @p;
	next unless $p;
	$p->transmit($dst, undef, @p);
	delete $queue{$dst};
    }
}

# This side is Hiems, Winter, this Ver, the Spring;
# the one maintained by the owl, the other by the
# cuckoo. Ver, begin.
#   -- Shakespeare, Loves Labours Lost

# run by cron every minute
sub maintenance {

    # what should I do with outstanding notifs?
    foreach my $p (values %unacked){
	
	# auto-ack if in override
	if( $p->{obj}->{ovstatus} eq 'override' ){
	    $p->ack('override');
	    next;
	}
	# auto-ack if no longer down and TO
	if( $p->{obj}->{status} ne 'down' && $^T - $p->{obj}->{transtime} > $UNACK_TO ){
	    $p->ack('timeout');
	    next;
	}
	# auto-ack if not down and ackonup is set
	if( $p->{objstate} eq 'down' && $p->{obj}->{status} ne 'down' && $p->{ackonup} ){
	    $p->ack('up');
	}
	
	if( $p->{period} && ($p->{lastsent} + $p->{period} <= $^T) ){
	    # time to resend
	    $p->renotify();
	}

	if( defined($p->{sendto}[ $p->{escalated}+1 ]) &&
	    $^T - $p->{created} >= $p->{sendto}[ $p->{escalated}+1 ]{start} ){
	    # time to escalate
	    $p->escalate();
	}
    }

    # run queue
    foreach my $dst (keys %queue){
	# print STDERR "sendq? $dst\n";
	# time to send?
	my $qt = NotMe::qtime($dst);
	$qt = $Conf::Config{qtime} unless defined $qt;
	$qt = $QUEUETIME unless defined $qt;
	
	if( $lastsent{$dst} + $qt <= $^T ){
	    # print STDERR "sending q $dst\n";
	    my $p = shift @{$queue{$dst}};
	    my @p = map { $_->{not} } @{$queue{$dst}};
	    # send all queued
	    $p->{not}->transmit($dst, $p->{tag}, @p) if $p;
	    delete $queue{$dst};
	}
    }

}

# And may she speed your footsteps in all good,
# Again began the courteous janitor;
# Come forward then unto these stairs of ours.
#   -- Dante, Divine Comedy

# perform occasional cleanup, etc
sub janitor {

    my( %n_byid, %n_unacked );
    
    foreach my $n (values %byid){
	# if no longer active, and old, => toss
	if( $n->{state} ne 'active' && $^T - $n->{created} > $OLD_AGE ){
	    my $f = "$::datadir/notify/$n->{idno}";
	    unlink $f;
	    $n->{obj}{notify}{list} = [ grep { $_ != $n }
					@{$n->{obj}{notify}{list}} ];
	    $n->{log} = $n->{sendto} = $n->{status} = $n->{obj} = undef
	}else{
	    $n_byid{ $n->{idno} } = $n;
	}
    }
    %byid = (); %byid = %n_byid;

    foreach my $n (values %unacked){
	$n_unacked{ $n->{idno} } = $n;
    }
    %unacked = (); %unacked = %n_unacked;

    # delete old orphaned files
    my $dir = "$::datadir/notify";
    opendir(ND, $dir);
    foreach my $f (readdir(ND)){
	next if $f =~ /^\./;
	next if $byid{$f};
	next if $^T - (stat("$dir/$f"))[9] < $OLD_AGE;
	unlink "$dir/$f";
    }
    closedir ND;
    
}

# On this Iris, fleet as the wind, sped forth to deliver her message.
#   -- Homer, The Iliad
sub transmit {
    my $me  = shift;
    my $dst = shift;
    my $tag = shift;
    my @more = @_;
    my( $msg, $extra );

    # is dst disabled?
    if( $dstparam{$dst} && $dstparam{$dst}{disabled} ){
	# $me->loggit( $dst, 'not sent' );
	$me->{status}{$dst} = 'disabled';
	
	return;
    }
    
    # if many, summarize
    if( @more ){
	my $nolots = NotMe::nolots( $dst );
	$nolots ||= $Conf::Config{nolotsmsgs};
	
	my $d;
	foreach my $p (@more){
	    $d = 1 if $p->{objstate} eq 'down';
	}
	if( $d ){
	    $msg = $Conf::Config{message_lotsdn} || 'Lots of stuff just went DOWN!';
	}else{
	    $msg = $Conf::Config{message_lotsup} || 'Lots of stuff just came UP!';
	}

	if( $nolots ){
	    # list all messages, don't summarize into Lots UP/DOWN
	    $msg = join("\n", map { $_->{msg} } @more);
	}
	
    }else{
	$msg = $me->{msg};
    }

    $extra .= " $tag" if $tag;
    $extra .= " ESCALATED" if $me->{escalated};	# RSN - from $who...

    NotMe::transmit($me, $dst, $msg, $extra, @more)
	unless $Conf::Config{_dont_ntfy};

    $lastsent{$dst} = $^T;
    foreach my $p ( $me, @more ){
	$p->loggit( $dst, 'transmit' );
	$p->{status}{$dst} = 'sent';
	if( $p->{autoack} ){
	    $p->ack();
	}else{
	    $p->save();
	}
    }
}

sub expand {
    my $me  = shift;
    my $fmt = shift;
    my $msg = shift;
    my $obj = shift;

    # interpolate message 1st, it may contain additional % sequences...
    $fmt =~ s/%m/$msg/g;
    
    if( $fmt =~ /%[zt]/ ){
	my $tz = $ENV{TZ};
	my( $t, $z, $f, @l );
	
	if( $me->{timezone} ){
	    $ENV{TZ} = $me->{timezone};
	    tzset();
	}
	if( $fmt =~ /%t{([^\}]+)}/ ){
	    $f = $1;
	}else{
	    $f = "%d/%b %R";	# dd/Mon hh:mm
	}
	@l = localtime($me->{created});
	$t = strftime $f, @l;		
	$z = strftime "%z", @l;
	if( $me->{timezone} ){
	    $ENV{TZ} = $tz;
	    delete $ENV{TZ} unless defined $tz;
	    tzset();
	}
	
	$fmt =~ s/%z/$z/g;
	$fmt =~ s/%t({[^\}]+})?/$t/g;
    }

    $fmt =~ s/%d/$DARP::info->{tag}/ if defined $DARP::info;
    $fmt =~ s/%i/$me->{idno}/g;
    $fmt =~ s/%p/$me->{priority}/g;
    $fmt =~ s/%r/$obj->{srvc}{reason}/g;
    $fmt =~ s/%s/\U$me->{objstate}/g;
    $fmt =~ s/%v/$obj->{srvc}{result}/g;
    $fmt =~ s/%y/$me->{severity}/g;
    
    if( $fmt =~ /%o/ ){
    	my( $param, $value );

	if( $fmt =~ /%o{([^\}]+)}/ ){
	    $param = $1;
	    $param =~ s/^\s+//;
	    $param =~ s/\s+$//;

	    eval {
		$value = $obj->getparam($param);
	    };
	    if( $@ ){
		$value = 'Param Not Found';
            }
	}else{
	    $value = $obj->{uname};
	}
        $fmt =~ s/%o({[^\}]+})?/$value/g;
    }
    
    $fmt;
}

################################################################

sub number_of_notifies { scalar keys %byid }
sub byid { my $id = shift; $byid{$id} }

sub cmd_list_line {
    my $me = shift;
    my $ctl = shift;
    
    $ctl->write("$me->{idno} $me->{objstate} $me->{created} " .
		encode($me->{obj}->filename()) . " " .
		encode($me->{msg} || '_') . ' ' .
		encode($me->{priority} || '_') . ' '. encode($me->{severity} || '_') .
		"\n");
}

sub cmd_list {
    my $ctl = shift;
    my $param = shift;
    
    $ctl->ok();

    if( $param->{which} eq 'unacked' ){
	# oldest first
	foreach my $p (sort {$a->{created} <=> $b->{created}} values %unacked){
	    $p->cmd_list_line($ctl);
	}
    }
    elsif( $param->{which} eq 'queued' ){
	foreach my $dst (sort keys %queue){
	    # print STDERR "list q $dst\n";
	    foreach my $p (@{$queue{$dst}}){
		$p->{not}->cmd_list_line($ctl);
	    }
	}
    }
    else{
	# newest first
	foreach my $p (sort {$b->{created} <=> $a->{created}} values %byid){
	    $p->cmd_list_line($ctl)
		unless $p->{audit} && !$param->{showaudit};
	}
    }
    $ctl->final();
}

sub cmd_detail {
    my $ctl = shift;
    my $param = shift;
    my( $p, $k, $v, $ww, $n );

    $p = $param->{idno};
    if( exists $byid{$p} ){
	$p = $byid{$p};
	$ctl->ok();
	foreach $k (sort keys %$p){
	    $v = $p->{$k};
	    if( ref($v) ){
		$v = "#<REF>";
	    }else{
		$v = encode( $v );
	    }
	    $ctl->write( "$k:\t$v\n" );
	}
	$ctl->write( "object: " . encode( $p->{obj}->unique() ) . "\n" );

	foreach my $w (sort keys %{$p->{status}}){
	    my $we = encode($w);
	    $ww .= " $we";
	    $ctl->write("status $we: " . $p->{status}{$w} . "\n" );
	}
	$ctl->write("statuswho: $ww\n");
	$n = 0;
	foreach my $l (@{$p->{log}}){
	    $ctl->write( "log $n: $l->{time} " . ($l->{who}? encode($l->{who}) : '_') .
			 ' ' . encode($l->{msg}) . "\n" );
	    $n++;
	}
	$ctl->write( "loglines: $n\n");
	$ctl->final();
    }else{
	$ctl->bummer(404, 'Notification Not Found');
    }
}

sub cmd_ack {
    my $ctl = shift;
    my $param = shift;
    my( $p, $u );
    
    $p = $param->{idno};
    $u = $param->{user} || 'anonymous';
    
    if( $p eq 'all' ){
	$ctl->ok_n();
	foreach $p (values %unacked){
	    $p->ack( $u );
	}
    }
    elsif( exists $byid{$p} ){
	$ctl->ok_n();
	$byid{$p}->ack( $u );
    }else{
	$ctl->bummer(404, 'Notification Not Found');
    }
}

sub cmd_set_dst {
    my $ctl = shift;
    my $param = shift;

    my $dst = $param->{dst};
    my $p   = $param->{param};
    my $v   = $param->{value};

    if( defined($dst) && defined($p) ){
	if( $p eq 'disable' || $p eq 'redirect' ){
	    if( $v ){
		$dstparam{$dst}{$p} = $v;
	    }else{
		delete $dstparam{$dst}{$p};
	    }
	    $ctl->ok_n();
	}else{
	    $ctl->bummer(404, 'Unknown param' );
	}
    }else{
	$ctl->bummer(500, 'Missing Param');
    }
}

sub cmd_get_dst {
    my $ctl = shift;
    my $param = shift;

    my $dst = $param->{dst};
    my $p   = $param->{param};

    if( defined($dst) && defined($p) ){
	my $v;
	if( exists $dstparam{$dst} && exists $dstparam{$dst}{$p} ){
	    $v = $dstparam{$dst}{$p};
	}
	$ctl->ok();
	$ctl->write( "value: " . encode($v) . "\n\n" );
    }else{
	$ctl->bummer(500, 'Missing Param');
    }
}

################################################################

Control::command_install( 'notify_list',   \&cmd_list,
			  'list notifications', 'which' );
Control::command_install( 'notify_detail', \&cmd_detail,
			  'tell about notification', 'idno' );
Control::command_install( 'notify_ack',    \&cmd_ack,
			  'ack a notification', 'idno user' );
Control::command_install( 'notify_setparam', \&cmd_set_dst,
			  'modify per dst params', 'dst param value' );
Control::command_install( 'notify_getparam', \&cmd_get_dst,
			  'get per dst param', 'dst param' );

Cron->new( freq => 60,
	   text => 'Notification maintenance',
	   func => \&maintenance );

Cron->new( freq => 6*3600,
	   text => 'Notification cleanup',
	   func => \&janitor );

1;
