--- 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