Server IP : 85.214.239.14 / Your IP : 18.116.10.48 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /proc/3/task/3/cwd/proc/2/root/usr/share/perl5/Amavis/Out/ |
Upload File : |
# SPDX-License-Identifier: GPL-2.0-or-later package Amavis::Out::SMTP; use strict; use re 'taint'; use warnings; use warnings FATAL => qw(utf8 void); no warnings 'uninitialized'; # use warnings 'extra'; no warnings 'experimental::re_strict'; use re 'strict'; BEGIN { require Exporter; use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); $VERSION = '2.412'; @ISA = qw(Exporter); @EXPORT = qw(&mail_via_smtp); } use Time::HiRes qw(time); use Encode (); # use Authen::SASL; use Amavis::Conf qw(:platform c cr ca $smtp_connection_cache_enable); use Amavis::Lookup qw(lookup lookup2); use Amavis::Out::EditHeader; use Amavis::rfc2821_2822_Tools; use Amavis::Timing qw(section_time); use Amavis::Util qw(untaint min max minmax ll do_log snmp_count xtext_encode xtext_decode orcpt_encode orcpt_decode idn_to_ascii mail_addr_idn_to_ascii prolong_timer get_deadline collect_equal_delivery_recips); # simple OO wrapper around Mail::DKIM::Signer to provide a method 'print' # and to convert \n to CRLF # sub new_dkim_wrapper { my($class, $handle,$strip_cr) = @_; bless { handle => $handle, strip_cr => $strip_cr }, $class; } sub close { 1 } sub print { my $self = shift; my $buff = @_ == 1 ? $_[0] : join('',@_); do_log(-1,"WARN: Unicode string passed to Amavis::Out::SMTP::print : %s", $buff) if utf8::is_utf8($buff); # false on tainted, Perl 5.8 bug #32687 $buff =~ tr/\015//d if $self->{strip_cr}; # sanitize bare CR $buff =~ s{\n}{\015\012}gs; $self->{handle}->PRINT($buff); } # Add a log_id to the SMTP status text, insert a fabricated RFC 3463 enhanced # status code if missing in an MTA response, see also RFC 5248 # sub enhance_smtp_response($$$$$) { my($smtp_resp,$am_id,$mta_id,$dflt_enhcode,$cmd_name) = @_; local($1,$2,$3,$4); my $resp_msg; my($resp_code,$resp_more,$resp_enhcode) = ('451', ' ', '4.5.0'); if (!defined($smtp_resp) || $smtp_resp eq '') { $smtp_resp = sprintf('No resp. to %s', $cmd_name); } elsif ($smtp_resp !~ /^[245]\d{2}/) { $smtp_resp = sprintf('Bad resp. to %s: %s', $cmd_name,$smtp_resp); } elsif ($smtp_resp =~ /^ (\d{3}) (\ |-|\z) [ \t]* ([245] \. \d{1,3} \. \d{1,3})? \s* (.*) \z/xs) { ($resp_code, $resp_more, $resp_enhcode, $resp_msg) = ($1, $2, $3, $4); if (!defined $resp_enhcode && $resp_code =~ /^[245]/) { my $c = substr($resp_code,0,1); $resp_enhcode = $dflt_enhcode; $resp_enhcode =~ s/^\d*/$c/; } } sprintf("%s%s%s from MTA(%s): %s", $resp_code, $resp_more, $resp_enhcode, $mta_id, $smtp_resp); } # Send mail using SMTP - single transaction # (e.g. forwarding original mail or sending notification) # May throw exception (die) if temporary failure (4xx) or other problem # # Multiple transactions may be necessary, either due to different delivery # methods (IP address, port, SMTP vs. LMTP) or due to '452 Too many recipients' # sub mail_via_smtp(@) { my($msginfo, $initial_submission, $dsn_per_recip_capable, $filter) = @_; # # RFC 2033: LMTP protocol MUST NOT be used on the TCP port 25 # # $initial_submission can be treated as a boolean, but for more detailed # needs it can be any of: false: 0 # or true: 'Quar', 'Dsn', 'Notif', 'AV', 'Arf' my $which_section = 'fwd_init'; my $id = $msginfo->parent_mail_id; $id = $msginfo->mail_id . (defined $id ? "($id)" : ""); my $sender_smtp = $msginfo->sender_smtp; my $logmsg = sprintf("%s %s", $id, $initial_submission?'SEND':'FWD'); my($per_recip_data_ref, $proto_sockname) = collect_equal_delivery_recips($msginfo, $filter, qr/^(?:smtp|lmtp):/i); if (!$per_recip_data_ref || !@$per_recip_data_ref) { do_log(5, "%s from %s, nothing to do", $logmsg, $sender_smtp); return 1; } my $proto_sockname_displ = !ref $proto_sockname ? $proto_sockname : '(' . join(', ',@$proto_sockname) . ')'; my(@per_recip_data) = @$per_recip_data_ref; undef $per_recip_data_ref; ll(4) && do_log(4, "about to connect to %s, %s from %s -> %s", $proto_sockname_displ, $logmsg, $sender_smtp, join(',', qquote_rfc2821_local( map($_->recip_final_addr, @per_recip_data)) )); my $am_id = $msginfo->log_id; my $dsn_envid = $msginfo->dsn_envid; my $dsn_ret = $msginfo->dsn_ret; my $smtputf8 = $msginfo->smtputf8; # SMTPUTF8 requested my $smtputf8_capable; # SMTPUTF8 offered by MTA, RFC 6531 my($relayhost, $protocol, $lmtp, $mta_id, @snmp_vars); my($smtp_session, $smtp_handle, $smtp_resp, $smtp_response); my($any_valid_recips, $any_tempfail_recips, $pipelining, $any_valid_recips_and_data_sent, $recips_done_by_early_fail, $in_datasend_mode, $dsn_capable, $auth_capable) = (0) x 8; my $mimetransport8bit_capable = 0; # RFC 6152 my(%from_options); # RFC 5321 (ex RFC 2821), section 4.5.3.2. Timeouts my $smtp_connect_timeout = 35; my $smtp_helo_timeout = 300; my $smtp_starttls_timeout = 300; my $smtp_xforward_timeout = 300; my $smtp_mail_timeout = 300; my $smtp_rcpt_timeout = 300; my $smtp_data_init_timeout = 120; my $smtp_data_xfer_timeout = 180; my $smtp_data_done_timeout = 600; my $smtp_quit_timeout = 10; # 300 my $smtp_rset_timeout = 20; # can appear anywhere in a pipelined command group: # RSET, MAIL FROM, SEND FROM, SOML FROM, SAML FROM, RCPT TO, data # can only appear as the last command in a pipelined group: --> flush # EHLO, DATA, VRFY, EXPN, TURN, QUIT, NOOP, # AUTH(RFC 4954), STARTTLS(RFC 3207), and all unknown commands # needed to implement dynamic_destination: a '*' in place of a host or port my($wildcard_implied_host, $wildcard_implied_port); my $conn = $msginfo->conn_obj; if ($conn) { my $host = $conn->client_ip; $wildcard_implied_host = $host if defined($host) && $host ne ''; my $port = $conn->socket_port; $wildcard_implied_port = $port+1 if defined($port) && $port =~ /^\d+\z/; } my($remaining_time, $deadline) = get_deadline($which_section, 1, 0); alarm(0); # stop the timer my $err; eval { $which_section = 'fwd-connect'; require Amavis::Out::SMTP::Session; $smtp_session = Amavis::Out::SMTP::Session->new($proto_sockname, $deadline, $wildcard_implied_host, $wildcard_implied_port) or die "Can't establish an SMTP/LMTP session with $proto_sockname_displ"; $smtp_handle = $smtp_session->smtp_handle; if ($smtp_handle) { $relayhost = $smtp_handle->socketname; $protocol = $smtp_handle->protocol; $lmtp = lc($protocol) eq 'lmtp' ? 1 : 0; # RFC 2033 $mta_id = sprintf("%s:%s", $protocol, $relayhost); @snmp_vars = !$initial_submission ? ('', 'Relay', 'Proto'.uc($protocol), 'Proto'.uc($protocol).'Relay') : ('', 'Submit', 'Proto'.uc($protocol), 'Proto'.uc($protocol).'Submit', 'Submit'.$initial_submission); snmp_count('OutMsgs'.$_) for @snmp_vars; } $dsn_capable = c('propagate_dsn_if_possible') && defined($smtp_session->supports('DSN')); # RFC 3461 $mimetransport8bit_capable = # 8bit-MIMEtransport service extension defined($smtp_session->supports('8BITMIME')); # RFC 6152 $smtputf8_capable = # "Internationalized Email" service extension $mimetransport8bit_capable && defined($smtp_session->supports('SMTPUTF8')); # RFC 6531 $pipelining = defined($smtp_session->supports('PIPELINING')); # RFC 2920 do_log(3,"No announced PIPELINING support by MTA?") if !$pipelining; ll(5) && do_log(5,"Remote host presents itself as: %s, handles %s", $smtp_handle->domain, join(', ', $dsn_capable ? 'DSN' : (), $pipelining ? 'PIPELINING' : (), $mimetransport8bit_capable ? '8BITMIME' : (), $smtputf8_capable ? 'SMTPUTF8' : () ) ); if ($lmtp && !$pipelining) { # RFC 2033 requirements die "An LMTP server implementation MUST implement PIPELINING"; } if ($lmtp && !defined($smtp_session->supports('ENHANCEDSTATUSCODES'))) { die "An LMTP server implementation MUST implement ENHANCEDSTATUSCODES"; } if (!$smtputf8_capable || !$smtputf8) { # if SMTPUTF8 is not requested or if MTA is unable to handle # IDN with U-labels, and local part is all-ASCII, then we may # still get this delivered by converting a domain name # to ASCII-compatible encoding (ACE) if ($sender_smtp =~ /^ [\x00-\x7F]* \@ [^\@]* [^\x00-\x7F] [^\@]*\z/xs) { # localpart all-ASCII, domain is non-ASCII my $idn_ascii = mail_addr_idn_to_ascii($sender_smtp); do_log(2,'sender IDN encoded to ACE: %s -> %s', $sender_smtp, $idn_ascii); $sender_smtp = $idn_ascii; } for my $r (@per_recip_data) { next if $r->recip_done; my $rcpt_addr = $r->recip_final_addr; if ($rcpt_addr =~ /^ [\x00-\x7F]* \@ [^\@]* [^\x00-\x7F] [^\@]*\z/xs) { my $idn_ascii = mail_addr_idn_to_ascii($rcpt_addr); do_log(2,'recipient IDN encoded to ACE: %s -> %s', $rcpt_addr, $idn_ascii); $rcpt_addr = $idn_ascii; $r->dsn_orcpt(join(';', orcpt_decode(';'.$r->recip_addr_smtp))) if !defined $r->dsn_orcpt; # N.B.: change recip_addr_modified(), not recip_final_addr() ! $r->recip_addr_modified($rcpt_addr); } } } if ($smtputf8) { # SMTPUTF8 handling was requested, RFC 6531 # # RFC 6531 section 3.4: If the SMTPUTF8-aware SMTP client is aware # that neither the envelope nor the message being sent requires any # of the SMTPUTF8 extension capabilities, it SHOULD NOT supply the # SMTPUTF8 parameter with the MAIL command. # my($sender_8bit, $recips_8bit); $sender_8bit = 1 if $msginfo->sender_smtp =~ tr/\x00-\x7F//c; for my $r (@per_recip_data) { next if $r->recip_done; $recips_8bit = 1 if $r->recip_final_addr =~ tr/\x00-\x7F//c; } if (!ll(5)) { # don't bother, just logging } elsif ($sender_8bit || $recips_8bit || $msginfo->header_8bit) { do_log(5,'SMTPUTF8 option requested and is needed, %s is non-ASCII', join(' & ', $sender_8bit ? 'sender' : (), $recips_8bit ? 'recip' : (), $msginfo->header_8bit ? 'header' : () )); } else { do_log(5,'SMTPUTF8 option requested but not needed'); } if (!$smtputf8_capable) { # RFC 6531 sect 3.5: An SMTPUTF8-aware SMTP client MUST NOT send # an internationalized message to an SMTP server that does not # support SMTPUTF8. # 550 5.6.7 Non-ASCII addresses not permitted for that sender # 553 5.6.7 Non-ASCII addresses not permitted for that recipient # after DATA-dot: # 554 5.6.9 UTF-8 header message cannot be transmitted to one or more # recipients, so the message must be rejected # if (!$sender_8bit && !$recips_8bit) { # mail addresses are all-ASCII, don't care for an 8bit header do_log(3,'SMTPUTF8 option requested but not offered, turning it off'); $smtputf8 = 0; # turn off if not needed } } } section_time($which_section); $which_section = 'fwd-xforward'; my $cl_ip = $msginfo->client_addr; if (defined $cl_ip && $cl_ip ne '' && defined($smtp_session->supports('XFORWARD'))) { $cl_ip = 'IPv6:'.$cl_ip if $cl_ip =~ /:[0-9a-f]*:/i && $cl_ip !~ /^IPv6:/i; my(%xfwd_supp_opt) = map((uc($_),1), split(' ',$smtp_session->supports('XFORWARD'))); my(@params) = map { my($n,$v) = @$_; # Postfix since version 20060610 uses xtext-encoded (RFC 3461) # strings in XCLIENT and XFORWARD attribute values, previous # versions expected plain text with neutered special characters; # see README_FILES/XFORWARD_README if (defined $v && $v ne '') { $v =~ s/[^\041-\176]/?/gs; # isprint $v =~ s/[<>()\\";\@]/?/gs; # other chars that are special in hdrs # postfix/src/smtpd/smtpd.c NEUTER_CHARACTERS $v = xtext_encode($v); substr($v,255) = '' if length($v) > 255; # chop xtext, not nice } !defined $v || $v eq '' || !$xfwd_supp_opt{$n} ? () : ("$n=$v") } ( ['ADDR',$cl_ip], ['NAME',$msginfo->client_name], ['PORT',$msginfo->client_port], ['PROTO',$msginfo->client_proto], ['HELO',$msginfo->client_helo], ['SOURCE',$msginfo->client_source], ['IDENT',$msginfo->queue_id] ); $smtp_session->timeout( max(60,min($smtp_xforward_timeout,$deadline-time()))); $smtp_handle->command('XFORWARD',@params); #flush! $smtp_resp = $smtp_session->smtp_response; # fetch response to XFORWARD if (!defined $smtp_resp || $smtp_resp eq '') { do_log(-1,"%s SMTP resp. to XFORWARD, dt: %.3f s", !defined $smtp_resp ? 'No' : 'Empty', time - $smtp_handle->last_io_event_tx_timestamp); } elsif ($smtp_resp !~ /^2/) { do_log(0,"Negative SMTP resp. to XFORWARD: %s", $smtp_resp); } else { # success, $smtp_resp =~ /^2/ do_log(3,"smtp resp to XFORWARD: %s", $smtp_resp); } section_time($which_section); } $which_section = 'fwd-auth'; my $auth_user = $msginfo->auth_user; my $mechanisms = $smtp_session->supports('AUTH'); if (!c('auth_required_out')) { do_log(3,"AUTH not needed, user='%s', MTA offers '%s'", $auth_user,$mechanisms); } elsif ($mechanisms eq '') { do_log(3,"INFO: MTA does not offer AUTH capability, user='%s'", $auth_user); } elsif (!defined $auth_user) { do_log(0,"INFO: AUTH needed for submission but AUTH data not available"); } else { do_log(3,"INFO: authenticating %s, server supports AUTH %s", $auth_user,$mechanisms); $auth_capable = 1; # my $sasl = Authen::SASL->new( # 'callback' => { 'user' => $auth_user, 'authname' => $auth_user, # 'pass' => $msginfo->auth_pass }); # $smtp_handle->auth($sasl) or die "sending AUTH, user=$auth_user\n";#flush do_log(0,"Sorry, AUTH not supported in this version of amavisd!"); section_time($which_section); } $which_section = 'fwd-pre-mail-from'; $smtp_session->timeout(max(60,min($smtp_mail_timeout,$deadline-time()))); my $fetched_mail_resp = 0; my $fetched_rcpt_resp = 0; my $data_command_accepted = 0; if ($initial_submission && $dsn_capable && !defined($dsn_envid)) { # ENVID identifies transaction, not a message $dsn_envid = xtext_encode(sprintf("AM.%s.%s\@%s", $msginfo->mail_id || $msginfo->log_id, iso8601_utc_timestamp(time), idn_to_ascii(c('myhostname')) )); } $from_options{'RET'} = $dsn_ret if $dsn_capable && defined $dsn_ret; if ($dsn_capable && defined $dsn_envid) { # check for proper encoding (RFC 3461), just in case if ($dsn_envid =~ tr/ =\x00-\x1F//) { do_log(-1, "Prohibited character in ENVID: %s", $dsn_envid); } else { $from_options{'ENVID'} = $dsn_envid; } } my $submitter = $msginfo->auth_submitter; $from_options{'AUTH'} = xtext_encode($submitter) # RFC 4954 (ex RFC 2554) if $auth_capable && defined($submitter) && $submitter ne '' && $submitter ne '<>'; if ($smtputf8 && $smtputf8_capable) { $from_options{'SMTPUTF8'} = undef; # turn option *on*, no value } my $btype = $msginfo->body_type; if (defined $btype && $btype ne '') { $btype = uc $btype; if ($btype ne '7BIT' && $btype ne '8BITMIME') { do_log(-1,'requested BODY type %s is unknown/unsupported', $btype); } elsif ($mimetransport8bit_capable) { $from_options{'BODY'} = $btype; } } if (!$mimetransport8bit_capable && defined $btype && $btype ne '' && uc $btype ne '7BIT') { do_log(-1,'requested BODY type is %s, but MTA does not announce '. '8bit-MIMEtransport capability', $btype); # RFC 6152 for my $r (@per_recip_data) { next if $r->recip_done; $r->recip_smtp_response('550 5.6.3 Conversion to 7BIT required '. 'but not supported'); $r->recip_remote_mta($relayhost); $r->recip_done(2); } $recips_done_by_early_fail = 1; } elsif ($smtputf8 && !$smtputf8_capable && $sender_smtp =~ tr/\x00-\x7F//c) { do_log(1,'SMTPUTF8 option requested, not offered by MTA, '. 'sender is non-ASCII: %s', $sender_smtp); for my $r (@per_recip_data) { next if $r->recip_done; $r->recip_smtp_response('550 5.6.7 Non-ASCII addresses not permitted '. 'for sender'); $r->recip_remote_mta($relayhost); $r->recip_done(2); } $recips_done_by_early_fail = 1; } else { $which_section = 'fwd-mail-from'; $smtp_handle->mail($sender_smtp, %from_options); # MAIL FROM # consider the transaction state unknown until we see a response $smtp_session->transaction_begins_unconfirmed; # also counts transactions if (!$pipelining) { $smtp_resp = $smtp_session->smtp_response; $fetched_mail_resp = 1; if (!defined $smtp_resp || $smtp_resp eq '') { die sprintf("%s response to MAIL, dt: %.3f s\n", !defined $smtp_resp ? 'No' : 'Empty', time - $smtp_handle->last_io_event_tx_timestamp); } elsif ($smtp_resp =~ /^2/) { do_log(3, "smtp resp to MAIL: %s", $smtp_resp); $smtp_session->transaction_begins; # transaction is active } else { # failure do_log(1, "smtp resp to MAIL: %s", $smtp_resp); # transaction state unchanged, consider it unknown my $smtp_resp_ext = enhance_smtp_response($smtp_resp,$am_id,$mta_id, '.1.0','MAIL FROM'); for my $r (@per_recip_data) { next if $r->recip_done; $r->recip_remote_mta($relayhost); $r->recip_remote_mta_smtp_response($smtp_resp); $r->recip_smtp_response($smtp_resp_ext); $r->recip_done(2); } $recips_done_by_early_fail = 1; } section_time($which_section); } } $which_section = 'fwd-rcpt-to'; $smtp_session->timeout(max(60,min($smtp_rcpt_timeout,$deadline-time()))); my($skipping_resp, @per_recip_data_rcpt_sent); for my $r (@per_recip_data) { # send recipient addresses next if $r->recip_done; if (defined $skipping_resp) { $r->recip_smtp_response($skipping_resp); $r->recip_done(2); next; } # prepare to send a RCPT TO command my $raddr = qquote_rfc2821_local($r->recip_final_addr); if ($smtputf8 && !$smtputf8_capable && $raddr =~ tr/\x00-\x7F//c) { do_log(1,'SMTPUTF8 option requested, not offered by MTA, '. 'recipient is non-ASCII: %s', $raddr); $r->recip_smtp_response('553 5.6.7 Non-ASCII addresses '. 'not permitted for recipient'); $r->recip_remote_mta($relayhost); $r->recip_done(2); } elsif (!$dsn_capable) { $smtp_handle->recipient($raddr); # a barebones RCPT TO command push(@per_recip_data_rcpt_sent, $r); # remember which recips were sent } else { # include dsn options with a RCPT TO command my(@dsn_notify); # implies a default when the list is empty my $dn = $r->dsn_notify; @dsn_notify = @$dn if $dn && $msginfo->sender ne ''; # if nondefault if (c('terminate_dsn_on_notify_success')) { # we want to handle option SUCCESS locally if (grep($_ eq 'SUCCESS', @dsn_notify)) { # strip out SUCCESS @dsn_notify = grep($_ ne 'SUCCESS', @dsn_notify); @dsn_notify = ('NEVER') if !@dsn_notify; do_log(3,"stripped out SUCCESS, result: NOTIFY=%s", join(',',@dsn_notify)); } } my(%rcpt_options); $rcpt_options{'NOTIFY'} = join(',', map(uc($_),@dsn_notify)) if @dsn_notify; my($addr_type, $addr) = orcpt_encode($r->dsn_orcpt, $smtputf8 && $smtputf8_capable, 1); $rcpt_options{'ORCPT'} = $addr_type.';'.$addr if defined $addr; $smtp_handle->recipient($raddr, %rcpt_options); # RCPT TO push(@per_recip_data_rcpt_sent, $r); # remember which recips were sent } if (!$pipelining) { # must fetch responses to RCPT TO right away $smtp_resp = $smtp_session->smtp_response; $fetched_rcpt_resp = 1; if (defined $smtp_resp && $smtp_resp ne '') { $r->recip_remote_mta($relayhost); $r->recip_remote_mta_smtp_response($smtp_resp); my $smtp_resp_ext = enhance_smtp_response($smtp_resp,$am_id,$mta_id, '.1.0','RCPT TO'); $r->recip_smtp_response($smtp_resp_ext); # preliminary response } if (!defined $smtp_resp || $smtp_resp eq '') { die sprintf("%s response to RCPT (%s), dt: %.3f s\n", !defined $smtp_resp ? 'No' : 'Empty', $raddr, time - $smtp_handle->last_io_event_tx_timestamp); } elsif ($smtp_resp =~ /^2/) { do_log(3, "smtp resp to RCPT (%s): %s", $raddr,$smtp_resp); $any_valid_recips++; } else { # failure do_log(1, "smtp resp to RCPT (%s): %s", $raddr,$smtp_resp); if ($smtp_resp =~ /^452/) { # too many recipients - see RFC 5321 do_log(-1, 'Only %d recips sent in one go: "%s"', $any_valid_recips, $smtp_resp) if !defined $skipping_resp; $skipping_resp = enhance_smtp_response($smtp_resp,$am_id,$mta_id, '.5.3','RCPT TO'); } elsif ($smtp_resp =~ /^4/) { $any_tempfail_recips++; } $r->recip_done(2); # got a negative response to RCPT TO } } } section_time($which_section) if !$pipelining; # otherwise it just shows 0 my $what_cmd; if (!@per_recip_data_rcpt_sent || # no recipients were sent $fetched_rcpt_resp && !$any_valid_recips) { # no recipients accepted # it is known there are no valid recipients, don't go into DATA section do_log(0,"no valid recipients, skip data transfer"); $smtp_session->timeout($smtp_rset_timeout); $what_cmd = 'RSET'; $smtp_handle->rset; # send a RSET $smtp_session->transaction_ends_unconfirmed; } elsif ($fetched_rcpt_resp && # no pipelining $any_tempfail_recips && !$dsn_per_recip_capable) { # we must not proceed if mail did not came in as LMTP, # or we would generate mail duplicates on each delivery attempt do_log(-1,"mail_via_smtp: DATA skipped, tempfailed recips: %s", $any_tempfail_recips); $smtp_session->timeout($smtp_rset_timeout); $what_cmd = 'RSET'; $smtp_handle->rset; # send a RSET $smtp_session->transaction_ends_unconfirmed; } else { # pipelining, or we know we got a clearance to proceed $which_section = 'fwd-data-cmd'; # pipelining in effect, or we have at least one valid recipient, go DATA $smtp_session->timeout( max(60,min($smtp_data_init_timeout,$deadline-time()))); $smtp_handle->data; #flush! DATA $in_datasend_mode = 1; # DATA command was sent (but not yet confirmed) if (!$fetched_mail_resp) { # pipelining in effect, late response to MAIL $which_section = 'fwd-mail-pip'; $smtp_session->timeout( max(60,min($smtp_mail_timeout,$deadline-time()))); $smtp_resp = $smtp_session->smtp_response; $fetched_mail_resp = 1; if (!defined $smtp_resp || $smtp_resp eq '') { die sprintf("%s response to MAIL (pip), dt: %.3f s\n", !defined $smtp_resp ? 'No' : 'Empty', time - $smtp_handle->last_io_event_tx_timestamp); } elsif ($smtp_resp =~ /^2/) { do_log(3, "smtp resp to MAIL (pip): %s", $smtp_resp); $smtp_session->transaction_begins; # transaction is active } else { # failure do_log(1, "smtp resp to MAIL (pip): %s", $smtp_resp); # transaction state unchanged, consider it unknown my $smtp_resp_ext = enhance_smtp_response($smtp_resp, $am_id, $mta_id, '.1.0', 'MAIL FROM'); for my $r (@per_recip_data) { next if $r->recip_done; $r->recip_remote_mta($relayhost); $r->recip_remote_mta_smtp_response($smtp_resp); $r->recip_smtp_response($smtp_resp_ext); $r->recip_done(2); } $recips_done_by_early_fail = 1; } section_time($which_section); } if (!$fetched_rcpt_resp) { # pipelining in effect, late response to RCPT $which_section = 'fwd-rcpt-pip'; $smtp_session->timeout( max(60,min($smtp_rcpt_timeout,$deadline-time()))); for my $r (@per_recip_data_rcpt_sent) { # only for those actually sent my $raddr = qquote_rfc2821_local($r->recip_final_addr); $smtp_resp = $smtp_session->smtp_response; $fetched_rcpt_resp = 1; if (defined $smtp_resp && $smtp_resp ne '') { if ($r->recip_done) { # shouldn't happen, unless MAIL FROM failed do_log(-1,"panic: recipient done, but got an ". "smtp resp to RCPT (pip) (%s): %s", $raddr,$smtp_resp) if !$recips_done_by_early_fail; next; # do not overwrite previous diagnostics } $r->recip_remote_mta($relayhost); $r->recip_remote_mta_smtp_response($smtp_resp); my $smtp_resp_ext = enhance_smtp_response($smtp_resp, $am_id, $mta_id, '.1.0', 'RCPT TO'); $r->recip_smtp_response($smtp_resp_ext); # preliminary response } if (!defined $smtp_resp || $smtp_resp eq '') { die sprintf("%s response to RCPT (pip) (%s), dt: %.3f s\n", !defined $smtp_resp ? 'No' : 'Empty', $raddr, time - $smtp_handle->last_io_event_tx_timestamp); } elsif ($smtp_resp =~ /^2/) { do_log(3, "smtp resp to RCPT (pip) (%s): %s", $raddr,$smtp_resp); $any_valid_recips++; } else { # failure do_log(1, "smtp resp to RCPT (pip) (%s): %s", $raddr,$smtp_resp); if ($smtp_resp =~ /^452/) { # too many recipients - see RFC 5321 do_log(-1, 'Only %d recips sent in one go: "%s"', $any_valid_recips, $smtp_resp) if !defined $skipping_resp; $skipping_resp = enhance_smtp_response($smtp_resp,$am_id,$mta_id, '.5.3','RCPT TO'); } elsif ($smtp_resp =~ /^4/) { $any_tempfail_recips++; } $r->recip_done(2); # got a negative response to RCPT TO } } section_time($which_section); } $which_section = 'fwd-data-chkpnt' if $pipelining; $smtp_session->timeout( max(60,min($smtp_data_init_timeout,$deadline-time()))); $smtp_resp = $smtp_session->smtp_response; # fetch response to DATA section_time($which_section); $data_command_accepted = 0; if (!defined $smtp_resp || $smtp_resp eq '') { do_log(-1,"%s SMTP resp. to DATA, dt: %.3f s", !defined $smtp_resp ? 'No' : 'Empty', time - $smtp_handle->last_io_event_tx_timestamp); $smtp_resp = sprintf("450 4.5.0 %s response to DATA", !defined $smtp_resp ? 'No' : 'Empty'); } elsif ($smtp_resp !~ /^3/) { do_log(0,"Negative SMTP resp. to DATA: %s", $smtp_resp); } else { # success, $smtp_resp =~ /^3/ $data_command_accepted = 1; do_log(3,"smtp resp to DATA: %s", $smtp_resp); } if (!$data_command_accepted) { $in_datasend_mode = 0; $smtp_session->timeout($smtp_rset_timeout); $what_cmd = 'RSET'; $smtp_handle->rset; # send a RSET $smtp_session->transaction_ends_unconfirmed; # replace success responses to RCPT TO commands with a response to DATA for my $r (@per_recip_data_rcpt_sent) { # only for those actually sent next if $r->recip_done; # skip those that failed at earlier stages $r->recip_remote_mta($relayhost); $r->recip_remote_mta_smtp_response($smtp_resp); my $smtp_resp_ext = enhance_smtp_response($smtp_resp, $am_id, $mta_id, '.5.0', 'DATA'); $smtp_response = $smtp_resp_ext if !defined $smtp_response; $r->recip_smtp_response($smtp_resp_ext); $r->recip_done(2); } } elsif (!$any_valid_recips) { # pipelining and no recipients, in DATA do_log(2,"Too late, DATA accepted but no valid recips, send dummy"); $which_section = 'fwd-dummydata-end'; $smtp_session->timeout( max(60,min($smtp_data_done_timeout,$deadline-time()))); $what_cmd = 'data-dot'; $smtp_handle->dataend; # .<CR><LF> as required by RFC 2920: if a DATA # command was accepted the SMTP client should send a single dot $in_datasend_mode = 0; $smtp_session->transaction_ends_unconfirmed; } elsif ($any_tempfail_recips && !$dsn_per_recip_capable) { # pipelining # we must not proceed if mail did not came in as LMTP, # or we would generate mail duplicates on each delivery attempt do_log(2,"Too late, DATA accepted but tempfailed recips, bail out"); die "Bail out, DATA accepted but tempfailed recips, not an LMTP input"; } else { # all ok so far, we are in a DATA state and must send contents $which_section = 'fwd-data-hdr'; my $hdr_edits = $msginfo->header_edits; $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits; $smtp_session->timeout( max(60,min($smtp_data_xfer_timeout,$deadline-time()))); my($received_cnt,$file_position) = $hdr_edits->write_header($msginfo,$smtp_handle,!$initial_submission); if ($received_cnt > 100) { # loop detection required by RFC 5321 (ex RFC 2821) section 6.3 # Do not modify the signal text, it gets matched elsewhere! die "Too many hops: $received_cnt 'Received:' header fields\n"; } $which_section = 'fwd-data-contents'; # a file handle or a string ref or MIME::Entity object my $msg = $msginfo->mail_text; my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy? $msg = $msg_str_ref if ref $msg_str_ref; if (!defined $msg) { # empty mail } elsif (ref $msg eq 'SCALAR') { # do it in chunks, saves memory, cache friendly while ($file_position < length($$msg)) { $smtp_handle->datasend(substr($$msg,$file_position,16384)); $file_position += 16384; # may overshoot, no problem } } elsif ($msg->isa('MIME::Entity')) { $msg->print_body($smtp_handle); } else { my($nbytes,$buff); while (($nbytes = $msg->read($buff,3*16384)) > 0) { $smtp_handle->datasend($buff); } defined $nbytes or die "Error reading: $!"; } section_time($which_section); $which_section = 'fwd-data-end'; $smtp_session->timeout( max(60,min($smtp_data_done_timeout,$deadline-time()))); $what_cmd = 'data-dot'; $smtp_handle->dataend; # .<CR><LF> $in_datasend_mode = 0; $smtp_session->transaction_ends_unconfirmed; $any_valid_recips_and_data_sent = 1; section_time($which_section) if !$pipelining; # otherwise it shows 0 } } if ($pipelining && !$smtp_connection_cache_enable) { do_log(5,"smtp connection_cache disabled, sending QUIT"); $smtp_session->quit; #flush! QUIT # can't be sure until we see a response, consider uncertain just in case $smtp_session->transaction_ends_unconfirmed; } $which_section = 'fwd-rundown-1'; $smtp_resp = undef; if (!defined $what_cmd) { # not expecting a response? } elsif ($what_cmd ne 'data-dot') { # must be a response to a RSET $smtp_resp = $smtp_session->smtp_response; # fetch a response if (!defined $smtp_resp || $smtp_resp eq '') { die sprintf("%s SMTP response to %s, dt: %.3f s", !defined $smtp_resp ? 'No' : 'Empty', $what_cmd, time - $smtp_handle->last_io_event_tx_timestamp); } elsif ($smtp_resp !~ /^2/) { die "Negative SMTP response to $what_cmd: $smtp_resp"; } else { # success, $smtp_resp =~ /^2/ do_log(3,"smtp resp to %s: %s", $what_cmd,$smtp_resp); $smtp_session->transaction_ends if $what_cmd eq 'RSET'; } } else { # get response(s) to data-dot # replace success responses to RCPT TO commands with a final response my $first = 1; my $anyfail = 0; my $anysucc = 0; for my $r (@per_recip_data_rcpt_sent) { # only for those actually sent if ($lmtp || $first) { $first = 0; my $raddr = qquote_rfc2821_local($r->recip_final_addr); $raddr .= ', etc.' if !$lmtp && @per_recip_data > 1; $smtp_resp = $smtp_session->smtp_response; # resp to data-dot if (!defined $smtp_resp || $smtp_resp eq '') { $anyfail = 1; do_log(0,"%s SMTP response to %s (%s), dt: %.3f s", !defined $smtp_resp ? 'No' : 'Empty', $what_cmd, $raddr, time - $smtp_handle->last_io_event_tx_timestamp); } elsif ($smtp_resp !~ /^2/) { $anyfail = 1; do_log(0,"Negative SMTP response to %s (%s): %s, dt: %.1f ms", $what_cmd, $raddr, $smtp_resp, 1000*(time-$smtp_handle->last_io_event_tx_timestamp)); } else { # success, $smtp_resp =~ /^2/ $anysucc = 1; ll(3) && do_log(3,"smtp resp to %s (%s): %s, dt: %.1f ms", $what_cmd, $raddr, $smtp_resp, 1000*(time-$smtp_handle->last_io_event_tx_timestamp)); } } next if $r->recip_done; # skip those that failed at earlier stages $r->recip_remote_mta($relayhost); $r->recip_remote_mta_smtp_response($smtp_resp); my $smtp_resp_ext = enhance_smtp_response($smtp_resp,$am_id,$mta_id, '.6.0','data-dot'); $smtp_response = $smtp_resp_ext if !defined $smtp_response; $r->recip_smtp_response($smtp_resp_ext); $r->recip_done(2); $r->recip_mbxname($r->recip_final_addr) if $smtp_resp =~ /^2/; } if ($first) { # fetch an uncollected response # fetch unprocessed response if all recipients were rejected # but we nevertheless somehow entered a data transfer mode # (i.e. if an SMTP server failed to reject a DATA command). # RFC 2033: when there have been no successful RCPT commands in the # mail transaction, the DATA command MUST fail with a 503 reply code $smtp_resp = $smtp_session->smtp_response; # resp to data-dot $smtp_resp = '' if !defined $smtp_resp; if ($smtp_resp =~ /^2/) { $anysucc = 1 } else { $anyfail = 1 } do_log(3,"smtp resp to _dummy_ data %s: %s", $what_cmd,$smtp_resp); } if ($anysucc && !$anyfail) { # we are certain all went fine and a transaction is definitely over $smtp_session->transaction_ends; } } # if ($pipelining) {} # QUIT was already sent # elsif (!$smtp_connection_cache_enable) { # $smtp_session->quit; #flush! QUIT # # can't be sure until we see a response, consider uncertain just in case # $smtp_session->transaction_ends_unconfirmed; # } # if ($smtp_session->session_state eq 'quitsent') { # $smtp_session->timeout($smtp_quit_timeout); # $smtp_resp = $smtp_session->smtp_response; # $smtp_resp = '' if !defined $smtp_resp; # do_log(3,"smtp resp to QUIT: %s", $smtp_resp); # if ($smtp_resp =~ /^2/) { # $smtp_session->transaction_ends; # } else { # $smtp_session->transaction_ends_unconfirmed; # do_log(0,"Negative SMTP resp. to QUIT: %s", $smtp_resp); # } # } my $keep_session = $smtp_session->session_state ne 'quitsent'; if ($keep_session && !defined($smtp_session->in_smtp_transaction)) { do_log(2,"SMTP transaction state uncertain, closing just in case"); $keep_session = 0; } $smtp_session->close($keep_session) or die "Error closing Amavis::Out::SMTP::Session"; undef $smtp_handle; undef $smtp_session; 1; # some unusual error conditions _are_ captured by eval, but fail to set $@ } or do { $err = $@ ne '' ? $@ : "errno=$!"; chomp $err }; my $saved_section_name = $which_section; $which_section = 'fwd-end-chkpnt'; do_log(2,"mail_via_smtp: session failed: %s", $err) if defined $err; prolong_timer($which_section); # restart timer # terminate the SMTP session if still alive if (!defined($smtp_session)) { # already closed normally } elsif ($in_datasend_mode) { # We are aborting SMTP session. Data transfer mode must NOT be terminated # with a dataend (dot), otherwise recipient will receive a chopped-off mail # (and possibly be receiving it over and over again during each MTA retry. do_log(-1, "mail_via_smtp: NOTICE: aborting SMTP session, %s", $err); $smtp_session->close(0); # abruptly terminate SMTP session, ignore status } else { do_log(5,"smtp session done, sending QUIT"); eval { $smtp_session->timeout(1); # don't wait for too long $smtp_session->quit; #flush! # send a QUIT regardless of success so far $smtp_session->transaction_ends_unconfirmed; for (my $cnt=0; ; $cnt++) { # curious if there are any pending responses my $smtp_resp = $smtp_session->smtp_response; last if !defined $smtp_resp; do_log(0,"discarding unprocessed reply: %s", $smtp_resp); if ($cnt > 20) { do_log(-1,"aborting, discarding many replies"); last } } } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; do_log(-1, "mail_via_smtp: error during QUIT: %s", $eval_stat); }; $smtp_session->close(0); # terminate SMTP session, ignore status } undef $smtp_handle; undef $smtp_session; # prepare final smtp response and log abnormal events for my $r (@per_recip_data) { my $resp = $r->recip_smtp_response; $smtp_response = $resp if !defined($smtp_response) || $resp =~ /^4/ && $smtp_response !~ /^4/ || $resp !~ /^2/ && $smtp_response !~ /^[45]/; } if (!defined $err) { # no errors } elsif ($err =~ /^timed out\b/ || $err =~ /: Timeout\z/) { $smtp_response = sprintf("450 4.4.2 Timed out during %s, MTA(%s), id=%s", $saved_section_name, $mta_id, $am_id); } elsif ($err =~ /^Can't connect\b/) { $smtp_response = sprintf("450 4.4.1 %s, MTA(%s), id=%s", $err, $mta_id, $am_id); } elsif ($err =~ /^Too many hops\b/) { $smtp_response = sprintf("554 5.4.6 Reject: %s, id=%s", $err, $am_id); } else { $smtp_response = sprintf("451 4.5.0 From MTA(%s) during %s (%s): id=%s", $mta_id, $saved_section_name, $err, $am_id); } # NOTE: $initial_submission argument is typically treated as a boolean # but a value of 'AV' is supplied by av_smtp_client to allow a forwarding # method to distinguish it from ordinary submissions my $ll = ($smtp_response =~ /^2/ || $initial_submission eq 'AV') ? 1 : -1; ll($ll) && do_log($ll, "%s from %s -> %s, %s %s", $logmsg, $sender_smtp, join(',', qquote_rfc2821_local( map($_->recip_final_addr, @per_recip_data))), join(' ', map { my $v=$from_options{$_}; defined($v)?"$_=$v":"$_" } (keys %from_options)), $smtp_response); if (defined $smtp_response) { $msginfo->dsn_passed_on($dsn_capable && $smtp_response=~/^2/ && !c('terminate_dsn_on_notify_success') ? 1 : 0); for my $r (@per_recip_data) { # attach an SMTP response to each recip that was not already processed if (!$r->recip_done) { # mark it as done $r->recip_smtp_response($smtp_response); $r->recip_done(2); $r->recip_mbxname($r->recip_final_addr) if $smtp_response =~ /^2/; } elsif ($any_valid_recips_and_data_sent && $r->recip_smtp_response =~ /^452/) { # 'undo' the RCPT TO '452 Too many recipients' situation, # mail needs to be transferred in more than one transaction $r->recip_smtp_response(undef); $r->recip_done(undef); } } if ($smtp_response =~ /^2/) { snmp_count('OutMsgsDelivers'); my $size = $msginfo->msg_size; snmp_count( ['OutMsgsSize'.$_, $size, 'C64'] ) for @snmp_vars; } elsif ($smtp_response =~ /^4/) { snmp_count('OutMsgsAttemptFails'); } elsif ($smtp_response =~ /^5/) { snmp_count('OutMsgsRejects'); } } section_time($which_section); die $err if defined($err) && $err =~ /^timed out\b/; # resignal timeout 1; } 1;