--- amavisd.ori 2011-05-13 19:22:29.277598284 +0200 +++ amavisd 2011-05-13 19:24:45.229961373 +0200 @@ -99,4 +99,5 @@ # Amavis::In::SMTP #( Amavis::In::Courier ) +# Amavis::In::QMQPqq # Amavis::Out::SMTP::Protocol # Amavis::Out::SMTP::Session @@ -3513,4 +3514,5 @@ $myproduct_name, $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 ''; @@ -8547,4 +8549,5 @@ $extra_code_sql_lookup $extra_code_ldap $extra_code_in_amcl $extra_code_in_smtp $extra_code_in_courier + $extra_code_in_qmqpqq $extra_code_out_smtp $extra_code_out_pipe $extra_code_out_bsmtp $extra_code_out_local $extra_code_p0f @@ -8575,4 +8578,5 @@ # Amavis::In::AMCL, Amavis::In::SMTP and In::Courier objects use vars qw($amcl_in_obj $smtp_in_obj $courier_in_obj); +use vars qw($qmqpqq_in_obj); # Amavis::In::QMQPqq object use vars qw($sql_dataset_conn_lookups); # Amavis::Out::SQL::Connection object @@ -9203,4 +9207,5 @@ do_log(0,"SMTP-in proto code %s loaded", $extra_code_in_smtp ?'':" NOT"); do_log(0,"Courier proto code %s loaded", $extra_code_in_courier ?'':" NOT"); + do_log(0,"QMQPqq-in proto code %s loaded", $extra_code_in_qmqpqq ?'':" NOT"); do_log(0,"SMTP-out proto code %s loaded", $extra_code_out_smtp ?'':" NOT"); do_log(0,"Pipe-out proto code %s loaded", $extra_code_out_pipe ?'':" NOT"); @@ -9708,5 +9713,9 @@ die "unavailable support for protocol: $suggested_protocol"; } elsif ($suggested_protocol eq 'QMQPqq') { - die "unavailable support for protocol: $suggested_protocol"; + 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); } elsif ($suggested_protocol eq 'TCP-LOOKUP') { #postfix maps, experimental process_tcp_lookup_request($sock, $conn); @@ -9801,4 +9810,5 @@ do_log(5,"child_finish_hook: invoking DESTROY methods"); undef $smtp_in_obj; undef $amcl_in_obj; undef $courier_in_obj; + undef $qmqpqq_in_obj; undef $sql_storage; undef $sql_wblist; undef $sql_policy; undef $ldap_policy; undef $sql_dataset_conn_lookups; undef $sql_dataset_conn_storage; @@ -9811,4 +9821,5 @@ # do_log(5,"at the END handler: invoking DESTROY methods"); undef $smtp_in_obj; undef $amcl_in_obj; undef $courier_in_obj; + undef $qmqpqq_in_obj; undef $sql_storage; undef $sql_wblist; undef $sql_policy; undef $ldap_policy; undef $sql_dataset_conn_lookups; undef $sql_dataset_conn_storage; @@ -13512,4 +13523,5 @@ $extra_code_sql_lookup, $extra_code_ldap, $extra_code_in_amcl, $extra_code_in_smtp, $extra_code_in_courier, + $extra_code_in_qmqpqq, $extra_code_out_smtp, $extra_code_out_pipe, $extra_code_out_bsmtp, $extra_code_out_local, $extra_code_p0f, @@ -13806,5 +13818,10 @@ undef $extra_code_in_courier; } - if ($needed_protocols_in{'QMQPqq'}) { die "In::QMQPqq code not available" } + if ($needed_protocols_in{'QMQPqq'}) { + 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 + } else { + $extra_code_in_qmqpqq = undef; + } } @@ -17138,4 +17155,276 @@ __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.18'; + @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(ll do_log am_id new_am_id prolong_timer + debug_oneshot sanitize_str rmdir_recursively); + import Amavis::Lookup qw(lookup); + import Amavis::Timing qw(section_time); + import Amavis::rfc2821_2822_Tools; + import Amavis::TempDir; + import Amavis::In::Message; + import Amavis::In::Connection; +} + +sub new($) { + my($class) = @_; + my($self) = bless {}, $class; + $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->{tempdir} = Amavis::TempDir->new; # TempDir object + $self->{session_closed_normally} = undef; # closed properly? (waited for K/Z/D) + $self; +} + +sub DESTROY { + my($self) = shift; + eval { do_log(5,"Amavis::In::QMQPqq DESTROY called, sock=%s, normal=%s", + $self->{sock}, $self->{session_closed_normally}) }; + eval { + if (ref($self->{sock}) && ! $self->{session_closed_normally}) { + $self->qmqpqq_resp("Z","Service shutting down, closing channel"); + } + }; + if ($@ ne '') + { my($eval_stat) = $@; eval { do_log(1,"QMQPqq shutdown: %s",$eval_stat) } } +} + +# 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->{session_closed_normally} = 0; # closed properly? +$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); + +new_am_id(undef, $Amavis::child_invocation_count, undef); +Amavis::Timing::init(); + +$conn->smtp_proto("QMQPqq"); # the name of the method is too specific +my($eval_stat); +eval { + # get length of whole package + $self->{bytesleft} = $self->getlen; + + # get length of 'email' + $len = $self->getlen; + section_time('initial length determination'); + + # prepare tempdir + Amavis::check_mail_begin_task(); + $self->{tempdir}->prepare; + $self->{tempdir}->prepare_file; + + $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->{tempdir}->fh} $_) || + die("Can't write to mail file: $!"); + $len -= $size; + } + if($len > 0) { + die("EOF on socket"); + } + $self->{tempdir}->fh->flush || die("Can't flush mail file: $!"); + $self->{tempdir}->fh->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 (presumably in unquoted form, really???) + $self->getnetstring($sender); + section_time('sender receiving'); + + # get recips (presumably in unquoted form, really???) + 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->sender_smtp(qquote_rfc2821_local($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}, + $msginfo->sender_smtp, + join(',', map { $_->recip_addr_smtp } + @{$msginfo->per_recip_data}), + 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}->path); + $msginfo->mail_text_fn($self->{tempdir}->path . '/email.txt'); + $msginfo->mail_text($self->{tempdir}->fh); + + my($smtp_resp,$exit_code,$preserve_evidence) = + &$check_mail($conn,$msginfo,0); + + if ($preserve_evidence) { $self->{tempdir}->preserve(1) } + + if ($smtp_resp !~ /^4/ && + grep { !$_->recip_done } @{$msginfo->per_recip_data}) { + die("TROUBLE/MISCONFIG: not all recipients done, ". + "\$forward_method is \"$forward_method\""); + } + + if ($smtp_resp !~ /^4/ && + grep { !$_->recip_done } @{$msginfo->per_recip_data}) { + if ($msginfo->delivery_method eq '') { + do_log(2,"not all recipients done, forward_method is empty"); + } + else { + die "TROUBLE: (MISCONFIG) not all recipients done, " . + "forward_method is: " . $msginfo->delivery_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); + } + 1; +} or do { + $eval_stat = $@ ne '' ? $@ : "errno=$!"; +}; + +$self->{tempdir}->clean; +alarm(0); do_log(4,"timer stopped after QMQPqq eval"); + +if($eval_stat ne '') { + chomp $eval_stat; + do_log(0,"QMQPqq: NOTICE: $eval_stat"); + $self->qmqpqq_resp("Z","Service shutting down, $eval_stat"); + } +# report elapsed times by section for each transaction +do_log(2, "%s", 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"); +$self->{sock}->print($self->netstring($code . $resp)); +} + +sub netstring($$) { +my($self,$string) = @_; +return(sprintf("%d:%s,",length($string),$string)); +} + +1; + +__DATA__ +# package Amavis::Out::SMTP::Protocol; use strict; --- amavisd.conf.ori 2011-05-13 19:22:13.915788826 +0200 +++ amavisd.conf 2011-05-13 19:24:45.229961373 +0200 @@ -59,6 +59,6 @@ # option(s) -p overrides $inet_socket_port and $unix_socketname -$inet_socket_port = 10024; # listen on this local TCP port(s) -# $inet_socket_port = [10024,10026]; # listen on multiple TCP ports +$protocol = 'QMQPqq'; # suggested protocol to use on all input sockets +$inet_socket_port = 10628; # accept connections on this local TCP port(s) $policy_bank{'MYNETS'} = { # mail originating from @mynetworks --- amavisd.conf-sample.ori 2011-05-13 19:22:25.123703387 +0200 +++ amavisd.conf-sample 2011-05-13 19:24:45.229961373 +0200 @@ -231,8 +231,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 !!! @@ -2461,8 +2464,14 @@ # ], # }; +# +# $policy_bank{'QMQPqq'} = { +# log_level => 3, +# protocol=>'QMQPqq', # possible with patch amavisd-new-qmqpqq.patch applied +# }; # NOTE: the use of policy banks for changing protocol on the input socket is # only needed when different protocols need to be spoken on different sockets # at the same time. For normal use just set globally e.g.: $protocol='AM.PDP'; +# $interface_policy{'10628'} = 'QMQPqq'; # #$policy_bank{'AM.PDP-SOCK'} = {