#!/usr/bin/perl
#
# exiscan by Tom Kistner <tom@duncanthrax.net>
#
#   Licensed under the GPL. See LICENSE.
#

$version = "2.4";

use IO::Handle;
use IO::Socket::UNIX;
use Socket;
use Unix::Syslog qw(:macros);
use Unix::Syslog qw(:subs); 
use File::Copy;
use Mail::Internet;
use POSIX qw(setsid);

# -------------------------------------------------------------------------
# set STDOUT autoflush
$| = 1;

# -------------------------------------------------------------------------
# read the configuration file
if (!(-e $ARGV[0])) {
  die "Please specify the location of exiscanv2.cf on the command line\n";
};
require "$ARGV[0]" or die "Configuration error, please check exiscanv2.cf\n";

# -------------------------------------------------------------------------
# chdir to base path, so core files will go into the right directory <g>
chdir("$basepath");

# -------------------------------------------------------------------------
# sanity checks
print "Checking for ripmime .. ";
if (!(-x $ripmime)) {
  print "not found. Falling back to reformime.\n";
  $ripmime = 0;
  print "Checking for reformime .. ";
  if (!(-x $reformime)) {
    print "not found ($reformime) ! Please check your settings\n";
    exit(1);
  }
  else {
    print "found, using reformime to unpack MIME mails.\n";
  }
}
else {
  print "found, using ripmime to unpack MIME mails.\n";
};

print "Checking for tnef .. ";
if (!(-x $tnef)) {
  print "not found, will not unpack MS-TNEF format\n";
  $tnef = 0;
} 
else {
  print "found\n";
};

print "Checking for virus scanner executable .. ";
if (!(-x $scannerex)) {
  print "not found ($scannerex) ! Please check your settings\n";
  exit(1);
};
print "found\n";

print "Checking for exim .. ";
if (!(-x $exim)) {
  print "not found ($exim) ! Please check your settings\n";
  exit(1);
};
print "found\n";

# -------------------------------------------------------------------------
# set defaults for some vars 
$queuekids = 5 unless defined ($queuekids);
$scankids = 5 unless defined ($scankids);

# -------------------------------------------------------------------------
# fork the master process
if ($pid = fork) {
  print "exiscan master process launched into the background (pid $pid),\n";
  print "further output via syslog.\n";
  exit(0);
}
elsif (!defined($pid)) {
  die "exiscan can't fork to the background: $!\n";
};

# -------------------------------------------------------------------------
# detach from TTY, become group leader
select undef,undef,undef, 0.3;
setsid();

# -------------------------------------------------------------------------
# the exit handler
# HUP signal makes exiscan quit gracefully
# children will inherit this behaviour
$quit = 0;
sub catch_zap {
       $quit = 1;        
}
$SIG{HUP} = \&catch_zap;


# -------------------------------------------------------------------------
# fork the scanner children
@ps_handle = ();
@cs_handle = ();
for ($i = 0; $i < $scankids; $i++) {
  $cs_handle[$i] = new IO::Handle;
  $ps_handle[$i] = new IO::Handle;
  socketpair($cs_handle[$i], $ps_handle[$i], AF_UNIX, SOCK_STREAM, PF_UNSPEC)
                                       or  die "socketpair: $!";
  $cs_handle[$i]->autoflush(1);
  $ps_handle[$i]->autoflush(1);
  $cs_handle[$i]->blocking(0);
  $ps_handle[$i]->blocking(0);
  if ($pid = fork) {
    close $ps_handle[$i];
  }
  else {
    die "cannot fork: $!" unless defined $pid;
    close $cs_handle[$i];
    &scankid($ps_handle[$i]);
  };
};

# -------------------------------------------------------------------------
# fork the dequeue children
@pq_handle = ();
@cq_handle = ();
for ($i = 0; $i < $queuekids; $i++) {
  $cq_handle[$i] = new IO::Handle;
  $pq_handle[$i] = new IO::Handle;
  socketpair($cq_handle[$i], $pq_handle[$i], AF_UNIX, SOCK_STREAM, PF_UNSPEC)
                                       or  die "socketpair: $!";
  $cq_handle[$i]->autoflush(1);
  $pq_handle[$i]->autoflush(1);
  $cq_handle[$i]->blocking(0);
  $pq_handle[$i]->blocking(0);
  if ($pid = fork) {
    close $pq_handle[$i];
  }
  else {
    die "cannot fork: $!" unless defined $pid;
    close $cq_handle[$i];
    &queuekid($pq_handle[$i]);
  };
};


# -------------------------------------------------------------------------
# set up syslog
use Unix::Syslog qw(:macros);
use Unix::Syslog qw(:subs); 
openlog "exiscanv2", LOG_PID, $facility;
slog(0,"started");
$lastreset = time();

# -------------------------------------------------------------------------
# The superloop
# It is in an eval block to trap unexpected errors. The handler will log
# an error with the mail ID and restart the superloop. Setting $quit to 1
# (via HUP signal) is the only way to exit this superloop cleanly ...
SUPERLOOP:
eval { 
  while(!($quit)) {
    
  if ((time() - $lastreset) > $resetinterval) {
 	  slog(1,"exiscan clearing ID cache");
	  $lastreset = time();
	  %scanned = ();
	  %sender_notified = ();
  };
    
    
  # -----------------------------------------------------------------------
  # get the contents of the queue
  undef %IDs;
  opendir(QUEUEDIR,"$queuedir");
  my @content = grep !/^\./, readdir QUEUEDIR;
  closedir(QUEUEDIR);  
  foreach my $entry (@content) {
    if (-d "$queuedir/$entry") {
      opendir(QUEUEDIR,"$queuedir/$entry");
      my @subids = grep /\-H$/, readdir QUEUEDIR;
      closedir(QUEUEDIR);
      foreach my $subid (@subids) {
        $subid =~ s/\-H$//g;
        $IDs{$subid} = $entry if ( (!($scanned{$subid})) &&
                                   (!(-e "$queuedir/$entry/$subid-J")) );
      };
    }
    elsif ($entry =~ /\-H$/) {
      $entry =~ s/\-H$//g;
      $IDs{$entry} = '.' if ( (!($scanned{$entry})) &&
                               (!(-e "$queuedir/$entry-J")) ) ;
    };
  };

  # -----------------------------------------------------------------------
  # grace time to rule out race conditions
  # and to decrease load when idle
  select undef,undef,undef, $sleepdelay;

  # -----------------------------------------------------------------------
  # poll & set up scanner children
  my @tmpIDs = keys %IDs;
  SCANCHILD: for ($i = 0; $i < $scankids; $i++) {
    
    # ---------------------------------------------------------------------
    # see if scanner children have something to report
    if ($csbusy{$i}) {
      my $id = $cs_handle[$i]->getline();
      if ($id) {
        # -----------------------------------------------------------------
        # get the complete child report
        chop($id);
        $cs_handle[$i]->blocking(1);
        my $from = $cs_handle[$i]->getline();
        chop($from);
        my @to = split / +/, $cs_handle[$i]->getline();
        chop($to[$#to]);
        my $subject = $cs_handle[$i]->getline();
        chop($subject);
        my $result = $cs_handle[$i]->getline();
        chop($result);
        my $remark = $result; $remark =~ s/.+\[(.+)\]$/$1/g;
        if ($result =~ /CLEAN/) {
          # ---------------------------------------------------------------
          # mail clean, add to dequeue chain
          $dequeue{$id} = time();
          if ( ($from eq 'VOID') || ($to[$#to] eq 'VOID') ) {
            slog(1,"$id R:$remark");
          }
          else {
            slog(1,"$id F:$from T:@to R:$remark");
          };
        }
        elsif ($result =~ /ERROR/) {
          # ---------------------------------------------------------------
          # some error occured .. log and continue for now
          slog(0,"$id R:ERROR [$remark]");
        }
        elsif ($result =~ /CONTENT/) {
          # unwanted content found.
          $result =~ s/(\[.+\])//g;
          $ftype = $1 || 'UNKNOWN';
          slog(0,"$id F:$from T:@to R:CONTENT $ftype");
          
          # ---------------------------------------------------------------
          # send notification

          @mail = mk_header($fromaddress,"WARNING! Blocked mail [$subject]", $from);
          push @mail, (eval $content_notification_text);
          push @mail,$notification_footer;
          slog(2,"sending content notification to $from");
          msgsend(\@mail, $from);
        }
        elsif ($result =~ /REGEXP/) {
          # unwanted string found.
          $result =~ s/(\[.+\])//g;
          $fstring = $1 || 'UNKNOWN';
          slog(0,"$id F:$from T:@to R:CONTENT $fstring");
          
          # ---------------------------------------------------------------
          # send notification

          @mail = mk_header($fromaddress,"WARNING! Blocked mail [$subject]", $from);
          push @mail, (eval $regexp_notification_text);
          push @mail,$notification_footer;
          slog(2,"sending regexp notification to $from");
          msgsend(\@mail, $from);
        }
        else {
          # ---------------------------------------------------------------
          # a virus was detected
          slog(0,"$id F:$from T:@to R:INFECTED");
          my $j;
          my @scanneroutput;
          do {
            push @scanneroutput, $cs_handle[$i]->getline();
          } while (!($scanneroutput[$#scanneroutput] =~ /ENDOFSCANNEROUTPUT/));
          pop @scanneroutput;
          
          # ---------------------------------------------------------------
          # send notifications

          # admin
          my @mail;
          if ($admin_notification) {
            @mail = mk_header($fromaddress,"Virus in mail ID $id",$postmaster);
            push @mail, (
                          "Time: ".scalar localtime()."\n",
                          "ID: $id\nFrom: $from\nTo: @to\nSubject: $subject\n\n"
                        );
            push @mail,@scanneroutput;
            push @mail,$notification_footer;
            slog(2,"sending admin notification to $postmaster");
            msgsend(\@mail, $postmaster);
          }

          # sender
          if ( ($sender_notification) &&
             (!($sender_notified{$from})) ) {
            @mail = mk_header($fromaddress,"WARNING! Virus detected",$from);
            push @mail, (eval $sender_notification_text);
            push @mail,$notification_footer;
            slog(2,"sending sender notification to $from");
            msgsend(\@mail, $from);
            $sender_notified{$from} = 1;
          };

          # recipients
          if ($rcpt_notification) {
            RCPTADDR: foreach my $rcpt (@to) {
              next RCPTADDR if (!($rcpt =~ /\@/));
              @mail = mk_header($fromaddress,"NOTIFICATION: Virus stopped",$rcpt);
              push @mail, (eval $rcpt_notification_text);
              push @mail,$notification_footer;
              slog(2,"sending rcpt notification(s) to $rcpt");
              msgsend(\@mail, $rcpt);
            };
          };

        };
        # -----------------------------------------------------------------
        # finished polling the child
        $cs_handle[$i]->blocking(0);
        delete $csbusy{$i};
      }
      else {
        # still busy, continue with next scanner child
        next SCANCHILD;
      };
    };
    
    # ---------------------------------------------------------------------
    # not busy any more? give the child some work!
    if (!($csbusy{$i})) {
      my $id = shift @tmpIDs;
      if ($id) {
        slog(2,"SCAN: Assigning $id to child \#$i");
        $cs_handle[$i]->print($id.'*'.$IDs{$id}."\n");
        $csbusy{$i} = 1;
        $scanned{$id} = 1;
      };
    };
    
  };
  
  
  # -----------------------------------------------------------------------
  # poll & set up dequeue children
  @tmpIDs = sort {$dequeue{$a} cmp  $dequeue{$b}} keys %dequeue;
  DEQUEUECHILD: for ($i = 0; $i < $queuekids; $i++) {
    # ---------------------------------------------------------------------
    # see if queue children have something to report
    if ($cqbusy{$i}) {
      my $id = $cq_handle[$i]->getline();
      if ($id) {
        chop($id);
        $cq_handle[$i]->blocking(1);
        my $result = $cq_handle[$i]->getline();
        chop($result);
        slog(2,"DEQUEUE: child \#$i ($id) returned $result");
        
        # -----------------------------------------------------------------
        # finished polling the child
        $cq_handle[$i]->blocking(0);
        delete $cqbusy{$i};
      }
      else {
        # still busy, continue with next scanner child
        next DEQUEUECHILD;
      };
    };
    
    # ---------------------------------------------------------------------
    # not busy any more? give the queue child some work!
    if (!($cqbusy{$i})) {
      my $id = shift @tmpIDs;
      if ($id) {
        slog(2,"DEQUEUE: Assigning $id to child \#$i");
        $cq_handle[$i]->print($id."\n");
        $cqbusy{$i} = 1;
        delete $dequeue{$id};
      };
    };
  };

  };
};

# -------------------------------------------------------------------------
# If $quit is not 1 then this code got  reached when the superloop eval
# block died unexpectedly. We try to output some meaningful error message
# to the log and simply restart the superloop ..
if (!($quit)) {
  slog("error occured ($@). Last mail ID was $id.");
  goto SUPERLOOP;
};


# -------------------------------------------------------------------------
# the exit routine. gets passed when we shut down with SIGHUP recvd
EXITEXISCAN:
slog(0,"SIGHUP received, terminating.");
closelog;
exit(0);
# *************************************************************************







# *************************************************************************
# *** SUBS start here *****************************************************

# -------------------------------------------------------------------------
# header routine
sub mk_header {
  my $from = shift;
  my $subject = shift;

  my @header;
  push @header, "From: $from\n";
  push @header, "To: @_\n";
  push @header, "Subject: $subject\n";
  my $crypttag = substr(crypt('000000-000000-00',$salt),2,11);
  push @header, "X-Scanner: exiscan \*000000-000000-00\*$crypttag\*$organization";
  push @header, "\n";
  
  return(@header);
};



# -------------------------------------------------------------------------
# syslog routine
sub slog {
  my $level = shift;
  if ($loglevel >= $level) {
    syslog LOG_INFO, "@_";
  };
};


# -------------------------------------------------------------------------
# this is the code run by the scanner child processes
sub scankid {
  
  my $parent = shift;
  # -----------------------------------------------------------------------
  # scanner main loop
  # quit if requested by HUP signal
  TASK: while (!($quit)) {
    # poll parent
    do {
      ($id,$path) = split /\*/,$parent->getline(),2;
      select undef,undef,undef, $sleepdelay;
    } while ( (!($id)) && (!($quit)) );
    exit(0) if ($quit);
    chop($path);
    
    # ---------------------------------------------------------------------
    # quit if requested by parent
    exit(0) if ($id =~ /^QUIT/);
    
    # ---------------------------------------------------------------------
    # check for presence of header file
    if (!(-e "$queuedir/$path/$id-H")) {
      print $parent "$id\nVOID\nVOID\nVOID\nERROR [no header file found, -odb used in local delivery ?]\n";
      next TASK;
    };
    
    # ---------------------------------------------------------------------
    # check for presence of data file
    if (!(-e "$queuedir/$path/$id-D")) {
      print $parent "$id\nVOID\nVOID\nVOID\nERROR [no data file found, -odb used in local delivery ?]\n";
      next TASK;
    };
    
    # ---------------------------------------------------------------------
    # read header and check for X-Scanner line
    open(HEADER,"< $queuedir/$path/$id-H");
    if ((@scanner = grep /X-Scanner\: exiscan/, <HEADER>)) {
      # check if our system was the one scanning it ...
      my ($rest,$oldid,$cryptid,$rest2) = split /\*/, @scanner[$#scanner], 4;
      my $checkid = substr(crypt($oldid,$salt),2,11);
      if ($checkid eq $cryptid) {
        # we did already scan that one, or we sent it ourselves
        print $parent "$id\nVOID\nVOID\nVOID\nCLEAN [resent, delayed or notification]\n";
        close(HEADER);
        next TASK;
      };
    };
    close(HEADER);
    
    open(HEADER,"< $queuedir/$path/$id-H");
    
    # ---------------------------------------------------------------------
    # build $from and @to
    my $from = '<>';
    my @to = ();
    do {
      if ($i =~ /^\</) {
        $from = $i;
        chop($from);
      };    
      if ( ($i =~ /.+\@.+/)  &&
           (!($i =~ /^\</) ) && 
           (!($i =~ /^.. /)) && 
           (!($i =~ /^\-/) )    ) {
        my $tmp = $i; chop($tmp);
        # remove percent signs from email addys
        # this works around a bug in Unix::Syslog
        $tmp =~ s/\%/\*/g;
        # limit @to to 5 addresses
        push @to, $tmp if ($#to < 5);
      };
      $i = <HEADER>;
    } while (!($i eq "\n"));
    if ($#to == 5) {
      push @to, "<max_5_RCPTs_shown>";
    };
    $from =~ s/\%/\*/g;
   
    # ---------------------------------------------------------------------
    # read the rest, and filter Subject if present
    my @mboxheader = ();
    my $subject = '<no_subject>';
    my $headerfrom = '<no_from_line_in_header>';
    while(<HEADER>) {
      # grab the subject if we find one
      if ($_ =~ /Subject\: /i) {
        my $tmp = $_; chop($tmp);
        ($dummy,$subject) = split /Subject\: /i, $tmp, 2;
      };
      
      # grab the header-from
      if ($_ =~ /From\: /i) {
        my $tmp = $_; chop($tmp);
        ($dummy,$headerfrom) = split /From\: /i, $tmp, 2;
      };
      
      # kick out old exiscan X-Scanner lines
      next if ($_ =~ /X-Scanner\: exiscan/);
      $_ =~ s/^.....//g if ($_ =~ /^[0-9][0-9][0-9]/);
      push @mboxheader, $_;
    };
    # add empty line
    push @mboxheader, "\n";
    
    close(HEADER);

    # ---------------------------------------------------------------------
    # do not scan the mail if it is sent to the postmaster address ONLY
    if ( ($#to == 0) && (@to[0] eq $postmaster) ) {
      print $parent "$id\n$from\n@to\n$subject\nCLEAN [postmaster-only mail]\n";
      next TASK;
    };

    # ---------------------------------------------------------------------
    # do not scan the mail if it is sent FROM ourselves
    if ($headerfrom =~ /$fromaddress/i) {
      print $parent "$id\n$from\n@to\n$subject\nCLEAN [notification or postmaster]\n";
      next TASK;
    };

    # ---------------------------------------------------------------------
    # write temporary MBOX file
    open(TMPMAIL,"> $checkdir/$id-complete");
    print TMPMAIL @mboxheader;
    open(BODY,"< $queuedir/$path/$id-D");
    $i = <BODY>;  # kick out 1st line
    while(<BODY>) {
      
      # -------------------------------------------------------------------
      # look for unwanted regular expressions, if configured
      if ($filter_expressions) {
        foreach $reject_regexp (@reject_regexps) {
         if ($_ =~ /$reject_regexp/) {
            # bail out
            close(BODY);
            close(TMPMAIL);
            unlink "$checkdir/$id-complete";
            quarantine($id);
            # remove mail from exim queue
            unlink "$queuedir/$path/$id-H";
            unlink "$queuedir/$path/$id-D";
            print $parent "$id\n$from\n@to\n$subject\nREGEXP [$reject_regexp]\n";
            next TASK;
          };
        };
      };
      print TMPMAIL $_;
    };
    close(BODY);
    close(TMPMAIL);
    
    # ---------------------------------------------------------------------
    # create temp dir, unpack mail
    mkdir "$checkdir/$id-tmp", 0755;
    unmime("$checkdir/$id-complete","$checkdir/$id-tmp");


    # ---------------------------------------------------------------------
    # get a list of files in our unpack directory
    opendir(CHECKDIR,"$checkdir/$id-tmp");
    @extractfiles = grep !/^\./, readdir CHECKDIR;
    closedir(CHECKDIR);

    # ---------------------------------------------------------------------
    # feed winmail.dat (MS-TNEF) to tnef, if configured
    if ($tnef) {
      @mimefiles = grep /winmail/i, @extractfiles;
      foreach $mimefile (@mimefiles) {
        system("$tnef -f $checkdir/$id-tmp/$mimefile -C $checkdir/$id-tmp --overwrite");
      };
    };
    
    # ---------------------------------------------------------------------
    # attempt to unpack SMIME encapsulated messages these will typically
    # come out of MS-TNEF encapsulated messages
    if ($unpack_smime) {
      @mimefiles = grep /smime/i, @extractfiles;
      foreach $mimefile (@mimefiles) {
        unmime("$checkdir/$id-tmp/$mimefile","$checkdir/$id-tmp");
      };
    };
    
    # ---------------------------------------------------------------------
    # check for unwanted content
    if ($filter_extensions) {
      foreach $filename (@extractfiles) {
        foreach $extension (@reject_extensions) {
          if ($filename =~ /($extension)$/i) {
            # we do not like this extension, clean up first
            rdel("$checkdir/$id-tmp");
            unlink "$checkdir/$id-complete";
            quarantine($id);
            # remove mail from exim queue
            unlink "$queuedir/$path/$id-H";
            unlink "$queuedir/$path/$id-D";
            # break
            print $parent "$id\n$from\n@to\n$subject\nCONTENT [$extension]\n";
            next TASK;
          };
        }; 
      };
    };
   
    # ---------------------------------------------------------------------
    # scan the unpacked files ....
    $infected = 0;
    
    if ($virus_scan) {
    
      @scanneroutput = ();
      push @scanneroutput, "\n-=----------= START OF SCANNER OUTPUT =-------------=-\n";
    
      if ($scanner =~ /avpdaemon/i) {
        # AVP Daemon
        my $sock = IO::Socket::UNIX->new( Peer => $scannerex,
                                          Type => SOCK_STREAM );
        print $sock '<0>11 Nov 11:11:11:'.$checkdir.'/'.$id.'-tmp';
        my ($hresult,$lresult);
        $sock->recv($lresult,1);
        $sock->recv($hresult,1);
        $hresult = ord($hresult);
        $lresult = ord($lresult) - 48;
        if ($lresult == 4) {  
          my $bufferlen;
          $sock->recv($bufferlen,4);
          my @temp = split(//,$bufferlen);
          my @mess_length = map(ord, @temp);
          $bufferlen = ( ($mess_length[0]) + 
                         ($mess_length[1]*256) + 
                         ($mess_length[2]*256*256) +
                         ($mess_length[3]*256*256*256) );
          my ($i,$j);
          my $buffer = '';
          for ($i = 0; $i < $bufferlen; $i++) {
            $sock->recv($j,1);
            $buffer .= $j;
          };
          push @scanneroutput, $buffer;
          $infected = 1;
        }
        else {
          $infected = 0;
        };
      }
      else {
        # normal command line scanner
        my $sflags = $scannerflags{$scanner};
        $sflags =~ s/\<DIRECTORY\>/$checkdir\/$id-tmp/g;
        open(SCANNER,"$scannerex $sflags 2>&1 |");
        while(<SCANNER>) {
          if ($_ =~ /$scannerregexp{$scanner}/) {
            $infected = 1;
          };
          push @scanneroutput, $_;
        };
        close(SCANNER);
      };

      push @scanneroutput, "\n-=-----------= END OF SCANNER OUTPUT =--------------=-\n";
    
    };
    
    
    if (!$infected) {
      # -------------------------------------------------------------------
      # add our header tag if not infected
      if (-e "$queuedir/$path/$id-H") {
        $crypttag = substr(crypt($id,$salt),2,11);
        addheader("$queuedir/$path/$id-H","X-Scanner: exiscan \*$id\*$crypttag\*$organization");
        $remark = 'clean, marked for dequeue';
      }
      else {
        # spool file disappeared -> locally submitted mail (?)
        $remark = 'disappeared, -odb used in local delivery ?';
      };
    }
    else {
      quarantine($id);
      # remove mail from exim queue
      unlink "$queuedir/$path/$id-H";
      unlink "$queuedir/$path/$id-D";
      $remark = 'virus detected';
    };
    
    # ---------------------------------------------------------------------
    # clean up our mess ...
    rdel("$checkdir/$id-tmp");
    unlink "$checkdir/$id-complete";
    
    # ---------------------------------------------------------------------
    # send report to the parent
    print $parent "$id\n$from\n@to\n$subject\n";
    if ($infected) {
      print $parent "INFECTED [$remark]\n";
      print $parent @scanneroutput;
      print $parent "\nENDOFSCANNEROUTPUT\n";
    }
    else {
      print $parent "CLEAN [$remark]\n";
    };
  };
  
  exit(0);
};

# -------------------------------------------------------------------------
# this is the code run by the dequeue child processes
sub queuekid {
  
  my $parent = shift;
  # -----------------------------------------------------------------------
  # dequeue main loop
  # quit if requested by HUP signal
  TASK: while (!($quit)) {
    # poll parent
    do {
      $id = $parent->getline();
      select undef,undef,undef, $sleepdelay;
    } while ( (!($id)) && (!($quit)) );
    exit(0) if ($quit);
    chop($id);
    
    # ---------------------------------------------------------------------
    # quit if requested by parent
    exit(0) if ($id =~ /^QUIT/);
    
    # ---------------------------------------------------------------------
    # run exim dequeue process
    my $result = system("$exim -Mc $id");

    # ---------------------------------------------------------------------
    # report to parent
    print $parent "$id\n";
    print $parent "$result\n";
    
  };
  
  exit(0);
};

# -------------------------------------------------------------------------
# unmime - wrapper
sub unmime {
  my $filename = shift;
  my $dirname = shift;
  if ($ripmime) {
    system("$ripmime -i $filename -d $dirname --stderr_off");
  }
  elsif ($reformime) {
    chdir("$dirname");
    system("$reformime -xf- < $filename");
    chdir("$basepath");
  };
};

# -------------------------------------------------------------------------
# rdel - recursive directory deletion
sub rdel {
  my $cand = shift;
  
  # some paranoia ....
  return 0 unless ($cand =~ /$checkdir/);
  
  # get files
  opendir(MYDIR,"$cand");
  my @entries = grep !/^\.+$/, readdir MYDIR;
  foreach my $entry (@entries) {
    # recursion
    if (-d "$entry") {
      rdel("$cand/$entry");
    };
    # delete file
    unlink "$cand/$entry";
  };
  # delete dir
  rmdir "$cand";
  return(1);
};


# -------------------------------------------------------------------------
# msgsend - send notification

sub msgsend {
  my $msgref = shift;
  my $rcpt = shift;
  my $msg = new Mail::Internet($msgref);
  if ($smtphost eq 'local') {
    $msg->send();
  }
  else {
    $msg->smtpsend( Host=>$smtphost,
                    Port=>25,
                    Hello=>'exiscan',
                    From=>$fromaddress,
                    To=>$rcpt
                  );
  };
};



# -------------------------------------------------------------------
# quarantine - move unwanted emails to "jail"

sub quarantine {
  my $id = shift;
  copy("$queuedir/$path/$id-H","$virusdir/$id-H");
  copy("$queuedir/$path/$id-D","$virusdir/$id-D");
  copy("$checkdir/$id-complete","$virusdir/$id-complete");
  # chmod to 600
  chmod 0600,"$virusdir/$id-H","$virusdir/$id-D","$virusdir/$id-complete";
};


# -------------------------------------------------------------------
# addheader - add a line to an exim spool file header

sub addheader {
  my $file = shift;
  my $string = shift;
  my $pstring = sprintf("%03d  ",(length($string) + 1)).$string."\n";
  open(HEADER,">> $file");
  print HEADER $pstring;
  close(HEADER);
};
