#
#   MailScanner - SMTP E-Mail Virus Scanner
#   Copyright (C) 2002  Julian Field
#
#   $Id: GenericSpam.pm 3116 2005-07-11 20:16:13Z jkf $
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#   The author, Julian Field, can be contacted by email at
#      Jules@JulianField.net
#   or by paper mail at
#      Julian Field
#      Dept of Electronics & Computer Science
#      University of Southampton
#      Southampton
#      SO17 1BJ
#      United Kingdom
#

package MailScanner::GenericSpam;

use strict 'vars';
use strict 'refs';
no  strict 'subs'; # Allow bare words for parameter %'s
#use English; # Needed for $PERL_VERSION to work in all versions of Perl

use IO;
use POSIX qw(:signal_h); # For Solaris 9 SIG bug workaround
# Don't do this any more as SpamAssassin prefers to do it itself
# use AnyDBM_File; # Doing this here keeps SpamAssassin quiet

use vars qw($VERSION);

### The package version, both in 1.23 style *and* usable by MakeMaker:
$VERSION = substr q$Revision: 3116 $, 10;

# Attributes are
#
#

my @GSsuccessqueue;      # queue of failure history
my $GSsuccessqsum;       # current sum of history queue

sub initialise {

  # Initialise the class variables
  @GSsuccessqueue = ();
  $GSsuccessqsum  = 0;
}


# Constructor.
sub new {
  my $type = shift;
  my $this = {};

  bless $this, $type;
  return $this;
}

# Do the Generic Spam checks on the passed in message
sub Checks {
  my($message) = @_;

  my(@WholeMessage, $scanner, $maxsize);

  #print STDERR "Doing Generic Spam Checks\n";
  # If they aren't using the generic spam scanner, bail out
  $scanner = MailScanner::Config::Value('gsscanner', $message);
  #return 0 unless $scanner;

  # Bail out and fake a miss if too many consecutive GS checks failed
  my $maxfailures = MailScanner::Config::Value('maxgstimeouts');

  # If we get maxfailures consecutive timeouts, then disable the
  # SpamAssassin RBL checks in an attempt to get it working again.
  # If it continues to time out for another maxfailures consecutive
  # attempts, then disable it completely.
  if ($maxfailures>0 && $GSsuccessqsum>=$maxfailures) {
      return (0,0,
        sprintf(MailScanner::Config::LanguageValue($message,'gsdisabled'),
        $maxfailures), 0);
  }

  $maxsize = MailScanner::Config::Value('maxgssize');

  push(@WholeMessage, $global::MS->{mta}->OriginalMsgHeaders($message, "\n"));
  #print STDERR "Headers are : " . join(', ', @WholeMessage) . "\n";
  push(@WholeMessage, "\n");
  $message->{store}->ReadBody(\@WholeMessage, $maxsize);

  my($GenericSpamResult, $GenericSpamReport);
  $GenericSpamResult = 0;
  $GenericSpamReport = "";
  ($GenericSpamResult, $GenericSpamReport) =
    GSForkAndTest($message, \@WholeMessage);
  return ($GenericSpamResult, $GenericSpamReport);
}

# Run the generic spam scanner, and capture the 2 lines of output
sub GSForkAndTest {
  my($Message, $Contents) = @_;

  my($pipe, $gsscore, $gsreport, $queuelength);
  my $PipeReturn = 0;

  $queuelength = MailScanner::Config::Value('gstimeoutlen', $Message);

  $pipe = new IO::Pipe
    or MailScanner::Log::DieLog('Failed to create pipe, %s, try reducing ' .
                  'the maximum number of unscanned messages per batch', $!);

  my $pid = fork();
  die "Can't fork: $!" unless defined($pid);

  if ($pid == 0) {
    # In the child
    $pipe->writer();
    $pipe->autoflush();
    my($gsscore, $gsreport);
    eval {
      #print STDERR "ClientIP = " . $Message->{clientip} . "\n";
      #print STDERR "From = " . $Message->{from} . "\n";
      #print STDERR "To = " . join(', ', @{$Message->{to}}) . "\n";
      #print STDERR "This is in the caller\n";

      ($gsscore, $gsreport) = MailScanner::CustomConfig::GenericSpamScanner(
       $Message->{clientip},
       $Message->{from},
       $Message->{to},
       $Contents);
    };
    
    $gsscore = $gsscore + 0.0;
    print $pipe "$gsscore\n";
    print $pipe $gsreport . "\n";
    $pipe->close();
    $pipe = undef;
    exit 0;
  }

  eval {
    $pipe->reader();
    local $SIG{ALRM} = sub { die "Command Timed Out" };
    alarm MailScanner::Config::Value('gstimeout');
    $gsscore = <$pipe>;
    $gsreport = <$pipe>;

    # Not sure if next 2 lines should be this way round...
    waitpid $pid, 0;
    $pipe->close();
    $PipeReturn = $?;
    alarm 0;
    $pid = 0;
    chomp $gsscore;
    chomp $gsreport;
    $gsscore = $gsscore + 0.0;
    # We got a result so store a success
    push @GSsuccessqueue, 0;
    # Roll the queue along one
    $GSsuccessqsum += (shift @GSsuccessqueue)?1:-1
          if @GSsuccessqueue>$queuelength;
    #print STDERR "Success: sum = $GSsuccessqsum\n";
    $GSsuccessqsum = 0 if $GSsuccessqsum<0;
  };
  alarm 0;
  # Workaround for bug in perl shipped with Solaris 9,
  # it doesn't unblock the SIGALRM after handling it.
  eval {
    my $unblockset = POSIX::SigSet->new(SIGALRM);
    sigprocmask(SIG_UNBLOCK, $unblockset)
      or die "Could not unblock alarm: $!\n";
  };

  # Note to self: I only close the KID in the parent, not in the child.

  # Catch failures other than the alarm
  MailScanner::Log::DieLog("Generic Spam Scanner failed with real error: $@")
    if $@ and $@ !~ /Command Timed Out/;

  # In which case any failures must be the alarm
  #if ($@ or $pid>0) {
  if ($pid>0) {
    my $maxfailures = MailScanner::Config::Value('maxgstimeouts');
    # Increment the "consecutive" counter
    #$safailures++;
    if ($maxfailures>0) {
      # We got a failure
      push @GSsuccessqueue, 1;
      $GSsuccessqsum++;
      # Roll the queue along one
      $GSsuccessqsum += (shift @GSsuccessqueue)?1:-1
        if @GSsuccessqueue>$queuelength;
      #print STDERR "Failure: sum = $GSsuccessqsum\n";
      $GSsuccessqsum = 0 if $GSsuccessqsum<0;

      if ($GSsuccessqsum>$maxfailures && @GSsuccessqueue>=$queuelength) {
        MailScanner::Log::WarnLog("Generic Spam Scanner timed out and was" .
          " killed, failure %d of %d", $GSsuccessqsum, $maxfailures);
      }
    } else {
      MailScanner::Log::WarnLog("Generic Spam Scanner timed out and was killed");
    }

    # Make the report say GS was killed
    $gsreport = MailScanner::Config::LanguageValue($Message, 'gstimedout');

    # Kill the running child process
    my($i);
    kill 15, $pid; # Was -15
    # Wait for up to 10 seconds for it to die
    for ($i=0; $i<5; $i++) {
      sleep 1;
      waitpid($pid, &POSIX::WNOHANG);
      ($pid=0),last unless kill(0, $pid);
      kill 15, $pid; # Was -15
    }
    # And if it didn't respond to 11 nice kills, we kill -9 it
    if ($pid) {
      kill 9, $pid; # Was -9
      waitpid $pid, 0; # 2.53
    }

  }
  #MailScanner::Log::WarnLog("8 PID is $pid");

  # Generic Spam Scanner may play with the umask
  umask 0077; # Safety net

  # The return from the pipe is a measure of how spammy it was
  MailScanner::Log::DebugLog("Generic Spam Scanner returned $PipeReturn");

  # The Generic Spam Scanner returned something interesting
  #print STDERR "Generic Spam Scanner points = $gsscore\n";
  #print STDERR "Generic Spam Scanner report = $gsreport\n";

  return ($gsscore, $gsreport);
}

1;

