File amavisd-qmqpqq-local.patch of Package amavisd-new
--- amavisd-new-2.11.1/amavisd.orig 2019-08-04 21:46:50.260220851 +0200
+++ amavisd-new-2.11.1/amavisd 2019-08-04 21:45:47.024815454 +0200
@@ -108,6 +108,7 @@
# Amavis::In::AMPDP
# Amavis::In::SMTP
#( Amavis::In::Courier )
+# Amavis::In::QMQPqq
# Amavis::Out::SMTP::Protocol
# Amavis::Out::SMTP::Session
# Amavis::Out::SMTP
@@ -12113,6 +12114,7 @@
$extra_code_sql_base $extra_code_sql_log $extra_code_sql_quar
$extra_code_sql_lookup $extra_code_ldap
$extra_code_in_ampdp $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
$extra_code_antivirus $extra_code_antispam
@@ -12140,6 +12142,7 @@
# Amavis::In::AMPDP, Amavis::In::SMTP and In::Courier objects
use vars qw($ampdp_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
use vars qw($sql_dataset_conn_storage); # Amavis::Out::SQL::Connection object
@@ -12958,6 +12961,7 @@
do_log(1,"AM.PDP-in proto code%s loaded", $extra_code_in_ampdp ?'':" NOT");
do_log(1,"SMTP-in proto code %s loaded", $extra_code_in_smtp ?'':" NOT");
do_log(1,"Courier proto code %s loaded", $extra_code_in_courier ?'':" NOT");
+ do_log(1,"QMQPqq-in proto code%s loaded", $extra_code_in_qmqpqq ?'':" NOT");
do_log(1,"SMTP-out proto code %s loaded", $extra_code_out_smtp ?'':" NOT");
do_log(1,"Pipe-out proto code %s loaded", $extra_code_out_pipe ?'':" NOT");
do_log(1,"BSMTP-out proto code%s loaded", $extra_code_out_bsmtp ?'':" NOT");
@@ -13691,7 +13695,11 @@
} elsif ($suggested_protocol eq 'COURIER') {
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);
do_log(2, "%s", Amavis::Timing::report()); # report elapsed times
@@ -13721,6 +13729,7 @@
do_log(-1, "Requesting process rundown after fatal error");
}
undef $smtp_in_obj; undef $ampdp_in_obj; undef $courier_in_obj;
+ undef $qmqpqq_in_obj;
$self->done(1);
} elsif (defined $max_requests && $max_requests > 0 &&
$child_task_count >= $max_requests) {
@@ -13729,10 +13738,12 @@
do_log(2, "Requesting process rundown after %d tasks (and %s sessions)",
$child_task_count, $child_invocation_count);
undef $smtp_in_obj; undef $ampdp_in_obj; undef $courier_in_obj;
+ undef $qmqpqq_in_obj;
$self->done(1);
} elsif ($extra_code_antivirus && Amavis::AV::sophos_savi_stale() ) {
do_log(0, "Requesting process rundown due to stale Sophos virus data");
undef $smtp_in_obj; undef $ampdp_in_obj; undef $courier_in_obj;
+ undef $qmqpqq_in_obj;
$self->done(1);
}
my(@modules_extra) = grep(!exists $modules_basic{$_}, keys %INC);
@@ -18906,6 +18917,7 @@
$extra_code_zmq, $extra_code_db,
$extra_code_sql_lookup, $extra_code_ldap,
$extra_code_in_ampdp, $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, $extra_code_redis,
@@ -19257,7 +19269,13 @@
} else {
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: $@";
+ # release memory occupied by the source code
+ undef $extra_code_in_qmqpqq; $extra_code_in_qmqpqq = 1;
+ } else {
+ undef $extra_code_in_qmqpqq;
+ }
}
if (!@lookup_sql_dsn) { undef $extra_code_sql_lookup }
@@ -23506,6 +23524,279 @@
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.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->appl_proto("QMQPqq");
+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_dir;
+ $self->{tempdir}->prepare_file;
+
+ $msginfo = Amavis::In::Message->new;
+ $msginfo->rx_time(time);
+ $msginfo->log_id(am_id());
+ $msginfo->conn_obj($conn);
+
+ # 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),
+ make_received_header_field($msginfo,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($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;