--- amavisd.ori Tue Nov 2 22:14:31 2004 +++ amavisd Tue Nov 2 22:20:27 2004 @@ -92,4 +92,5 @@ # Amavis::In::AMCL # Amavis::In::SMTP +# Amavis::In::QMQPqq # Amavis::AV # Amavis::SpamControl @@ -1788,4 +1789,5 @@ : sprintf(" (%s [%s])", $myhostname, $conn->socket_ip) ), ($conn->socket_port eq '' ? 'unix socket' : "port ".$conn->socket_port) ); + # must not use proto name QMQPqq in 'with' $s .= "\n with $smtp_proto" if $smtp_proto=~/^(ES|S|L)MTPS?A?\z/i; # rfc3848 $s .= "\n id $id" if $id ne ''; @@ -5339,5 +5341,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_qmqpqq $extra_code_antivirus $extra_code_antispam $extra_code_unpackers); @@ -5359,4 +5361,5 @@ use vars qw($amcl_in_obj $smtp_in_obj); # Amavis::In::AMCL and In::SMTP objects +use vars qw($qmqpqq_in_obj); # Amavis::In::QMQPqq object use vars qw($sql_policy $sql_wblist); # Amavis::Lookup::SQL objects use vars qw($ldap_policy); # Amavis::Lookup::LDAP objects @@ -5407,4 +5410,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,"QMQPqq-in protocol code ".($extra_code_in_qmqpqq?'':" NOT")." loaded"); do_log(0,"ANTI-VIRUS code ".($extra_code_antivirus?'':" NOT")." loaded"); do_log(0,"ANTI-SPAM code ".($extra_code_antispam ?'':" NOT")." loaded"); @@ -5880,4 +5884,10 @@ $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj; $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0); + } elsif ($suggested_protocol eq 'QMQPqq') { + if (!$extra_code_in_qmqpqq) { + die "incoming TCP connection, but dynamic QMQPqq code not loaded"; + } + $qmqpqq_in_obj = Amavis::In::QMQPqq->new if !$qmqpqq_in_obj; + $qmqpqq_in_obj->process_qmqpqq_request($sock,$conn,\&check_mail); } else { # defaults to SMTP or LMTP if (!$extra_code_in_smtp) { @@ -5948,4 +5958,5 @@ do_log(5,"child_finish_hook: invoking DESTROY methods"); $smtp_in_obj = undef; # calls Amavis::In::SMTP::DESTROY + $qmqpqq_in_obj = undef; # calls Amavis::In::QMQPqq::DESTROY $amcl_in_obj = undef; # (currently does nothing for Amavis::In::AMCL) $sql_wblist = undef; # calls Amavis::Lookup::SQL::DESTROY @@ -5961,4 +5972,5 @@ do_log(5,"at the END handler: invoking DESTROY methods"); $smtp_in_obj = undef; # at end calls Amavis::In::SMTP::DESTROY + $qmqpqq_in_obj = undef; # at end calls Amavis::In::QMQPqq::DESTROY $amcl_in_obj = undef; # (currently does nothing for Amavis::In::AMCL) $sql_wblist = undef; # at end calls Amavis::Lookup::SQL::DESTROY @@ -7611,5 +7623,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_qmqpqq, $extra_code_antivirus, $extra_code_antispam, $extra_code_unpackers, $Amavis::Conf::log_templ, $Amavis::Conf::log_recip_templ); @@ -7739,5 +7751,7 @@ if (c('protocol') eq 'QMQPqq') { # simpleminded, not checking all policy banks - die "In::QMQPqq code not available"; + eval $extra_code_in_qmqpqq or die "Problem in the In::QMQPqq code: $@"; + $extra_code_in_qmqpqq = 1; # release memory occupied by the source code + $extra_code_in_smtp = undef; } elsif (c('protocol') =~ /^(SMTP|LMTP)\z/ || $inet_socket_port ne '' && @@ -7745,6 +7759,8 @@ eval $extra_code_in_smtp or die "Problem in the In::SMTP code: $@"; $extra_code_in_smtp = 1; # release memory occupied by the source code + $extra_code_in_qmqpqq = undef; } else { $extra_code_in_smtp = undef; + $extra_code_in_qmqpqq = undef; } @@ -10019,4 +10035,331 @@ $stat or die "Error writing a SMTP response to the socket: $!"; } +} + +1; + +__DATA__ +# +package Amavis::In::QMQPqq; +use strict; +# use re 'taint'; # (is this module ready for this yet?) + +BEGIN { + use Exporter (); + use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); + $VERSION = '1.17'; + @ISA = qw(Exporter); +} +use POSIX qw(strftime); +use Errno qw(ENOENT); + +BEGIN { + import Amavis::Conf qw(:platform :confvars :dynamic_confvars c cr ca); + import Amavis::Util qw(do_log am_id prolong_timer debug_oneshot + untaint sanitize_str strip_tempdir rmdir_recursively); + import Amavis::Lookup qw(lookup); + import Amavis::Timing qw(section_time); + import Amavis::rfc2821_2822_Tools; + import Amavis::In::Message; + import Amavis::In::Connection; +} + +sub new($) { + my($class) = @_; + my($self) = bless {}, $class; + $self->{fh_pers} = undef; # persistent file handle for email.txt + $self->{tempdir_pers} = undef; # temporary directory for check_mail + $self->{preserve} = undef; # don't delete tempdir on exit + $self->{tempdir_empty} = 1; # anything of interest in tempdir? + $self->{bytesleft} = undef; # bytes left for whole package + $self->{len} = undef; # set by getlen() method + $self->{sock} = undef; # connected socket + $self->{proto} = undef; # protocol + $self->{session_closed_normally} = undef; # closed properly? (waited for K/Z/D) + $self; +} + +sub preserve_evidence # try to preserve temporary files etc in case of trouble + { my($self)=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift) } + +sub DESTROY { + my($self) = shift; +# do_log(0, "Amavis::In::QMQPqq::DESTROY called"); + $self->{fh_pers}->close + or die "Can't close temp file: $!" if $self->{fh_pers}; + 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}); + } + } + if (! $self->{session_closed_normally}) { + $self->qmqpqq_resp("Z","Service shutting down, closing channel"); + } +} + +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'); + } + # prepare temporary file for writing (and reading later) + my($fname) = $self->{tempdir_pers} . "/email.txt"; + my($errn) = stat($fname) ? 0 : 0+$!; + if ($self->{fh_pers} && !$errn && -f _) { + $self->{fh_pers}->seek(0,0) or die "Can't rewind mail file: $!"; + $self->{fh_pers}->truncate(0) or die "Can't truncate mail file: $!"; + } else { + $self->{fh_pers} = IO::File->new($fname, 'w+', 0640) + or die "Can't create file $fname: $!"; + section_time('create email.txt'); + } +} + + +# get byte, die if no bytes left +sub getbyte($) { +my($self) = shift; +if(!$self->{bytesleft}--) { + die("No bytes left"); + } +if(defined($_ = $self->{sock}->getc)) { + return($_); + } +die("EOF on socket"); +} + +sub getlen($) { +my($self) = shift; +my($ch,$len); + +for(;;) { + $ch = $self->getbyte; + if($ch eq ':') { + return($self->{len} = $len); + } + if($ch !~ /^\d$/) { + die("Char '$ch' is not a number while determining length"); + } + $len .= $ch; + } +} + +sub getcomma($) { +my($self) = shift; +if($self->getbyte ne ',') { + die("Comma expected, found '$_'"); + } +} + +sub getnetstring($$) { +my($self) = shift; +($self->{sock}->read($_[0],$self->getlen) == $self->{len}) || + die("EOF on socket"); +$self->{bytesleft} -= $self->{len}; +$self->getcomma; +} + + +# Accept a QMQPqq connect +# and call content checking for the message received +# +sub process_qmqpqq_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 + +$self->{proto} = "QMQPqq"; +$self->{sock} = $sock; # store $sock info for getbyte() method +$self->{bytesleft} = 20; # initial bytesleft value, there should + # NEVER EVER be longer email than 10^20 (approximately) + # bytes but increase if needed ;) +$self->{len} = undef; + +my($msginfo); + +my($sender,@recips); + +my($len); + +$conn->smtp_proto("QMQPqq"); # the name of the method is too specific +eval { + # get length of whole package + $self->{bytesleft} = $self->getlen; + + # get length of 'email' + $len = $self->getlen; + section_time('initial length determination'); + + am_id(sprintf("%05d-%02d",$$,$Amavis::child_invocation_count)); + + # prepare tempdir + $self->prepare_tempdir; + $msginfo = Amavis::In::Message->new; + $msginfo->rx_time(time); + $msginfo->delivery_method(c('forward_method')); + + # get 'email' + $self->{tempdir_empty} = 0; + my $size = 16384; + while(($len > 0) && ($sock->read($_,($len >= $size ? $size : $size = $len)) == $size)) { + (print {$self->{fh_pers}} $_) || + die("Can't write to mail file: $!"); + $len -= $size; + } + if($len > 0) { + die("EOF on socket"); + } + $self->{fh_pers}->flush || die("Can't flush mail file: $!"); + $self->{fh_pers}->seek(0,1) || die("Can't seek on file: $!"); + $self->{bytesleft} -= $self->{len}; + section_time('email receiving'); + # comma has to follow + $self->getcomma; + + # get sender + $self->getnetstring($sender); + section_time('sender receiving'); + + # get recips + my $i = 0; + while($self->{bytesleft}) { + $self->getnetstring($recips[$i++]); + } + section_time('recips receiving'); + + # final comma has to follow + $self->{bytesleft} = 1; + $self->getcomma; + + $msginfo->sender($sender); + $msginfo->recips(\@recips); + + do_log(1, sprintf("%s:%s:%s %s: <%s> -> %s Received: %s", + $self->{proto},$conn->socket_ip eq $inet_socket_bind ? + '' : '['.$conn->socket_ip.']', + $conn->socket_port, $self->{tempdir_pers}, + $sender, join(',', map{"<$_>"}@recips), + join(' ', + ($msginfo->msg_size eq '' ? () + : 'SIZE='.$msginfo->msg_size), + ($msginfo->body_type eq '' ? () + : 'BODY='.$msginfo->body_type), + received_line($conn,$msginfo,am_id(),0) ) + )); + + $msginfo->mail_tempdir($self->{tempdir_pers}); + $msginfo->mail_text_fn($self->{tempdir_pers} . '/email.txt'); + $msginfo->mail_text($self->{fh_pers}); + + my($smtp_resp,$exit_code,$preserve_evidence) = + &$check_mail($conn,$msginfo,0,$self->{tempdir_pers}); + + if ($preserve_evidence) { + $self->preserve_evidence(1); + } + if ($smtp_resp !~ /^4/ && + grep { !$_->recip_done } @{$msginfo->per_recip_data}) { + die("TROUBLE/MISCONFIG: not all recipients done, ". + "\$forward_method is \"$forward_method\""); + } + + # all ok + if($smtp_resp =~ /^2/) { + $self->qmqpqq_resp("K",$smtp_resp); + } + # permanent reject + elsif($smtp_resp =~ /^5/) { + $self->qmqpqq_resp("D",$smtp_resp); + } + # temporary reject (or other error if !~ /^4/) + else { + $self->qmqpqq_resp("Z",$smtp_resp); + } + }; + +alarm(0); do_log(5,"timer stopped after QMQPqq eval"); + +if($@ ne '') { + chomp($@); + + do_log(0,"QMQPqq: NOTICE: $@"); + $self->qmqpqq_resp("Z","Service shutting down, $@"); + } + +if ($self->preserve_evidence && !$self->{tempdir_empty}) { + # keep evidence in case of trouble + do_log(0,"PRESERVING EVIDENCE in ".$self->{tempdir_pers}); + $self->{fh_pers}->close or die "Can't close mail file: $!"; + $self->{fh_pers} = undef; $self->{tempdir_pers} = undef; + $self->{tempdir_empty} = 1; + } + +# cleanup, but leave directory (and file handle +# if possible) for reuse +if ($self->{fh_pers} && !$can_truncate) { + # truncate is not standard across all Unix variants, + # it is not Posix, but is XPG4-UNIX. + # So if we can't truncate a file and leave it open, + # we have to create it anew later, at some cost. + # + $self->{fh_pers}->close or die "Can't close mail file: $!"; + $self->{fh_pers} = undef; + unlink($self->{tempdir_pers}."/email.txt") + or die "Can't delete file ". + $self->{tempdir_pers}."/email.txt: $!"; + section_time('delete email.txt'); + } + +if (defined $self->{tempdir_pers}) { # prepare for the next one + strip_tempdir($self->{tempdir_pers}); + $self->{tempdir_empty} = 1; + } + +$self->preserve_evidence(0); # reset +# report elapsed times by section for each transaction +do_log(2, Amavis::Timing::report()); + +$self->{session_closed_normally} = 1; +# closes connection after child_finish_hook +} + +# sends a QMQPqq response consisting of K/D/Z code and an optional message; +# slow down evil clients by delaying response on permanent errors +sub qmqpqq_resp($$$;$$) { +my($self,$code,$resp,$penalize,$line) = @_; +if($code !~ /^(K|Z|D)$/) { + die("Internal error(2): bad QMQPqq response code: '$code'"); + } +if($penalize) { + do_log(0,"QMQPqq: $resp; PENALIZE: $line"); + sleep 5; + section_time('QMQPqq penalty wait'); + } +$resp = sanitize_str($resp,1); +do_log(4,"QMQPqq> $resp"); +print($self->netstring($code . $resp)); +} + +sub netstring($$) { +my($self,$string) = @_; +return(sprintf("%d:%s,",length($string),$string)); } --- amavisd.conf.ori Tue Nov 2 22:14:36 2004 +++ amavisd.conf Tue Nov 2 22:20:27 2004 @@ -42,5 +42,6 @@ $enable_global_cache = 1; # enable use of libdb-based cache if $enable_db=1 -$inet_socket_port = 10024; # listen on this local TCP port(s) (see $protocol) +$protocol = 'QMQPqq'; # suggested protocol to use on all input sockets +$inet_socket_port = 10628; # accept connections on this local TCP port(s) # $unix_socketname = "$MYHOME/amavisd.sock"; # when using sendmail milter --- amavisd.conf-sample.ori Tue Nov 2 22:14:52 2004 +++ amavisd.conf-sample Tue Nov 2 22:20:27 2004 @@ -213,8 +213,11 @@ # SMTP SERVER (INPUT) PROTOCOL SETTINGS (e.g. with Postfix, Exim v4, ...) # (used when MTA is configured to pass mail to amavisd via SMTP or LMTP) -$inet_socket_port = 10024; # accept SMTP on this local TCP port +#$inet_socket_port = 10024; # accept connection on this local TCP port # (default is undef, i.e. disabled) # multiple ports may be provided: $inet_socket_port = [10024, 10026, 10028]; +$protocol = 'QMQPqq'; # suggested protocol to use on all input sockets +$inet_socket_port = 10628; # accept connections on this local TCP port(s) + # SMTP SERVER (INPUT) access control # - do not allow free access to the amavisd SMTP port !!! @@ -1970,4 +1973,5 @@ # ], # }; +# # NOTE: the use of policy banks for changing protocol on the input socket is @@ -1979,4 +1983,8 @@ # protocol=>'AM.PDP', # Amavis policy delegation protocol (new milter helper) # }; +# $policy_bank{'QMQPqq'} = { +# log_level => 3, +# protocol=>'QMQPqq', # possible with patch amavisd-new-qmqpqq.patch applied +# }; ## the name 'MYNETS' has special semantics: this policy bank gets loaded @@ -2001,4 +2009,5 @@ # $interface_policy{'10026'} = 'ALT'; +# $interface_policy{'10628'} = 'QMQPqq'; # $interface_policy{'9998'} = 'AM.PDP'; # $interface_policy{'SOCK'} = 'AM.PDP';