amavisd-new-courier.patch   [plain text]


--- amavisd.ori	Tue Nov  2 22:14:31 2004
+++ amavisd	Tue Nov  2 22:16:44 2004
@@ -92,4 +92,5 @@
 #  Amavis::In::AMCL
 #  Amavis::In::SMTP
+#  Amavis::In::Courier
 #  Amavis::AV
 #  Amavis::SpamControl
@@ -123,5 +124,5 @@
   fetch_modules('REQUIRED BASIC MODULES', 1, qw(
     Exporter POSIX Fcntl Socket Errno Carp Time::HiRes
-    IO::Handle IO::File IO::Socket IO::Socket::UNIX IO::Socket::INET
+    IO::Handle IO::File IO::Select IO::Socket IO::Socket::UNIX IO::Socket::INET
     IO::Wrap IO::Stringy Digest::MD5 Unix::Syslog File::Basename File::Copy
     Mail::Field Mail::Address Mail::Header Mail::Internet
@@ -231,5 +232,5 @@
       $myversion $myhostname
       $MYHOME $TEMPBASE $QUARANTINEDIR
-      $daemonize $pid_file $lock_file $db_home
+      $daemonize $courierfilter_shutdown $pid_file $lock_file $db_home
       $enable_db $enable_global_cache
       $daemon_user $daemon_group $daemon_chroot_dir $path
@@ -405,4 +406,7 @@
 $child_timeout = 8*60; # abort child if it does not complete each task in n sec
 
+# Assume STDIN is a courierfilter pipe and shutdown when it becomes readable
+$courierfilter_shutdown = 0;
+
 # Can file be truncated?
 # Set to 1 if 'truncate' works (it is XPG4-UNIX standard feature,
@@ -5300,4 +5304,5 @@
 use Errno qw(ENOENT);
 use IO::File ();
+use IO::Select;
 # body digest for caching, either SHA1 or MD5
 #use Digest::SHA1;
@@ -5339,5 +5344,5 @@
   $extra_code_db $extra_code_cache
   $extra_code_sql $extra_code_ldap
-  $extra_code_in_amcl $extra_code_in_smtp
+  $extra_code_in_amcl $extra_code_in_smtp $extra_code_in_courier
   $extra_code_antivirus $extra_code_antispam $extra_code_unpackers);
 
@@ -5358,5 +5363,6 @@
             @banned_filename @bad_headers);
 
-use vars qw($amcl_in_obj $smtp_in_obj); # Amavis::In::AMCL and In::SMTP objects
+use vars qw($amcl_in_obj $smtp_in_obj $courier_in_obj);
+# Amavis::In::AMCL, In::SMTP and In::Courier objects
 use vars qw($sql_policy $sql_wblist);   # Amavis::Lookup::SQL objects
 use vars qw($ldap_policy);              # Amavis::Lookup::LDAP objects
@@ -5407,4 +5413,5 @@
   do_log(0,"AMCL-in protocol code ".($extra_code_in_amcl?'':" NOT")." loaded");
   do_log(0,"SMTP-in protocol code ".($extra_code_in_smtp?'':" NOT")." loaded");
+  do_log(0,"Courier protocol code ".($extra_code_in_courier?'':" NOT")." loaded");
   do_log(0,"ANTI-VIRUS code       ".($extra_code_antivirus?'':" NOT")." loaded");
   do_log(0,"ANTI-SPAM  code       ".($extra_code_antispam ?'':" NOT")." loaded");
@@ -5426,4 +5433,5 @@
   # Subroutine will be called in scalar context with no arguments.
   # It may return a scalar string (or undef), or an array reference.
+
   %builtins = (
     '.' => undef,
@@ -5592,7 +5600,35 @@
 
 ### Net::Server hook
+### This hook takes place immediately after the "->run()" method is called.
+### This hook allows for setting up the object before any built in configuration
+### takes place.  This allows for custom configurability.
+sub configure_hook {
+  my($self) = @_;
+  if ($courierfilter_shutdown) {
+    # Duplicate the courierfilter pipe to another fd since STDIN is closed if we
+    # daemonize
+    $self->{courierfilter_pipe} = IO::File->new('<&STDIN')
+      or die "Can't duplicate courierfilter shutdown pipe: $!";
+    $self->{courierfilter_select} = IO::Select->new($self->{courierfilter_pipe});
+  }
+}
+
+### Net::Server hook
+### This hook occurs just after the bind process and just before any
+### chrooting, change of user, or change of group occurs.  At this point
+### the process will still be running as the user who started the server.
+sub post_bind_hook {
+  my ($self) = @_;
+  if (c('protocol') eq 'COURIER') {
+    # Allow courier to write to the socket
+    chmod(0660, $unix_socketname);
+  }
+}
+
+### Net::Server hook
 ### This hook occurs after chroot, change of user, and change of group has
 ### occured.  It allows for preparation before looping begins.
 sub pre_loop_hook {
+  my ($self) = @_;
   my($self) = @_;
   local $SIG{CHLD} = 'DEFAULT';
@@ -5631,4 +5667,15 @@
     }
     Amavis::SpamControl::init()  if $extra_code_antispam;
+    if ($courierfilter_shutdown) {
+      # Tell courierfilter we have finished initialisation by closing fd 3
+      # But make sure it's a pipe (and not the courierfilter shutdown pipe)
+      # first: if we have been started using filterctl (i.e. not when
+      # courierfilter itself starts) then there is no initial pipe on fd 3 so
+      # it could be assigned to another file
+      open(my $fh3, '<&3');
+      if (-p $fh3 && $self->{courierfilter_pipe}->fileno() != 3) {
+        POSIX::close(3);
+      }
+    }
   };
   if ($@ ne '') {
@@ -5861,5 +5908,7 @@
     if ($sock->NS_proto eq 'UNIX') {     # traditional amavis helper program
       if ($suggested_protocol eq 'COURIER') {
-        die "unavailable support for protocol: $suggested_protocol";
+        # courierfilter client
+        $courier_in_obj = Amavis::In::Courier->new  if !$courier_in_obj;
+        $courier_in_obj->process_courier_request($sock, $conn, \&check_mail);
       } elsif ($suggested_protocol eq 'AM.PDP') {
         $amcl_in_obj = Amavis::In::AMCL->new  if !$amcl_in_obj;
@@ -5936,4 +5985,14 @@
 }
 
+### Net::Server::PreForkSimple hook
+### Is run by the master process every 10 seconds if $courierfilter_shutdown is set
+sub run_dequeue {
+  my($self) = @_;
+  if ($self->{courierfilter_select}->can_read(0)) {
+    do_log(0, "Instructed by courierfilter to shutdown");
+    $self->server_close();
+  }
+}
+
 ### Child is about to be terminated
 ### user customizable Net::Server hook
@@ -5948,4 +6007,5 @@
   do_log(5,"child_finish_hook: invoking DESTROY methods");
   $smtp_in_obj = undef;  # calls Amavis::In::SMTP::DESTROY
+  $courier_in_obj = undef;  # calls Amavis::In::Courier::DESTROY
   $amcl_in_obj = undef;  # (currently does nothing for Amavis::In::AMCL)
   $sql_wblist = undef;   # calls Amavis::Lookup::SQL::DESTROY
@@ -5961,4 +6021,5 @@
   do_log(5,"at the END handler: invoking DESTROY methods");
   $smtp_in_obj = undef;  # at end calls Amavis::In::SMTP::DESTROY
+  $courier_in_obj = undef;  # at end calls Amavis::In::Courier::DESTROY
   $amcl_in_obj = undef;  # (currently does nothing for Amavis::In::AMCL)
   $sql_wblist = undef;   # at end calls Amavis::Lookup::SQL::DESTROY
@@ -7611,5 +7672,5 @@
     $extra_code_db, $extra_code_cache,
     $extra_code_sql, $extra_code_ldap,
-    $extra_code_in_amcl, $extra_code_in_smtp,
+    $extra_code_in_amcl, $extra_code_in_smtp, $extra_code_in_courier,
     $extra_code_antivirus, $extra_code_antispam, $extra_code_unpackers,
     $Amavis::Conf::log_templ, $Amavis::Conf::log_recip_templ);
@@ -7730,10 +7791,14 @@
 
 if (c('protocol') eq 'COURIER') {
-  die "In::Courier code not available";
+  eval $extra_code_in_courier or die "Problem in the In::Courier code: $@";
+  $extra_code_in_courier = 1; # release memory occupied by the source code
+  $extra_code_in_amcl = undef;
 } elsif (c('protocol') eq 'AM.PDP' || $unix_socketname ne '') {
   eval $extra_code_in_amcl or die "Problem in the In::AMCL code: $@";
   $extra_code_in_amcl = 1;    # release memory occupied by the source code
+  $extra_code_in_courier = undef;
 } else {
   $extra_code_in_amcl = undef;
+  $extra_code_in_courier = undef;
 }
 
@@ -7861,4 +7926,9 @@
     chroot     => $daemon_chroot_dir ne '' ? $daemon_chroot_dir : undef,
     no_close_by_child => 1,
+    
+    # 9 to ensure it runs EVERY 10 seconds
+    # (Net::Server::PreForkSimple only checks every 10 seconds)
+    check_for_dequeue => $courierfilter_shutdown ? 9 : undef,
+    max_dequeue => $courierfilter_shutdown ? 1 : undef,
 
     # controls log level for Net::Server internal log messages:
@@ -10020,4 +10090,223 @@
   }
 }
+
+1;
+
+__DATA__
+#
+package Amavis::In::Courier;
+use strict;
+use re 'taint';
+
+BEGIN {
+  use Exporter ();
+  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
+  $VERSION = '1.15';
+  @ISA = qw(Exporter);
+}
+
+use IO::File;
+use POSIX qw(strftime);
+use Errno qw(ENOENT);
+
+BEGIN {
+  import Amavis::Conf qw(:platform :confvars c cr ca);
+  import Amavis::Util qw(do_log am_id debug_oneshot rmdir_recursively
+                         strip_tempdir untaint);
+  import Amavis::Lookup qw(lookup);
+  import Amavis::Timing qw(section_time);
+  import Amavis::In::Message;
+}
+
+sub new($) {
+  my($class) = @_;
+  my($self) = bless {}, $class;
+  $self->{tempdir_pers} = undef;
+  $self->{tempdir_empty} = 1;
+  $self->{preserve} = 0;
+  return $self;
+}
+
+# Remove the temporary directory, unless we've been asked to preserve it
+sub DESTROY {
+  my($self) = @_;
+  my($errn) = $self->{tempdir_pers} eq '' ? ENOENT
+                : (stat($self->{tempdir_pers}) ? 0 : 0+$!);
+  if (defined $self->{tempdir_pers} && $errn != ENOENT) {
+    # this will not be included in the TIMING report,
+    # but it only occurs infrequently and doesn't take that long
+    if ($self->preserve_evidence && !$self->{tempdir_empty}) {
+      do_log(0, "tempdir is to be PRESERVED: ".$self->{tempdir_pers});
+    } else {
+      do_log(2, "tempdir being removed: ".$self->{tempdir_pers});
+      rmdir_recursively($self->{tempdir_pers});
+    }
+  }
+}
+
+# Accept a single request for virus scanning from courierfilter
+sub process_courier_request($$$) {
+  my($self, $sock, $conn, $check_mail) = @_;
+  # $sock:       connected socket from Net::Server
+  # $conn:       information about client connection
+  # $check_mail: subroutine ref to be called with file handle
+  
+  my($msginfo) = Amavis::In::Message->new;
+  my($fh, $smtp_resp);
+  my($which_section) = "initialization";
+  
+  am_id("$$-$Amavis::child_invocation_count");
+  
+  eval {
+    local $/ = "\n";  # just make sure
+    
+    # Get the path to the data file
+    $which_section = "RX_msgpath";
+    my($msgpath) = scalar(<$sock>);
+    die "$!"  unless defined($msgpath);
+    chomp $msgpath;
+    $msgpath = untaint($msgpath)  if $msgpath =~ m{^[A-Za-z0-9/._=+-]+\z};
+    
+    # Get the control files which contain sender and recipients
+    $which_section = "RX_controlfiles";
+    my(@recips, $sender, $msgid, $ip, $name, $helo);
+    while (<$sock>) {
+      chomp;
+      # courier indicates end of control files by sending a blank line
+      last  unless $_;
+      ($sender, $msgid, $ip, $name, $helo) = read_control_file($_, \@recips);
+      debug_oneshot(1)  if lookup(0, $sender, @{ca('debug_sender_maps')});
+    }
+    $msginfo->sender($sender);
+    $msginfo->recips(\@recips);
+    $msginfo->rx_time(time);
+    $msginfo->client_addr($ip);
+    $msginfo->client_name($name);
+    $msginfo->client_helo($helo);
+    $msginfo->queue_id($msgid);
+    
+    # Open the data file
+    $which_section = "opening_mail_file";
+    $fh = IO::File->new($msgpath, 'r')
+      or die "Can't open $msgpath: $!";
+    binmode($fh, ":bytes")
+      or die "Can't cancel :utf8 mode: $!"  if $unicode_aware;
+    $msginfo->mail_text($fh);
+    $msginfo->mail_text_fn($msgpath);
+    $msginfo->mail_tempdir($TEMPBASE);  # defaults to $TEMPBASE !?
+    section_time('got data');
+    do_log(1, sprintf("Courier <%s> -> %s", $sender,
+		      join(',', map{"<$_>"}@recips)));
+  };
+  
+  if ($@ ne '') { # something went wrong
+    chomp($@);
+    do_log(0, "$which_section FAILED, retry: $@");
+    $fh->close  if $fh;
+    $fh = undef;
+    $msginfo->mail_text(undef);
+    $smtp_resp = '451 Virus checking error';
+  } else {
+    # Get a temporary directory - check_mail needs one
+    $self->prepare_tempdir();
+    
+    # Do the work
+    $self->{tempdir_empty} = 0;
+    my($exit_code, $preserve_evidence);
+    ($smtp_resp, $exit_code, $preserve_evidence) =
+      &$check_mail($conn, $msginfo, 0, $self->{tempdir_pers});
+    if ($preserve_evidence) { $self->preserve_evidence(1) }
+    $fh->close or die "Can't close temp file: $!"  if $fh;
+    $fh = undef;
+    $msginfo->mail_text(undef);
+    
+    # Tidy up
+    if ($self->preserve_evidence) { # Move onto a new temporary directory
+      do_log(0, "PRESERVING EVIDENCE in $self->{tempdir_pers}");
+      $self->{tempdir_pers} = undef;
+    } else { # Clean out the present one and re-use it
+      strip_tempdir($self->{tempdir_pers});
+    }
+    $self->{tempdir_empty} = 1;
+    $self->preserve_evidence(0);
+    
+    if (c('forward_method') eq '' && $smtp_resp =~ /^25/) {
+      # when forwarding is left for MTA on the input side to do,
+      # warn if there is anything that should be done, but MTA is not
+      # capable of doing (or a helper program can not pass the request)
+      my($any_deletes);
+      for my $r (@{$msginfo->per_recip_data}) {
+        my($addr,$newaddr) = ($r->recip_addr, $r->recip_final_addr);
+        if ($r->recip_done) {
+          do_log(0, 
+                 "WARN: recip addr <$addr> should be removed, but MTA can't do it");
+          $any_deletes++;
+        } elsif ($newaddr ne $addr) {
+          do_log(0, "WARN: recip addr <$addr> should be replaced with <$newaddr>, but MTA can't do it");
+        }
+      }
+      if ($any_deletes) {
+        do_log(0, "WARN: REJECT THE WHOLE MESSAGE, MTA-in can't do the recips deletion");
+        $smtp_resp = '550 Redirection failed';
+      }
+    }
+  }
+  
+  do_log(3, "mail checking ended: $smtp_resp");
+  send($sock, $smtp_resp, 0);
+}
+
+# Read the recipients from one control file and pushes them onto the array
+# referenced by the second argument
+# Returns the sender specified by this control file (if any)
+sub read_control_file($$) {
+  my($path, $recips) = @_;
+  my($sender,$queue_id,$ip,$name,$helo);
+  
+  my($fh) = IO::File->new($path, 'r')
+    or die "Can't open control file $path: $!";
+  binmode($fh, ":bytes")
+    or die "Can't cancel :utf8 mode: $!"  if $unicode_aware;
+  
+  # Parse the control file
+  while (<$fh>) {
+    chomp;
+    /^ s ( .*? \@ (?:  \[  (?: \\. | [^\[\]\\] )*  \]
+                   |  [^@"<>\[\]\\\s] )* )
+     \z/xs && ($sender = $1);
+    /^ r ( .*? \@ (?:  \[  (?: \\. | [^\[\]\\] )*  \]
+                   |  [^@"<>\[\]\\\s] )* )
+     \z/xs && push(@$recips, $1);
+    /^ M ( [0-9a-fA-F]+ \. [0-9a-fA-F]+ \. [0-9a-fA-F]+ ) 
+     \z/xs && ($queue_id = $1); 
+    /^ f .*? ; \s* ( [A-Za-z0-9\.-]* ) \s* \( ( [A-Za-z0-9\.-]* )
+     \s* \[ ( [0-9A-Fa-f\.:]+ ) \] \)
+     \z/xs && (($helo, $name, $ip) = ($1, $2, $3));
+  }
+  
+  $fh->close or die "Can't close control file $path: $!";
+  return ($sender,$queue_id,$ip,$name,$helo);
+}
+
+# create ourselves a temporary directory
+sub prepare_tempdir($) {
+  my($self) = @_;
+  if (! defined $self->{tempdir_pers} ) {
+    # invent a name for a temporary directory for this child, and create it
+    my($now_iso8601) = strftime("%Y%m%dT%H%M%S", localtime);
+    $self->{tempdir_pers} = sprintf("%s/amavis-%s-%05d",
+                                    $TEMPBASE, $now_iso8601, $$);
+  }
+  my($errn) = stat($self->{tempdir_pers}) ? 0 : 0+$!;
+  if ($errn == ENOENT || ! -d _) {
+    mkdir($self->{tempdir_pers}, 0750)
+      or die "Can't create directory $self->{tempdir_pers}: $!";
+    $self->{tempdir_empty} = 1;
+    section_time('mkdir tempdir');
+  }
+}
+
+sub preserve_evidence  # try to preserve temporary files etc in case of trouble
+  { my($self)=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift) }
 
 1;
--- amavisd.conf-sample.ori	Tue Nov  2 22:14:52 2004
+++ amavisd.conf-sample	Tue Nov  2 22:15:06 2004
@@ -139,4 +139,11 @@
 #$notify_method = $forward_method;
 
+# COURIER using courierfilter
+#$forward_method = undef;  # no explicit forwarding, Courier does it itself
+#$notify_method = 'pipe:flags=q argv=perl -e $pid=fork();if($pid==-1){exit(75)}elsif($pid==0){exec(@ARGV)}else{exit(0)} /usr/sbin/sendmail -f ${sender} -- ${recipient}';
+# Only set $courierfilter_shutdown to 1 if you are using courierfilter to
+# control the startup and shutdown of amavis
+#$courierfilter_shutdown = 1; # (default 0)
+
 # prefer to collect mail for forwarding as BSMTP files?
 #$forward_method = "bsmtp:$MYHOME/out-%i-%n.bsmtp";
@@ -203,8 +210,10 @@
 			          # (default is true)
 
-# AMAVIS-CLIENT PROTOCOL INPUT SETTINGS (e.g. with sendmail milter)
+# AMAVIS-CLIENT AND COURIER PROTOCOL INPUT SETTINGS (e.g. with sendmail milter)
 #   (used with amavis helper clients like amavis-milter.c and amavis.c,
 #   NOT needed for Postfix or Exim or dual-sendmail - keep it undefined.
 $unix_socketname = "$MYHOME/amavisd.sock"; # amavis helper protocol socket
+#$unix_socketname = "/var/lib/courier/allfilters/amavisd"; # Courier socket
+#$protocol = 'COURIER';           # uncomment if using Courier
 #$unix_socketname = undef;        # disable listening on a unix socket
                                   # (default is undef, i.e. disabled)
@@ -433,5 +442,5 @@
 #   With D_REJECT, MTA may reject original SMTP, or send DSN (delivery status
 #            notification, colloquially called 'bounce') - depending on MTA;
-#            Best suited for sendmail milter, especially for spam.
+#            Best suited for sendmail milter and Courier, especially for spam.
 #   With D_BOUNCE, amavisd-new (not MTA) sends DSN (can better explain the
 #            reason for mail non-delivery or even suppress DSN, but unable
@@ -439,5 +448,5 @@
 #            viruses, and for Postfix and other dual-MTA setups, which can't
 #            reject original client SMTP session, as the mail has already
-#            been enqueued.
+#            been enqueued. Do not use with Courier.
 
 $final_virus_destiny      = D_BOUNCE;  # (defaults to D_DISCARD)
@@ -453,5 +462,5 @@
 # D_BOUNCE is preferred for viruses, but consider:
 # - use D_PASS (or virus_lovers) to deliver viruses;
-# - use D_REJECT instead of D_BOUNCE if using milter and under heavy
+# - use D_REJECT instead of D_BOUNCE if using Courier or milter and under heavy
 #   virus storm;
 #
@@ -941,7 +950,7 @@
 # picture above), and infected mail (if passed) gets additional header:
 #   X-AMaViS-Alert: INFECTED, message contains virus: ...
-# (header not inserted with milter interface!)
+# (header not inserted with Courier or milter interface!)
 #
-# NOTE (milter interface only): in case of multiple recipients,
+# NOTE (Courier and milter interface only): in case of multiple recipients,
 # it is only possible to drop or accept the message in its entirety - for all
 # recipients. If all of them are virus lovers, we'll accept mail, but if