#!/usr/bin/perl
#
# tenshi 0.4 2006/01/04
# <tenshi@inversepath.com>
#
# Copyright 2004-2006 Andrea Barisani <lcars@gentoo.org> <andrea@inversepath.com>
#                 and Rob Holland    <tigger@gentoo.org> <rob@inversepath.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as 
# published by the Free Software Foundation.

use strict;
use warnings;
use Net::SMTP;
use File::Temp;
use Getopt::Std;
use Sys::Hostname;
use filetest 'access';
use Term::ANSIColor qw(:constants);
use POSIX qw(locale_h setsid setuid setgid strftime floor);

setlocale(LC_TIME, "C");
File::Temp->safe_level(File::Temp::HIGH);
$Term::ANSIColor::AUTORESET = 1;

my %opts; 
getopts('c:hdfpCP:', \%opts);
if ($opts{'h'}) { usage(); }

my $version  = '0.4';
my $hostname = hostname();
my @startup_time = localtime();

my ($uid, $gid);

my $last_check  = 0;
my $last_minute = 0;
my $sleep       = 5;
my $mailtimeout = 10;

my $config_reinit = 1;

my ($mailserver, $limit, $pager_limit, $fifo_file, $hidepid, $status);
my ($config_read, $queue_flush_needed, $queue_check_needed, $time_to_die); 
my (@log_files, @log_prefix, @regexp, @queues, @skip, @group_stack);
my (%main, %last_match, %last_queue);

my $debug       = $opts{'d'} || 0;
my $profile     = $opts{'p'} || 0;
my $foreground  = $opts{'f'} || 0;
my $config_file = $opts{'c'} || '/etc/tenshi/tenshi.conf';
my $pid_file    = $opts{'P'} || '/var/run/tenshi.pid';

my $tail_file   = '/usr/bin/tail';
my $tail_args   = '-q --follow=name --retry -n 0';

my $filter_file = 0;
my $filter_args = "";

my %days =   ( 'mon' => 1, 'tue' => 2, 'wed' => 3,
               'thu' => 4, 'fri' => 5, 'sat' => 6,
               'sun' => 0 );

my %months = ( 'jan' => 1,  'feb' => 2,  'mar' => 3,
               'apr' => 4,  'may' => 5,  'jun' => 6,
               'jul' => 7,  'aug' => 8,  'sep' => 9,
               'oct' => 10, 'nov' => 11, 'dec' => 12 );

my @cron_specs = (
    { 'min' => 0, 'max' => 59, 'shift' => 0,  'wrap' => 0, 'localtime_field' => 1 },
    { 'min' => 0, 'max' => 23, 'shift' => 0,  'wrap' => 0, 'localtime_field' => 2 },
    { 'min' => 1, 'max' => 31, 'shift' => 0,  'wrap' => 0, 'localtime_field' => 3 },
    { 'min' => 1, 'max' => 12, 'shift' => -1, 'wrap' => 0, 'localtime_field' => 4, 'strings' => \%months },
    { 'min' => 0, 'max' => 7,  'shift' => 0,  'wrap' => 1, 'localtime_field' => 6, 'strings' => \%days   },
);
 
my $mask        = '______';
my $mask_length = length $mask;
my $subject     = 'tenshi report'; 
my $timezone    = get_timezone();

config_read($config_file);
$config_read = 1;

if ($opts{'C'}) { exit 0; }

#
# environment sanity check
#
  
die RED "[ERROR] no smtp server specified!"   if (!$mailserver);
die RED "[ERROR] $fifo_file: no such file!"   if ((!$profile and $fifo_file)         and   -f $fifo_file);
die RED "[ERROR] $tail_file: no such file!"   if ((!$profile and scalar(@log_files)) and ! -f $tail_file);
die RED "[ERROR] $filter_file: no such file!" if ((!$profile and $filter_file)       and ! -f $filter_file);

foreach my $log (@log_files) {
    die RED "[ERROR] $log: no such file!"      if (!$profile and ! -f $log);
    die RED "[ERROR] $log: file not readable!" if (!$profile and ! -r $log);
}

if (!$uid) { $uid = getpwnam('tenshi') or die RED "[ERROR] no such user: tenshi\n"; }
if (!$gid) { $gid = getgrnam('tenshi') or die RED "[ERROR] no such group: tenshi\n"; }

#
# log file parsing 
#

daemonize() unless ($debug || $profile || $foreground);

if (!$profile and !$fifo_file) {

    if (scalar(@log_files) < 1) {
        die RED "[ERROR] No log file has been specified\n";
    }

    my $log_files = join(' ', @log_files);

    open(FH, "$tail_file $tail_args $log_files|") or
        die RED "[ERROR] could not open pipe to tail: $!\n";

} elsif (!$profile and $fifo_file) {

    open(FH, "<$fifo_file") or
        die RED "[ERROR] could not open fifo file: $!\n";
    
} else {

    open(FH, "-") or
        die RED "[ERROR] could not open standard in $!\n";

}

$debug && debug(3);

$SIG{'TERM'} = sub { $debug && debug(5,'TERM') ; $status = 'terminating'; $queue_flush_needed = 1; $time_to_die   = 1; };
$SIG{'INT'}  = sub { $debug && debug(5, 'INT') ; $status = 'terminating'; $queue_flush_needed = 1; $time_to_die   = 1; };
$SIG{'HUP'}  = sub { $debug && debug(5, 'HUP') ; $status = 'reloading'  ; $queue_flush_needed = 1; $config_reinit = 1; };
$SIG{'USR1'} = sub { $debug && debug(5,'USR1') ; $status = 'queue check'; $queue_check_needed = 1; };
$SIG{'USR2'} = sub { $debug && debug(5,'USR2') ; $status = 'flushing'   ; $queue_flush_needed = 1; };
$SIG{'CHLD'} = sub { $debug && debug(5,'CHLD') ; print RED "[ERROR] Child died. Bailing out\n"; $time_to_die = 1; };

while (1) {
    my $now = time;
    
    if ($now > ($last_check + $sleep)) {
        $queue_check_needed = 1;
    }

    if ($queue_flush_needed) { queues_flush();            $queue_flush_needed = 0; $queue_check_needed = 0; }
    if ($queue_check_needed) { queues_check($now);        $queue_check_needed = 0; }
    if ($config_reinit)      { config_read($config_file); $config_reinit      = 0; }
    if ($time_to_die) { unlink $pid_file unless ($debug || $profile); close FH; die RED "Killed!\n"; }

    my $line;

    eval {
        local $SIG{'ALRM'} = sub { die };

        alarm 1;
        $line = <FH>;
        alarm 0;
    };

    if ($profile and eof(FH)) { print BLUE "[PROFILE] Reached end of file\n"; $time_to_die = 1; next; }

    if (!$@) {

        if ($time_to_die) { next; }
       
        my $hostname;

        chomp($line); $debug && debug(6,$line);
        foreach my $log_prefix (@log_prefix) {
            if ($line =~ s/$log_prefix//) { $hostname = $1; last; }
        }
        next unless $hostname;

        if ($hidepid) { $line =~ s/^(\S+)\[\d+\]: /$1: /o; }

        for (my $index = 0; $index <= $#regexp; $index++) {

            my $regexp = $regexp[$index];
            my $queue  = $queues[$index];
            my @queue  = split(/,/, $queues[$index]);

            if ($line =~ /$regexp/) { 

                $debug && debug(7,$queue,$line);
                last if ($queue eq 'trash');
                next if ($queue eq 'group');
 
                if ($queue eq 'repeat' and $last_match{$hostname}) {
                    my @last_queue = split(/,/, $last_queue{$hostname});
                    foreach my $last_queue (@last_queue) {
                        $main{$last_queue}{'logs'}{$hostname}{$last_match{$hostname}} += $1;
                    }
                    last;
                }

                my $offset = 0;
                my ($begin, $end);

                foreach my $i (1 .. $#-) {

                    $begin = $-[$i] + $offset;
                    $end   = $+[$i] + $offset; 
                    my $length = ($end - $begin);

                    substr($line, $begin, $length, $mask); 

                    $offset += ($mask_length - $length); 
                }    

                $debug && debug(8,$line);

                $last_queue{$hostname} = $queue;
                $last_match{$hostname} = $line;

                foreach my $queue (@queue) {
                    $main{$queue}{'logs'}{$hostname}{$line}++;
                }    

                last;
            }
            elsif ($skip[$index] > 0) {
                $index = ($skip[$index] - 1); $debug && debug(9,$skip[$index],$line);
            }
        }
    }
}

close FH;

queues_flush();

exit 0;

#
# subs
#

sub config_read {

    my $config_file = shift;
    
    $debug && debug(0,$config_file);

    if ($config_reinit) {
        %main   = ();
        @regexp = ();
        @queues = ();
        @skip   = ();
        @log_prefix     = ();
        $main{'group'}  = {};
        $main{'trash'}  = {};
        $main{'repeat'} = {};

        $hidepid = 0;

        push @log_prefix, qr/^[A-Z][a-z]{2}\s(?:\s|\d)\d\s\d{2}:\d{2}:\d{2}\s(\S+)\s/;
        
        $config_reinit = 0;
    }        

    #
    # configuration file parsing
    #

    open(my $CONF,$config_file) or die RED "[ERROR] could not open configuration file $config_file: $!\n";

    while (<$CONF>) {
        s/^\s+//;
        next if (/^#|^$/); 
        chomp;

        if (/^include\s+(\S+)/) { $debug && debug(1,$_) ; config_read($1); next; }

        if (/^includedir\s+(\S+)/) { 
            $debug && debug(1,$_);
            opendir(my $DIR, $1) or die RED "[ERROR] could not open directory $1: $!\n";
            while (defined(my $file = readdir($DIR))) {
                next unless -f "$1/$file";
                config_read("$1/$file"); 
            }
            next; 
        }

        if (/^set\s+logfile\s+(\S+)/) {
            if ($config_read and (!grep(/^$1$/, @log_files))) {
                die RED "[ERROR] Tried to change a protected setting: logfile. Please restart tenshi for this change to take effect\n";
            } elsif (!$config_read) {
                $debug && debug(1,$_);
            }
            if ($fifo_file) {
                die RED "[ERROR] Tried to change from tail mode to fifo mode.\n";
            }
            push @log_files, $1; 
        }
        elsif (/^set\s+fifo\s+(\S+)/) {
            if ($config_read and ($fifo_file ne $1)) {
                die RED "[ERROR] Tried to change a protected setting: fifo. Please restart tenshi for this change to take effect\n";
            } elsif (!$config_read) {
                $debug && debug(1, $_);
            }
            if ($#log_files > 0) {
                die RED "[ERROR] Tried to change from fifo mode to tail mode.\n";
            }
            $fifo_file = $1;
        }
        elsif (/^set\s+pidfile\s+(\S+)/) {
            next if $opts{'P'};
            if ($config_read and ($1 ne $pid_file)) {
                die RED "[ERROR] Tried to change a protected setting: pidfile. Please restart tenshi for this change to take effect\n";
            } elsif (!$config_read and (!$opts{'P'})) {
                $debug && debug(1,$_);
            }
            $pid_file = $1;
        }
        elsif (/^set\s+tail\s+(\S+)/) {
            if ($config_read and ($1 ne $tail_file)) {
                die RED "[ERROR] Tried to change a protected setting: tail. Please restart tenshi for this change to take effect\n";
            } elsif (!$config_read) {
                $debug && debug(1,$_);
            }
            $tail_file = $1;
        }
        elsif (/^set\s+tailargs\s+(.+)/) {
            if ($config_read and ($1 ne $tail_args)) {
                die RED "[ERROR] Tried to change a protected setting: tailargs. Please restart tenshi for this change to take effect\n";
            } elsif (!$config_read) {
                $debug && debug(1,$_);
            }
            $tail_args = $1;
        }
        elsif (/^set\s+uid\s+(.+)/) {
            if ($config_read and (getpwnam($1) ne $uid)) {
                die RED "[ERROR] Tried to change a protected setting: uid. Please restart tenshi for this change to take effect\n";
            } elsif (!$config_read) {
                $debug && debug(1,$_);
            }
            $uid = getpwnam($1) or die RED "[ERROR] no such user: $1\n";
        }
        elsif (/^set\s+gid\s+(.+)/) {
            if ($config_read and (getgrnam($1) ne $gid)) {
                die RED "[ERROR] Tried to change a protected setting: gid. Please restart tenshi for this change to take effect\n";
            } elsif (!$config_read) {
                $debug && debug(1,$_);
            }
            $gid = getgrnam($1) or die RED "[ERROR] no such group: $1\n";
        }
        elsif (/^set\s+limit\s+(\d+)/)       { $limit       = $1; $debug && debug(1,$_); next; }
        elsif (/^set\s+subject\s+(.+)/)      { $subject     = $1; $debug && debug(1,$_); next; }
        elsif (/^set\s+filter\s+(\S+)/)      { $filter_file = $1; $debug && debug(1,$_); next; }
        elsif (/^set\s+mailserver\s+(.+)/)   { $mailserver  = $1; $debug && debug(1,$_); next; }
        elsif (/^set\s+filterargs\s+(.+)/)   { $filter_args = $1; $debug && debug(1,$_); next; }
        elsif (/^set\s+pager_limit\s+(\d+)/) { $pager_limit = $1; $debug && debug(1,$_); next; }
        elsif (/^set\s+mailtimeout\s+(\d+)/) { $mailtimeout = $1; $debug && debug(1,$_); next; }
        elsif (/^set\s+sleep\s+(\d+)/)       { 
            if ($sleep > 60) { die RED "[ERROR] sleep time should be <= 60 seconds\n"; } else {
                $sleep = $1; $debug && debug(1,$_); next; 
            }
        }    
        elsif (/^set\s+logprefix\s+(.+)/)  {
            push @log_prefix, qr/$1/;
            $debug && debug(1,$_);
        }    
        elsif (/^set\s+mask(\s+(\S+))?/)   {
            $mask        = ($1 ? $2: '');
            $mask_length = length $mask;
            $debug && debug(1,$_);
        }
        elsif (/^set\s+hidepid\s+(off|on)/) {
            if ($1 eq 'on') { $hidepid = 1; }
            else { $hidepid = 0; }
            $debug && debug(1,$_);
        }
        elsif
        (/^set\s+queue\s+(\S+)\s+(\S+(?:\@\S+)?)\s+(pager:)?(\S+(?:\@\S+)?)\s+\[((?:\S+(?:\s+)?){5}|now)\]\s*(\S+.*)?/o) {

            my ($queue, $mail_from, $pager, $mail_to, $cron_spec, $subject) = ($1, $2, $3, $4, $5, $6);

            if (queue_is_builtin($queue)) {
                die RED "[ERROR] '$queue' is a builtin queue!\n"; 
            }

            if ($cron_spec eq 'now') { 
                $main{$queue}{'now'} = 1; 
            } else { 
                $main{$queue}{'cron_mask'} = cron_spec_to_mask($cron_spec); 
            }

            if ($pager) { $main{$queue}{'pager'} = 1; }

            $main{$queue}{'mailfrom'} = $mail_from; $debug && debug(1,"queue: $queue - mail_from => $mail_from"); 
            $main{$queue}{'mailto'}   = $mail_to;   $debug && debug(1,"queue: $queue - mailto    => $mail_to"); 
            
            if ($subject) {
                $main{$queue}{'subject'} = $subject;  $debug && debug(1,"queue: $queue - subject => $subject");
            }

        }
        elsif (/^set\s+/) {

            die RED "[ERROR] Invalid set directive!: $_\n";

        }
        elsif (my ($queue, $reg) = $_ =~ /(^\S+)\s+(.+$)/) {

            $debug && debug(1,"queue: $queue regexp: $reg");

            my @queue = split(/,/, $queue);

            foreach my $queue (@queue) {
                if (!($main{$queue})) {
                    die RED "[ERROR] Invalid configuration directive!: queue $queue not defined\n";
                }
                if (($#queue > 0) and queue_is_builtin($queue)) {
                    die RED "[ERROR] builtin queue not allowed in multiple queues declaration!\n";
                }    
            }    

            if ($queue eq 'group') { push @group_stack, scalar(@regexp); }
            push @regexp, qr/$reg/;
            push @queues, $queue;
            push @skip, 0;

        }
        elsif (/^group_end/) {

            if (scalar(@group_stack) < 1) {
                die RED "[ERROR] Tried to close a group when there are non open\n";
            }

            $skip[pop @group_stack] = scalar(@regexp) || 0;
            
        }
        else {

            die RED "[ERROR] Invalid configuration directive!: $_\n";

        }
    }    

    $debug && debug(2,$config_file);

    if ($debug) {
        for (my $i = 0; $i < scalar(@regexp); $i++) {
        debug(18, $i, $regexp[$i]);
        }
    }

}

sub queue_is_builtin {
    my $queue = shift;
    if (($queue eq 'trash') or ($queue eq 'repeat') or ($queue eq 'group')) {
        return 1;
    }

    return 0;
}

sub queues_check {
    my $now  = shift;
    my @time = localtime($now);
    my $check_crons = 0;

    $last_check = $now;

    my $current_minute = floor($now / 60);

    if ($current_minute > $last_minute) {
        $check_crons = 1;
        $last_minute = $current_minute;
    }

    $debug && debug(12);

    foreach my $queue (keys %main) {

        next if queue_is_builtin($queue);

        if ($main{$queue}{'now'} || ($check_crons && cron_mask_match(\@time, $main{$queue}{'cron_mask'}))) {
            queue_mail($queue) if (!$profile);
        }
    }
}

sub queues_flush {
    $debug && debug(13);

    foreach my $queue (keys %main) {
        next if queue_is_builtin($queue);
        queue_mail($queue) if (!$profile);
    }  
    if ($status) { $status = 0; }
}

sub queue_mail {
    my $queue = shift;
    my @lines;

    return unless (keys %{$main{$queue}{'logs'}});
    $debug && debug(11,$queue);

    my $smtp = Net::SMTP->new($mailserver, Timeout => $mailtimeout);
        
    if (!$smtp) { 
        print RED "[ERROR] could not contact $mailserver:25\n"; 
        return; 
    }
    if (!$smtp->mail($main{$queue}{'mailfrom'})) { 
        print RED "[ERROR] mail from: $main{$queue}{'mailfrom'} rejected!\n"; 
        return; 
    }
    if (!$smtp->to(split(/,/, $main{$queue}{'mailto'}))) {
        print RED "[ERROR] rcpt to: $main{$queue}{'mailto'} rejected!\n"; 
        return;
    }
    if (!$smtp->data()) { 
        print RED "[ERROR] data rejected!\n"; 
        return; 
    }
    
    my $subject = $main{$queue}{'subject'} || $subject;
   
    $smtp->datasend("From: $main{$queue}{'mailfrom'}\n");
    $smtp->datasend("To: $main{$queue}{'mailto'}\n");
    $smtp->datasend("Date: " . strftime("%a, %d %b %Y %H:%M:%S $timezone", localtime()) . "\n");
    $smtp->datasend("X-tenshi-version: $version\n");
    $smtp->datasend("X-tenshi-hostname: $hostname\n");

    if (!$main{$queue}{'now'}) {
        my @now = localtime();
        $main{$queue}{'report_time'} = [ @startup_time ] if (!$main{$queue}{'report_time'});
        $smtp->datasend("X-tenshi-report-start: " . strftime("%a %b %d %H:%M:%S $timezone %Y", @{$main{$queue}{'report_time'}}) . "\n");
        $main{$queue}{'report_time'} = [ @now ];
    }
    
    $smtp->datasend("Subject: $subject [$queue]\n\n");
    
    my $tmp = new File::Temp(UNLINK => 1, DIR => '/tmp/', SUFFIX => '.tenshi')
        or die RED "[ERROR] could not open temporary file: $!\n";
    $debug && debug(19,$tmp);
    
    if ($status and (!$main{$queue}{'pager'})) {
        print $tmp "*** Status: $status ***\n";
    }
    foreach my $hostname (keys %{$main{$queue}{'logs'}}) {

        my $index = 0;
        next unless (keys %{$main{$queue}{'logs'}{$hostname}});
        print $tmp "\n$hostname: \n" if (!$main{$queue}{'pager'});

        foreach my $key (reverse sort { $main{$queue}{'logs'}{$hostname}{$a} <=> $main{$queue}{'logs'}{$hostname}{$b} } keys %{$main{$queue}{'logs'}{$hostname}}) {
    
            if ($main{$queue}{'pager'}) {
                last if ($pager_limit and ($index >= $pager_limit));
                print $tmp "$hostname,$main{$queue}{'logs'}{$hostname}{$key},$key\n";
                $index++;
            } else {
                last if ($limit and ($index >= $limit));
                print $tmp "    $main{$queue}{'logs'}{$hostname}{$key}: $key\n";
                $index++;
            }
        }        

        print $tmp "\n  *** Too many alerts (limit: $limit)  ***\n"
            if ($limit and ($index >= $limit));
    }
    
    seek($tmp, 0, 0) or die RED "[ERROR] can't rewind $tmp->filename: $!\n";

    if ($filter_file) {
        local $SIG{CHLD} = 'IGNORE'; # FIXME it's ugly I know, need something smarter here
        
        open(my $filter, "$filter_file $filter_args < $tmp|") or 
            die RED "[ERROR] '$filter_file $filter_args < $tmp' failed: $!\n";
      
        while (<$filter>) { push  @lines, $_; }
    } else {
        while (<$tmp>)    { push  @lines, $_; }
    }
    
    $smtp->datasend(@lines);
    $smtp->quit;
    $main{$queue}{'logs'} = {};
}

sub daemonize {
    chdir '/'                   or die RED "[ERROR] can't chdir to /: $!\n";
    defined(my $pid = fork)     or die RED "[ERROR] can't fork: $!\n";
    exit if $pid;
    setsid()                    or die RED "[ERROR] can't start a new session: $!\n";
    setgid($gid)                or die RED "[ERROR] can't setgid to $gid: $!\n";
    setuid($uid)                or die RED "[ERROR] can't setuid to $uid: $!\n";

    open (PIDFILE,">$pid_file") or die RED "[ERROR] could not open pid file $pid_file: $!\n";
    print PIDFILE $$; $debug && debug(4,$$);
    close PIDFILE;

    close STDIN                 or die RED "[ERROR] can't close STDIN: $!\n";
    close STDOUT                or die RED "[ERROR] can't close STDOUT: $!\n";
    close STDERR                or die RED "[ERROR] can't close STDERR: $!\n";
}    

sub get_timezone {
    use Time::Local;

    my @time = localtime;
    my $timediff = (timegm(@time) - timelocal(@time));
    return sprintf("%+03d%02d", $timediff/3600 , $timediff%3600/60);
}    

sub cron_field_resolve {
    my $field       = lc(shift);
    my $strings_ref = shift;

    if (ref($strings_ref) && $strings_ref->{$field}) {
        return $strings_ref->{$field};
    }
    else
    {
        return $field;
    }
}

sub cron_spec_to_mask {
    
    my $string = shift;
    
    my @mask;

    for (my $i = 0; $i < scalar(@cron_specs); $i++) {
        $string =~ s/^(\S+)\s*//o
            or die RED "[ERROR] Unable to parse cron string: $string\n";

        my $cron_spec = $cron_specs[$i];

        my @mask_fields;
        $#mask_fields = $cron_spec->{'max'} + $cron_spec->{'shift'};
        @mask_fields  = map { 0 } @mask_fields;

        foreach my $field (split(/,/, $1)) {
            my $start = 0;
            my $end   = 0;
            my $skip  = 1;
            if ($field =~ /\*(?:\/([0-9]+))?/o) {
                $start = $cron_spec->{'min'};
                $end   = $cron_spec->{'max'};
                if ($1) { $skip = $1 }
            }
            else
            {
                if (!($field =~ /(\w+)(?:-(\w+)(?:\/([0-9]+))?)?/o)) {
                    die RED "[ERROR] Error in field syntax: $field\n";
                }

                if ($#- == 1) {
                    $start = cron_field_resolve($1, $cron_spec->{'strings'});
                    $end   = cron_field_resolve($1, $cron_spec->{'strings'});
                }
                elsif ($#- == 2) {
                    $start = cron_field_resolve($1, $cron_spec->{'strings'});
                    $end   = cron_field_resolve($2, $cron_spec->{'strings'});
                } 
                elsif ($#- == 3) {
                    $start = cron_field_resolve($1, $cron_spec->{'strings'});
                    $end   = cron_field_resolve($2, $cron_spec->{'strings'});
                    $skip  = $3;
                }

                if ($start > $end) {
                    die RED "[ERROR] Error in field syntax. Ranges should be <lower>-<higher>: $field\n"
                }
            }

            if ($start < $cron_spec->{'min'}) {
                die RED "[ERROR] $start is below minimum value for field in: $field\n";
            }

            if ($end > $cron_spec->{'max'}) {
                die RED "[ERROR] $end is above maximum value for field in: $field\n";
            }

            if ($cron_spec->{'shift'}) {
                $start += $cron_spec->{'shift'};
                $end   += $cron_spec->{'shift'};
            }

            for (my $j = $start; $j <= $end; $j += $skip) {
                if (($j == $end) && $cron_spec->{'wrap'} && ($j == $cron_spec->{'max'})) {
                    $mask_fields[$cron_spec->{'min'}] = 1;
                    last;
                }
                $mask_fields[$j] = 1;
            }

            $mask[$i] = \@mask_fields;
        }
    }

    return \@mask;
}

sub cron_mask_match {
    my @time = @{shift()};
    my @mask = @{shift()};

    $debug && debug(15, join(' - ', map { join(',', @{$_}) } @mask), join(',', @time));

    for (my $i = 0; $i < scalar(@mask); $i++) {
        if (!$mask[$i]->[$time[$cron_specs[$i]->{'localtime_field'}]]) {
            $debug && debug(16);
            return 0;
        }
    }

    $debug && debug(17);
    return 1;
}

sub debug {

    if (!defined($_[1])) { $_[1] = 'foo'; }
    if (!defined($_[2])) { $_[2] = 'foo'; }

    my (%debug_msg);

    $debug_msg{'0'}{'msg'}  = "[CONF]  reading config file $_[1]\n";
    $debug_msg{'0'}{'col'}  = CYAN;

    $debug_msg{'1'}{'msg'}  = "[CONF]  parsing conf directive - $_[1]\n";
    $debug_msg{'1'}{'col'}  = CYAN;

    $debug_msg{'2'}{'msg'}  = "[CONF]  configuration file  $_[1] successfully parsed\n";
    $debug_msg{'2'}{'col'}  = WHITE;
    
    $debug_msg{'3'}{'msg'}  = "[INIT]  entering tail loop\n";
    $debug_msg{'3'}{'col'}  = BLUE;

    $debug_msg{'4'}{'msg'}  = "[INIT]  saving pid $$ in $pid_file\n";
    $debug_msg{'4'}{'col'}  = MAGENTA;

    $debug_msg{'5'}{'msg'}  = "[MAIN]  trapped $_[1] signal!\n";
    $debug_msg{'5'}{'col'}  = RED;

    $debug_msg{'6'}{'msg'}  = "[MAIN]  got message: $_[1]\n";
    $debug_msg{'6'}{'col'}  = WHITE;

    $debug_msg{'7'}{'msg'}  = "[MAIN]  matched message for queue $_[1]: $_[2]\n";
    $debug_msg{'7'}{'col'}  = GREEN;

    $debug_msg{'8'}{'msg'}  = "[MAIN]  masked message: $_[1]\n";
    $debug_msg{'8'}{'col'}  = YELLOW;

    $debug_msg{'9'}{'msg'}  = "[MAIN]  skipping to regex: $_[1] after failed match for group regex on line: $_[2]\n";
    $debug_msg{'9'}{'col'}  = YELLOW;

    $debug_msg{'11'}{'msg'} = "[QUEUE] flushing queue $_[1]\n";
    $debug_msg{'11'}{'col'} = RED;

    $debug_msg{'12'}{'msg'} = "[QUEUE] checking queues\n";
    $debug_msg{'12'}{'col'} = CYAN;

    $debug_msg{'13'}{'msg'} = "[QUEUE] flushing all queues\n";
    $debug_msg{'13'}{'col'} = RED;

    $debug_msg{'14'}{'msg'} = "[CRON] creating cron mask from: $_[1]\n";
    $debug_msg{'14'}{'col'} = GREEN;

    $debug_msg{'15'}{'msg'} = "[CRON] testing mask: $_[1] against current time: $_[2]\n";
    $debug_msg{'15'}{'col'} = GREEN;

    $debug_msg{'16'}{'msg'} = "[CRON] test returned negative\n";
    $debug_msg{'16'}{'col'} = GREEN;

    $debug_msg{'17'}{'msg'} = "[CRON] test returned positive\n";
    $debug_msg{'17'}{'col'} = GREEN;

    $debug_msg{'18'}{'msg'} = "[REGEX] Set regex: $_[1] to: $_[2]\n";
    $debug_msg{'18'}{'col'} = YELLOW;
    
    $debug_msg{'19'}{'msg'} = "[FILE] opening $_[1]\n";
    $debug_msg{'19'}{'col'} = BLUE;

    print $debug_msg{$_[0]}{'col'}, $debug_msg{$_[0]}{'msg'};
}

sub usage {
   die "tenshi 0.4                    <tenshi\@inversepath.com> || http://dev.inversepath.com/tenshi
Copyright 2004-2006 Andrea Barisani <lcars\@gentoo.org> || <andrea\@inversepath.com>
                and Rob Holland    <tigger\@gentoo.org> || <rob\@inversepath.com>\n
Usage: $0 [-c conf_file] [-C|-d|-f|-p] [-P pid_file]
   -c configuration file
   -C test configuration syntax 
   -d debug mode
   -f foreground mode
   -p profile mode
   -P pid file
   -h this help\n\n";
}

# vim: set ts=4 sw=4 expandtab:
