#!/usr/bin/perl -sw
##
## Razor2::Client::Agent -- UI routines for razor agents.
##
## Copyright (c) 2002, Vipul Ved Prakash.  All rights reserved.
## This code is free software; you can redistribute it and/or modify
## it under the same terms as Perl itself.
##
## $Id: Agent.pm,v 1.98 2006/10/18 06:15:08 rsoderberg Exp $

package Razor2::Client::Agent;

use lib qw(lib);
use strict;
use Getopt::Long;
use IO::File;

use Razor2::String qw(fisher_yates_shuffle);

use base qw(Razor2::Client::Core);
use base qw(Razor2::Client::Config);
use base qw(Razor2::Logger);
use base qw(Razor2::String);
use Razor2::Preproc::Manager;
use Data::Dumper;
use vars qw( $VERSION $PROTOCOL );


$PROTOCOL = $Razor2::Client::Version::PROTOCOL;
$VERSION  = $Razor2::Client::Version::VERSION;



sub new {
    my ($class, $breed) = @_;

    # For Taint Friendliness
    delete $ENV{PATH};
    delete $ENV{BASH_ENV};

    my @valid_program_names = qw(
            razor-check
            razor-report
            razor-revoke
            razor-admin
    );

    my $ok = 0;
    foreach (@valid_program_names) { $breed =~ /$_$/ and $ok = $_; }
    unless ($ok) {
        if ($breed =~ /razor-client$/) {
            # We no longer create symlinks, but for backwards compatibility
            # return success.
            exit 0;
        }
        die "Invalid program name, must be one of: @valid_program_names\n";
    }

    $ok =~ /razor-(.*)$/;
    my %me = (
            name_version => "Razor-Agents v$VERSION",  # used in register
            breed        => $1,
            preproc      => new Razor2::Preproc::Manager (no_deHTMLcomment => 1),
            preproc_vr8  => new Razor2::Preproc::Manager (no_deHTML => 1),
            global_razorhome => '/usr/local/etc',
    );


    return bless \%me, $class;
}

sub do_conf {
    my $self = shift;

    # parse config-related cmd-line args
    #

    # identity is parsed later after razorhome is fully resolved

    if ($self->{opt}->{config}) {
        if ($self->{opt}->{create_conf}) {
            $self->{razorconf} = $self->{opt}->{config};
        } elsif (-r $self->{opt}->{config}) {
            $self->{razorconf} = $self->{opt}->{config};
        } else {
            return $self->error("Can't read conf file: $self->{opt}->{config}")
        }
    }
    if ($self->{opt}->{razorhome}) {
        if (-d $self->{opt}->{razorhome}) {
            $self->{razorhome} = $self->{opt}->{razorhome};
        } else {
            return $self->error("Can't read: $self->{opt}->{razorhome}")
                unless $self->{opt}->{create_conf};
        }
        # once razorhome is successfully overridden, override the global razorhome as well.
        $self->{global_razorhome} = $self->{razorhome};
    }
    return unless $self->read_conf();

    if ($self->{opt}->{create_conf}) {
        $self->{force_discovery} = 1;
        $self->{force_bootstrap_discovery} = 1;
        $self->log(8," -create will force complete discovery");
    }
    if ($self->{opt}->{force_discovery}) {
        $self->{force_discovery} = 1;
        $self->{force_bootstrap_discovery} = 1;
        $self->log(8," -discover will force complete discovery");
    }
    if ($self->{opt}->{debug} && !$self->{opt}->{debuglevel}) {
        $self->{conf}->{debuglevel} ||= 9;
        $self->{conf}->{debuglevel} = 9 if $self->{conf}->{debuglevel} < 9;
    }


    #
    # Note: we start logging before we process '-create' ,
    # so logfile will not go into a newly created razorhome
    #
    #my $logto = $self->{opt}->{debug} ? "stdout" : "file:$self->{conf}->{logfile}";
    my $logto;
    if ($self->{opt}->{debug}) {
        $logto = 'stdout';
    } elsif ($self->{conf}->{logfile} eq 'syslog') {
        $logto = 'syslog';
    } elsif ($self->{conf}->{logfile} eq 'sys-syslog') {
        $logto = 'sys-syslog';
    } else {
        $logto = "file:$self->{conf}->{logfile}";
    }
    if (exists $self->{conf}->{logfile}) {
        my $debuglevel = exists $self->{conf}->{debuglevel} ? $self->{conf}->{debuglevel} : 9;
        my $logger = new Razor2::Logger (
                        LogDebugLevel => $debuglevel,
                        LogTo         => $logto,
                        LogPrefix     => $self->{breed},
                        LogTimestamp  => 1,
                        DontDie       => 1,
                        Log2FileDir   => defined($self->{conf}->{tmp_dir}) ? $self->{conf}->{tmp_dir} : "/tmp",
                     );
        $self->{logref} = ref($logger) ? $logger : 0;
        # log error strings at loglevel 11.  Pick a high number 'cuz
        # if its really an error, it will be in errstr for caller
        $self->{logerrors} = 11;
    }
    $self->logobj(15,"cmd-line options", $self->{opt});
    $self->{preproc}->{rm}->{log} = $self->{logref};

    # creates razorhome, and sets $self->{razorhome} if successful
    return $self->errprefix("Could not create 'razorhome'") unless $self->create_home_conf();
    $self->compute_identity;

    $self->log(5,"computed razorhome=$self->{razorhome}, conf=$self->{razorconf}, ident=$self->{identity}");
    return 1;
}

# if a debug log statement requires extra work, check this call before doing it.
sub logll {
    my ($self, $loglevel) = @_;
    return unless $self->{logref};
    return 1 if ($self->{logref}->{LogDebugLevel} >= $loglevel);
    return;
}

sub create_home_conf {
    my $self = shift;

    unless ($self->{opt}->{create_conf}) {
        #
        # if the global razorhome exists, don't create anything
        # without '-create' option
        #
        return 1 if (-d $self->{global_razorhome});

        #
        # if there is not global razorhome,
        # try to create razorhome one anyway.
        # if it fails, thats ok.
        #
        $self->create_home($self->{razorhome_computed});
        $self->errstrrst;  # nuke error string
        return 1;
    }

    #
    # user passed in 'create' option, so create.
    #
    my $rhome = $self->{opt}->{razorhome}
              ? $self->{opt}->{razorhome}
              : $self->{razorhome_computed};

    if ($rhome) {

        if (-d $rhome) {
            $self->log(6,"Not creating razorhome $rhome, already exists");
        } else {
            return unless $self->create_home($rhome);
        }
    }


    if ($self->{opt}->{config}) {

        # if create and conf specified, exit if write is not successful
        #
        $self->{razorconf} = $self->{opt}->{config};
        return $self->write_conf();

    } else {

        # else just try and create, if fail ok.
        #
        $self->compute_razorconf();
        $self->{razorconf} ||= $self->{computed_razorconf};
        $self->write_conf();
        $self->errstrrst;  # nuke error string
    }
    return 1;
}

# wrapper for log
sub log {
    my $self = shift;
    my $level = shift;
    my $msg = shift;

    if ($self->{logref}) {
        return  $self->{logref}->log($level, $msg);
    } elsif ($self->{opt}->{debug}) {
        print " Razor-Log: $msg\n" if $self->{opt}->{debug};
    }
}
sub log2file {
    my $self = shift;
    return unless $self->{logref};
    return        $self->{logref}->log2file(@_);
}

sub doit {
    my $self = shift;
    my $args = shift;
    my $r;

    $self->log(2," $self->{name_version} starting razor-$self->{breed} $self->{args}");
#    $self->log(9,"uname -a: ". `uname -a`) if $self->logll(9);

    $r = $self->checkit($args)    if $self->{breed} eq 'check';
    $r = $self->adminit($args)    if $self->{breed} eq 'admin';
    $r = $self->reportit($args)   if $self->{breed} eq 'report';
    $r = $self->reportit($args)   if $self->{breed} eq 'revoke';

    # return exit code
    # 0, 1 => ok
    #  > 1 => error  (caller should prolly print $self->errstr)
    #
    if ($r > 1) {
        my $msg = $self->errstr;
        $self->log(1,"razor-$self->{breed} error: ". $msg);
    } else {
        $self->log(8,"razor-$self->{breed} finished successfully.");
    }
    return $r;
}


sub _help {
    my ($self,$breed) = @_;

    chomp(my $all = <<EOFALL);
            -h  Print this usage message.
            -v  Print version number and exit
            -d  Turn on debugging.  Logs to stdout.
            -s  Simulate Only.  Does not connect to server.
    -conf=file  Use this config file instead of <razorhome>/razor.conf
     -home=dir  Use this as razorhome
   -ident=file  Use this identity file instead of <razorhome>/identity
           -rs  Use this razor server instead of reading .lst
EOFALL
    chomp(my $sigs = <<EOFSIGS);
            -H  Compute and print signature.
   -S |  --sig  Accept a signatures to check on the command line
        -e eng  Engine used to compute sig, integer
      -ep4 val  String value required when engine == 4
EOFSIGS

    chomp(my $mbox = <<EOFMBOX);
   -M | --mbox  Accept a mailbox name on the command line (default)
                If no filename, mbox, or signatures, input read from stdin.
EOFMBOX

    my %b;
    $b{check} = <<EOFCHECK;

razor-check [options] [ filename | -M mbox | -S signatures | < filename ]
$all
$sigs
$mbox

See razor-check(1) manpage for details.

EOFCHECK

    $b{report} = <<EOFREPORT;

razor-report [options] [ filename | -M mbox | -S signatures -e engine]
$all
$sigs
$mbox
       -i file  Use identity from this file
            -f  Stay in foreground.
            -a  Authenticate only.  Exit 0 if authenticated, 1 if not
                Stays in foreground.

See razor-report(1) manpage for details.

EOFREPORT

    $b{admin} = <<EOFREGISTER;

razor-admin [options] [ -register | -create | -discover ]
$all
       -create  Create razorhome, does discover, does not register
     -discover  Discover Razor servers: write .lst files
     -register  Register a new identity
    -user name  Request 'name' when registering (requires -register)
    -pass pass  Request 'password' when registering (requires -register)
            -l  Make new identity the the default identity.
                Used only when registering.

See razor-admin(1) manpage for details.

EOFREGISTER

    $b{revoke} = <<EOFREVOKE;

razor-revoke [options] filename
$all
$mbox
       -i file  Use identity from this file
            -f  Stay in foreground.
            -a  Authenticate only.  exit 0 if authenticated, 1 if not
                Stays in foreground.

See razor-revoke(1) manpage for details.

EOFREVOKE

    my $future = <<EOFFUTURE;
EOFFUTURE

    return $b{$self->{breed}};
}


# maybe this should be in Client::Config
#
sub read_options {
    my ($self, $agent) = @_;
    $self->{args} = join ' ', @ARGV;
    Getopt::Long::Configure ("no_ignore_case");
    my %opt;
    #
    # These options override what is loaded in config file
    # the names on the right should match keys in config file
    #
    my $ret = GetOptions(
        's'   => \$opt{simulate},
        'd'   => \$opt{debug},
  'verbose'   => \$opt{debug},
        'v'   => \$opt{version},
        'h'   => \$opt{usage},
     'help'   => \$opt{usage},
        'H'   => \$opt{printhash},
      'C=s'   => \$opt{printcleaned},
    'sig=s'   => \$opt{sig},
      'S=s'   => \$opt{sig},
      'e=s'   => \$opt{sigengine},
    'ep4=s'   => \$opt{sigep4},
     'mbox'   => \$opt{mbox},
        'M'   => \$opt{mbox},
        'n'   => \$opt{negative},
   'conf=s'   => \$opt{config},
 'config=s'   => \$opt{config},
   'home=s'   => \$opt{razorhome},
        'f'   => \$opt{foreground},
     'noml'   => \$opt{noml},
   'user=s'   => \$opt{user},
      'u=s'   => \$opt{user},
   'pass=s'   => \$opt{pass},
        'a'   => \$opt{authen_only},
     'rs=s'   => \$opt{server},
 'server=s'   => \$opt{server},
        'r'   => \$opt{register},
 'register'   => \$opt{register},
        'l'   => \$opt{symlink},
      'i=s'   => \$opt{identity},
  'ident=s'   => \$opt{identity},
   'create'   => \$opt{create_conf},
'logfile=s'   => \$opt{logfile},
 'discover'   => \$opt{force_discovery},
     'dl=s'   => \$opt{debuglevel},
'debuglevel=s' => \$opt{debuglevel},
'whitelist=s' => \$opt{whitelist},
     'lm=s'   => \$opt{logic_method},
     'le=s'   => \$opt{logic_engines},
    );

    if ($ret == 0) {
        $self->error("failed to parse command line options.\n");
        return;
    }

    # remove elements not set in the cmd-line
    foreach (keys %opt) { delete $opt{$_} unless defined $opt{$_}; }

    if ($opt{usage}) {
        $self->error($self->_help);
        return;
    } elsif ($opt{mbox} && $opt{sig}) {
        $self->error("--mbox and --sig are mutually exclusive.\n");
        return;
    } elsif ($opt{sig} && !$opt{sigengine}) {
        $self->error("--sig requires -e (engine used to generate sig)\n");
        return;
        #
        # fixme - require ep4 if -e 4 is used ?
        #
    } elsif ($opt{version}) {
        $self->error("Razor Agents $VERSION, protocol version $PROTOCOL");
        return;
    }
    $self->{opt} = \%opt;
    return 1;
}



# returns 0 if match (spam)
# returns 1 if no match (legit)
# returns 2 if error
sub checkit {

    my $self = shift;
    my $args = shift;

    # check for spam.
    # input can be one of
    #   file - single mail
    #   mbox - many  mail
    #   sig  - 1 or more sigs
    #   or a filehandle provided via args

    my $objects;
    if ($self->{conf}->{sig}) {
        my @sigs;
        #
        # cmd-line sigs
        #
        # prepare 1 mail object per sig
        #
        foreach my $sig (split ',', $self->{conf}->{sig}) {
            $sig =~ s/^\s*//;  $sig =~ s/\s*$//;
            my $hr = {
                eng => $self->{conf}->{sigengine},
                sig => $sig,
            };
            $hr->{ep4} = "7542-10";
            $hr->{ep4} = $self->{conf}->{sigep4} if $self->{conf}->{sigep4};
            push @sigs, $hr;
        }
        $self->log (5,"received ". (scalar @sigs) ." valid cmd-line sigs.");
        $objects = $self->prepare_objects(\@sigs) or return 2;
    } else {

        my $mails = $self->parse_mbox($args) or return 2;

        $objects  = $self->prepare_objects($mails) or return 2;

        #
        # if mail is whitelisted, its not spam.
        # flag it so it we don't check it against server
        #
        foreach my $obj (@$objects) {
            if ($self->local_check($obj)) {
                $obj->{skipme} = 1;
                $obj->{spam} = 0;
            } else {
                next;
            }
        }

    }

    # compute_sigs needs server info like ep4, so get_server_info first
    $self->get_server_info()                            or return 2;
    my $printable_sigs = $self->compute_sigs($objects)  or return 2;

    if ($self->{opt}->{printhash}) {
        my $i = 0;
        foreach (@$printable_sigs) {
            if ($self->{opt}->{sigengine}) {
                next unless (/ e$self->{opt}->{sigengine}: /);
            }
            print "$_\n";
            $i++;
        }
        $self->log (4, "Done. Printed $i sig(s) for ". scalar(@$objects) ." mail(s)");
    }
    if ($self->{opt}->{printcleaned}) {
        my $totalp = 0;
        my $totalc = 0;
        foreach my $obj (@$objects) {
            my $n = 0;
            mkdir("$self->{opt}->{printcleaned}/cleaned");
            foreach ($obj->{headers}, @{$obj->{bodyparts_cleaned}}) {
                my $fn = "$self->{opt}->{printcleaned}/cleaned/mail$obj->{id}.". $n++;
                $self->write_file($fn, $_);
                $totalc++;
            }
            $n = 0;
            mkdir("$self->{opt}->{printcleaned}/uncleaned");
            foreach ($obj->{headers}, @{$obj->{bodyparts}}) {
                my $fn = "$self->{opt}->{printcleaned}/uncleaned/mail$obj->{id}.". $n++;
                $self->write_file($fn, $_);
                $totalp++;
            }
        }
        $self->log (4, "Done. $totalp uncleaned, $totalc cleaned mails saved in $self->{opt}->{printcleaned}");
        print "Done. $totalp uncleaned, $totalc cleaned mails saved in $self->{opt}->{printcleaned}\n";
        return 1;

    }

    return 1 if $self->{opt}->{printhash};

    # only check good objects
    my @goodones;                 # this should be optimized!
    foreach my $obj (@$objects) {
        next if $obj->{skipme};
        push @goodones, $obj;
    }
    unless (scalar @goodones) {
        $self->log (4,"Done.  No valid mail or signatures to check.");
        return 1;
    }

    if ($self->{conf}->{simulate}) {
        $self->log (4, "Done. (simulate only)");
        return 1;
    }

    #
    # Connect to catalogue server
    #
    $self->{s}->{list} = $self->{s}->{catalogue};
    $self->nextserver();
    $self->connect()          or return 2;

    #
    # Check against server
    #
    $self->check (\@goodones) or return 2;
    $self->disconnect()       or return 2;


    #
    # print out responses and exit
    #
    my $only1check = (scalar(@$objects) == 1) ? 1 : 0;
    my $has_spam = 0;
    foreach my $obj (@$objects) {

        $obj->{spam} = 0 if $obj->{skipme};
        $obj->{spam} = 0 unless defined $obj->{spam};

        if ($obj->{spam} > 0) {
            return 0 if $only1check;
            $has_spam = 1;
            print $obj->{id} ."\n";
            next;

        } elsif ($obj->{spam} == 0) {
            return 1 if $only1check;
            print "-". $obj->{id} ."\n" if $self->{conf}->{negative};
            next;

        } else {
            # error
            #
            $self->logobj(1,"bad 'spam' in checkit", $obj);
            return 2 if $only1check;
            print "-". $obj->{id} ."\n" if $self->{conf}->{negative};
            next;
        }
    }
    return 0 if $has_spam;
    return 1;
}



# returns 0 if success
# returns 2 if error
sub adminit {
    my $self = shift;

    my $done_something = 0;

    if ($self->{opt}->{create_conf}) {
        $done_something++;
        # $self->create_home_conf() is always checked
    }

    if (  $self->{opt}->{force_discovery} ||
          $self->{opt}->{create_conf}) {
        $done_something++;
        # get_server_info() calls nextserver() which calls discovery()
        $self->get_server_info()    or return 2;
    }

    if ($self->{opt}->{register}) {
        $done_something++;
        my $r = $self->registerit();
        return $r if $r;
    }

    unless ($done_something) {
        $self->error("An option needs to be specified,  -h for help.");
        return 2;
    }

    return 0;
}

# returns 0 if success
# returns 2 if error
sub registerit {
    my($self, $auto) = @_;

    unless ($self->{razorhome} || $self->{opt}->{identity}) {
        $self->errprefix("Unable to register without a valid razorhome or identity");
        return 2;
    }

    my $ident;

    if (exists $self->{opt}->{user}
        && ($ident = $self->get_ident)
        && $ident->{user} eq $self->{opt}->{user} ) {
        $self->error("You are already registered as user=$ident->{user} in $self->{razorhome}");
        return 2;
    }
    if ($self->{conf}->{simulate}) {
        $self->log(5,"Done - simulate only.");
        return 0;
    }

    if ($self->{opt}->{create_conf}) {
        $self->log(3, "Register create successful.");
        return 0;
    }

    if ($auto) {
        $self->log(3, "Write test underway");
        my($ident) = {
            user    =>  'writetest',
            pass    =>  'writetest',
        };
        my($fn);
        unless ($fn = $self->save_ident($ident)) {
            $self->log(3, "Unable to write identity to home");
            return 2;
        }
        unlink($fn) or return 2;
        $self->log(3, "Write test completed");
    }

    $self->get_server_info()    or return 2;
    $self->connect()            or return 2;

    $self->log(3, "Attempting to register.");
    # attempt to register the user/pass
    $ident = $self->register_identity($self->{opt}->{user}, $self->{opt}->{pass});

    $self->disconnect()     or return 2;

    unless (ref $ident) {
        $self->log(3, "Failed to register identity.");
        return 2;
    }

    if (my $fn = $self->save_ident($ident)) {
        my $msg = "Register successful.  Identity stored in $fn";
        $self->log(3, $msg);
        print "$msg\n";
        return 0;
    } else {
        $self->log(3, "Register failed.");
        return 2;
    }
}

#
# handles report and revoke
#
# returns 0 if success
# returns 2 if error
sub reportit {

    my ($self, $args) = @_;

    my $ident = $self->get_ident;
    unless ($ident) {
        $self->log(3, "Razor2 identity not found.  Attempting to register automatically.");
        if ($self->registerit("auto")) {
            $self->log(3, "Automatic registration failed.");
            $self->errprefix("Bootstrap Error: Your Razor2 identity was not found.\n   " .
                             "  If you haven't registered, please do so:\n" .
                             "     \"razor-admin -register -user=[name/email_address] -pass=[password]\".\n".
                             "     (Further information can be found in the razor-admin(1) manpage)\n" .
                             "  If you did register, please ensure your identity symlink (or file) is in order.\n");
            return 2;
        }
        $ident = $self->get_ident;
        unless ($ident) {
            $self->log(3, "Unable to load automatically registered identity.");
            $self->errprefix("Bootstrap Error: Your Razor2 identity was not found.\n   " .
                             "  If you haven't registered, please do so:\n" .
                             "     \"razor-admin -register -user=[name/email_address] -pass=[password]\".\n".
                             "     (Further information can be found in the razor-admin(1) manpage)\n" .
                             "  If you did register, please ensure your identity symlink (or file) is in order.\n");
            return 2;
        }
    }

    if (!$self->{opt}{foreground} &&
        (@ARGV < 1 || $ARGV[0] eq "-" || $ARGV[0] eq "")) {
        if (-t STDIN) {
            $self->error("Unable to read from a TTY using STDIN while forked. \n" .
                         "Doing so leads to undefined behaviour in certain shells.");
            return 2;
        }
    }

    # background myself
    unless ($self->{opt}->{foreground}) {
        chdir '/';
        fork && return 0;
        POSIX::setsid;
        # close 0, 1, 2;
    }

    if ($self->{opt}->{authen_only}) {
        $self->authenticate($ident) or return;
        $self->log(5,"Done - authenticate only.");
        return 0 if $self->{authenticated};
        return 2;
    }

    my $mails   = $self->parse_mbox($args) or return 2;

    my $objects = $self->prepare_objects($mails) or return 2;


    # compute_sigs needs server info like ep4, so get_server_info first
    $self->get_server_info()                            or return 2;

    my $printable_sigs = $self->compute_sigs($objects)  or return 2;

    if ($self->{opt}->{printhash}) {
        foreach (@$printable_sigs) {
            if ($self->{opt}->{sigengine}) {
                next unless (/ e$self->{opt}->{sigengine}: /);
            }
            print "$_\n";
        }
        exit 0;
    }

    if ( $self->{conf}->{simulate}) {
        $self->log (4, "Done. (simulate only)");
        exit 0;
    }
    unless (scalar @$objects) {
        $self->log (4,"Done.  No valid mail or signatures to check.");
        exit 1;
    }

    $self->{s}->{list} = $self->{s}->{nomination};
    $self->nextserver();
    $self->connect()            or return 2;
    $self->authenticate($ident) or return 2;
    $self->report($objects)     or return 2;
    $self->disconnect()         or return 2;


    if ($self->{opt}->{foreground}) {
        foreach my $obj (@$objects) {
            # my $line = debugobj($obj->{r});
            # $line =~ /(\S+=\S+)/s;  # could be res=0|1, err=xxx
            # print "$obj->{id}: $1\n";
            #print "$obj->{id}\n" if $obj->{r}->{res} == '1';
        }
    }
    return 0;
}


sub parse_mbox {
    my ($self, $args) = @_;

    my @mails;
    my @message;
    my $passed_fh = 0;
    my $aref;

    # There are different kinds of mbox formats, we just split on simplest case.
    # djb defines mbox, mboxrd, mboxcl, mboxcl2
    # http://www.qmail.org/qmail-manual-html/man5/mbox.html
    #
    # non-mbox support added, thanx to Aaron Hopkins <aaron@die.net>

    if (exists $$args{"fh"}) {
        @ARGV = ();
        push @ARGV, $$args{'fh'};
        $passed_fh = 1;
    } elsif (exists $$args{"aref"}) {
       $aref = $$args{"aref"};
    } elsif (!scalar @ARGV) {
        push @ARGV, "-"
    }

    if ($$args{'aref'}) {
        my @foo = (\join'', @{$$args{'aref'}});
        return \@foo;
    }

    foreach my $file (@ARGV) {
        my $fh = new IO::File;
        my @message = ();
        if (ref $file) {
            $fh = $file
        } else {
            open $fh, "<$file" or return $self->error("Can't open $file: $!");
        }

        my $line = <$fh>;
        next unless $line;

        if ($line =~ /^From /) {
            $self->log(8,"reading  mbox formatted mail from ".
                ($file eq '-' ? "<stdin>" : $file));
            while (1) {
                push @message, $line;
                $line = <$fh>;
                if (!defined($line) || $line =~ /^From /) {
                    push @mails, \join ('', @message);
                    @message = ();
                    last unless defined $line;
                }
            }
        } else {
            $self->log(8,"reading straight RFC822 mail from ".
                ($file eq '-' ? "<stdin>" : $file));
            push @mails, \join ('', map {s/^(>*From )/>$1/; $_} $line, <$fh>);
        }
        close $fh unless $passed_fh;
    }

    my $cnt = scalar @mails;
    $self->log (6, "read $cnt mail". ($cnt>1 ? 's' : '') );

    return \@mails;
}



sub raise_error {
    my ($self, $errstr) = @_;;
    my $str;
    if (ref $self) {
        $str = $self->errstr;
    }
    $str = $errstr if $errstr;
    my ($code) = $str =~ /Razor Error (\d+):/;
    $code = 255 unless $code;
    print "FATAL: $str";
    exit $code;
}

# returns 1 if mail should be skipped
#
sub local_check {
    my ($self, $obj) = @_;
    my ($headers, $body) = split /\n\r*\n/, ${$obj->{orig_mail}}, 2;

    $headers =~ s/\n\s+//sg;  # merge multi-line headers

    if ($self->{conf}->{ignorelist}) {
        if ($headers =~ /\n((X-)?List-Id[^\n]+)/i) {
            my $listid = $1;
            my ($line1) = substr(${$obj->{orig_mail}}, 0, 50) =~ /^([^\n]+)/;
            $self->log (5,"Mailing List post; mail ". $obj->{id} ." not spam.");
           #$self->log (5,"Mailing List post; mail ". $obj->{id} ." not spam.\n  $line1\n  $listid");
            return 1;
        }
    }
    return 0 if $self->{no_whitelist};
    if (-s $self->{conf}->{whitelist}) {
        $self->read_whitelist;
        foreach my $sh (keys %{$self->{whitelist}}) {
            if ($sh ne 'sha1') {
                while ($headers =~ /^$sh:\s+(.*)$/img) {
                    last unless $1;
                    my $fc = $1;
                    $self->log (13,"whitelist checking headers for match $sh: $fc");
                    foreach my $address (@{$self->{whitelist}->{$sh}}) {
                        if ($fc =~ /$address/i) {
                            $self->log (3,"ignoring mail $obj->{id}, whitelisted by rule: $sh: $address");
                            return 1;
                        }
                    }
                }
            }
        }
        $self->log (12,"Whitelist rules did not match mail $obj->{id}");
    } elsif ($self->{conf}->{whitelist}) {
        $self->log (6,"skipping whitelist file (empty?): $self->{conf}->{whitelist}");
        $self->{no_whitelist} = 1;
    }
    return 0;
}



sub read_whitelist {
    my ($self) = @_;
    return if $self->{whitelist};

    my %whitelist;
    my $lines = $self->read_file($self->{conf}->{whitelist},0,1);
    for (@$lines) {
        s/^\s*//;
        next if /^#/;
        chomp;
        my ($type, $value) = split /\s+/, $_, 2;
        $type =~ y/A-Z/a-z/ if $type;
        push @{$whitelist{$type}}, $value if ($type && $value);
    }
    $self->{whitelist} = \%whitelist;
    $self->log (8,"loaded ". scalar(keys %whitelist) ." different types of whitelist");
    #$self->logobj (15,"loaded whitelist:", \%whitelist);
    return 1;
}


sub logerr {
    my ($self,$msg) = @_;
    $msg = $self->errstr unless $msg;
    $self->log(1,"$self->{breed} error: ". $msg);
    return;
}



# see nextserver() for explanation of how data is stored
#
sub get_server_info {
    my $self = shift;

    unless (exists $self->{s}) { $self->{s} = {}; }

    if ($self->{opt}->{server}) {  # cmd-line
        $self->{s}->{list} = [$self->{opt}->{server}];
        $self->log(8,"Using cmd-line server ($self->{opt}->{server}), skipping .lst files");
    } else {
        $self->readservers;
    }
    $self->loadservercache;
    #$self->logobj(6,"find_closest_server server info (before nextserver)", $self->{s});
    $self->{loaded_servers} = 1;
    return $self->nextserver;  # this will connect and get state info if not cached
}


# see nextserver() for explanation of how data is stored
#
sub readservers {
    my $self = shift;

    unless (exists $self->{s}) { $self->{s} = {}; }

    # read .lst files
    foreach my $lf (qw(discovery nomination catalogue)) {

        my $h = $self->read_file($self->{conf}->{"listfile_$lf"},0,1) or next;
        $self->{s}->{$lf} = [];
        foreach (@$h) {
            push @{$self->{s}->{$lf}}, $1
                if /^(([^\.\s]+\.)+[^\.\s]+(:\S+)?)/;
        }
        if (defined($self->{s}->{$lf}) && ref($self->{s}->{$lf})) {
            $self->log(11,"Read ". scalar(@{$self->{s}->{$lf}}) ." from server listfile: ".
                $self->{conf}->{"listfile_$lf"});
        }
    }
    foreach my $lf (qw(discovery nomination catalogue)) {
        next unless defined($self->{s}->{$lf});
        next unless ref($self->{s}->{$lf});
        next unless @{$self->{s}->{$lf}} > 1;
        fisher_yates_shuffle($self->{s}->{$lf});
    }
    if ($self->{breed} =~ /^check/) {
        $self->{s}->{list} = $self->{s}->{catalogue};
        $self->{s}->{listfile} = $self->{conf}->{listfile_catalogue}; # for discovery()
    } else {
        $self->{s}->{list} = $self->{s}->{nomination};
        $self->{s}->{listfile} = $self->{conf}->{listfile_nomination}; # for discovery()
    }
}

sub loadservercache {
    my $self = shift;

    #
    # Read in server-specific config, using defaults for stuff not found
    #
    # NOTE: this reads all server.*.conf files in razor home, not just those in .lst
    #

    # load defaults for .lst servers
    foreach (qw(nomination catalogue)) {
        next unless $self->{s}->{$_};
        foreach my $server (@{$self->{s}->{$_}}) {
            next if $self->{s}->{allconfs}->{$server};  # avoid repeats
            $self->{s}->{allconfs}->{$server} = $self->default_server_conf();
            $self->log(9,"Assigning defaults to $server");
        }
    }
    my @fns;
    my $sep = '\.';
    $sep = '_' if $^O eq 'VMS';
    if (opendir D,$self->{razorhome}) {
        @fns = map {s/_/./g; "$self->{razorhome}/$_";} grep /^server$sep[\S]+\.conf$/, readdir D;
        @fns = map { /^(\S+)$/, $1 } @fns; # untaint
        closedir D;
    }
    foreach (@fns) {
        /server\.(.+)\.conf$/ and my $sn = $1;
        next unless $sn;
        $self->{s}->{allconfs}->{$sn} = $self->read_file($_, $self->{s}->{allconfs}->{$sn} );
        if ($self->{s}->{allconfs}->{$sn}) {
            #$self->log(8,"Loaded server specific conf info for $sn");
        } else {
            $self->log(5,"loadservercache skipping $_");
        }
    }

    return $self;
}


sub writeservers {
    my $self = shift;

    unless ($self->{razorhome}) {
        $self->log(5,"no razorhome, not caching server info to disk");
        return;
    }

    foreach (@{$self->{s}->{modified_lst}}) {
        my $fn = $self->{conf}->{"listfile_$_"};
        $self->write_file($fn, $self->{s}->{$_}, 0, 0, 1)
            || $self->log(5,"writeservers skipping .lst file: $fn");
    }
    $self->log(11,"No bootstrap_discovery (DNS) recently, not recording .lst files")
        unless scalar (@{$self->{s}->{modified_lst}});
    $self->{s}->{modified_lst} = [];

    foreach (@{$self->{s}->{modified}}) {
        my $fn = "$self->{razorhome}/server.$_.conf";
        my $header = "#\n# Autogenerated by $self->{name_version}, ". localtime() ."\n";
        $self->write_file($fn, $self->{s}->{allconfs}->{$_}, 0, $header)
            || $self->debug("writeservers skipping $fn");
    }
    $self->{s}->{modified} = [];
    $self->errstrrst;  # nuke error string if write errors
    return $self;
}


1;
