#!/usr/bin/perl
### exim-seltatr.perl  -*- Perl -*-
## Track sender (MAIL FROM:) long-term abuse by consulting Exim logs.

### Ivan Shmakov, 2021

## To the extent possible under law, the author(s) have dedicated
## all copyright and related and neighboring rights to this software
## to the public domain worldwide.  This software is distributed
## without any warranty.

## You should have received a copy of the CC0 Public Domain Dedication
## along with this software.  If not, see
## <http://creativecommons.org/publicdomain/zero/1.0/>.

### History:

## 0.1  2021-07-02 18:50Z
##      Initial revision as a separate file.

### Code:

use common::sense;
use English qw (-no_match_vars);

require Date::Manip::Date;

our $dev_fd_prefix
    = ($ENV{"DEVFDPREFIX"} // "/dev/fd/");

sub dev_fd {
    my ($fn) = @_;
    our ($dev_fd_prefix);
    my $l
        = length ($dev_fd_prefix);
    ## .
    (substr ($fn, 0, $l) eq $dev_fd_prefix
     ? substr ($fn, $l) : undef);
}

sub open_file {
    my ($fn, $mode) = @_;
    ## .
    die ()
        if (index ($mode, ">") > 0);
    return \*STDIN
        if ($fn eq "-");

    $mode
        //= "<";
    my $fd
        = ("GLOB" eq ref ($fn) ? $fn->fileno () : dev_fd ($fn));
    ## .
    (defined ($fd) ? IO::File->new_from_fd ($fd, $mode)
     : IO::File->new ($fn, $mode));
}

sub int_width {
    my ($v) = @_;
    ## .
    die ()
        unless ($v >= 0);

    my $r;
    for ($r = 0; $v > 0; ++$r, ($v >>= 1)) {
        ## do nothing
    }

    ## .
    $r;
}

## main

shift (@ARGV)
    if ("--" eq $ARGV[0]);

my @files
    = @ARGV
    or die ("Usage: ${0} [--] MAINLOG... BLACKLIST");
my $do
    = Date::Manip::Date->new ();
## .
my $fh
    = open_file ($files[0])
    or die ($files[0], ": Cannot open file: ", $!);
my (%last, %first)
    = ();
while (@files > 0) {
    local $_
        = <$fh>;
    if (defined ($_)) {
        ## do nothing
    } elsif (eof ($fh)) {
        shift (@files);
        last
            unless (@files > 0);
        ## .
        $fh = open_file ($files[0])
            or die ($files[0], ": Cannot open file: ", $!);
    } else {
        ## .
        die ($files[0], ": Error reading file: ", $!);
    }

    if ((my ($tsta) = m {^([0-9]+-[0-9]{2}-[0-9]{2}\s[0-9:]{8})\s})) {
        ## This looks like an exim4/mainlog entry;
        ## update the hashes accordingly

        my ($from, $fail)
            = m {(?:<=\s(\S+)|F=<(\S+)>)}
            or next;
        my $ff
            = ($from // $fail);

        foreach my $rpat ($ff, $ff =~ s/.*?\@//r) {
            $last{$rpat} = $tsta
                if (! exists ($last{$rpat}) || $last{$rpat} lt $tsta);
            $first{$rpat} = $tsta
                if (! exists ($first{$rpat}) || $first{$rpat} gt $tsta);
        }
        next;
    }

    my ($rpat)
        = m {(^[^#]\S*)}
        or do {
            print ($_);
            next;
        };

    chomp ();
    unless (exists ($last{$rpat})) {
        print ($_, "\n");
        next;
    };

    my ($last, $first) = map {
        $do->parse ($_);
        ## .
        $do->secs_since_1970_GMT ()
            or die ($_, ": Cannot parse timestamp");
    } ($last{$rpat}, $first{$rpat});

    if (! m {#}) {
        ## Looks like a freshly-added local_sender_blacklist entry;
        ## add a comment to it
        $_ .=  (("\t" x (4 - (length () >> 3)))
                . "\t## " . sprintf ("%2d", int_width ($last - $first))
                . " " . $first . " .. " . $last);
    } elsif (m {(?:\b[0-9]+\s)?(\(?)\b([0-9]+)(\s\.\.\s)[0-9]+\b}) {
        ## Update the comment of this local_sender_blacklist entry
        unless ($first <= ${2}) {
            $first{$rpat}
                = $first
                = ${2};
        }
        s//${
            ## .
            \sprintf ("%2d", int_width ($last - $first));
        } ${1}${first}${3}${last}/;
    }
    print ($_, "\n");
} 

### exim-seltatr.perl ends here
