Server IP : 85.214.239.14 / Your IP : 18.223.125.42 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/2/root/usr/share/perl5/Amavis/ |
Upload File : |
# SPDX-License-Identifier: GPL-2.0-or-later package Amavis::Notify; use strict; use re 'taint'; BEGIN { require Exporter; use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); $VERSION = '2.412'; @ISA = qw(Exporter); @EXPORT_OK = qw(&delivery_status_notification &delivery_short_report &build_mime_entity &defanged_mime_entity &msg_from_quarantine &expand_variables); } use subs @EXPORT_OK; use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL); use MIME::Entity; use Time::HiRes (); use Amavis::Conf qw(:platform :confvars c cr ca); use Amavis::Expand qw(expand); use Amavis::In::Message; use Amavis::In::Message::PerRecip; use Amavis::Lookup qw(lookup lookup2); use Amavis::MIME::Body::OnOpenFh; use Amavis::Out::EditHeader qw(hdr); use Amavis::ProcControl qw(exit_status_str proc_status_ok run_command collect_results); use Amavis::rfc2821_2822_Tools; use Amavis::Timing qw(section_time); use Amavis::Util qw(ll do_log sanitize_str min max minmax untaint untaint_inplace idn_to_ascii idn_to_utf8 mail_addr_idn_to_ascii is_valid_utf_8 safe_decode_utf8 safe_encode safe_encode_utf8 safe_encode_utf8_inplace orcpt_encode orcpt_decode xtext_decode safe_decode_mime make_password ccat_split ccat_maj generate_mail_id); # replace substring ${myhostname} with a value of a corresponding variable sub expand_variables($) { my $str = $_[0]; local($1,$2); my $myhost = idn_to_utf8(c('myhostname')); $str =~ s{ \$ (?: \{ ([^\}]+) \} | ([a-zA-Z](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?\b) ) } { { 'myhostname' => $myhost, 'myhostname_utf8' => $myhost, 'myhostname_ascii' => idn_to_ascii($myhost), }->{lc($1.$2)} }xgse; $str; } # wrap a mail message into a ZIP archive # sub wrap_message_into_archive($$) { my($msginfo,$prefix_lines_ref) = @_; # a file with a copy of a mail msg as retrieved from a quarantine: my $attachment_email_name = c('attachment_email_name'); # 'msg-%m.eml' # an archive file (will contain a retrieved message) to be attached: my $attachment_outer_name = c('attachment_outer_name'); # 'msg-%m.zip' my($email_fh, $arch_size); my $mail_id = $msginfo->mail_id; if (!defined $mail_id || $mail_id eq '') { $mail_id = ''; } else { $mail_id =~ /^[A-Za-z0-9_-]*\z/ or die "unsafe mail_id: $mail_id"; untaint_inplace($mail_id); } for ($attachment_email_name, $attachment_outer_name) { local $1; s{%(.)}{ $1 eq 'b' ? $msginfo->body_digest : $1 eq 'P' ? $msginfo->partition_tag : $1 eq 'm' ? $mail_id : $1 eq 'n' ? $msginfo->log_id : $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1) #,'-') : $1 eq '%' ? '%' : '%'.$1 }gse; $_ = $msginfo->mail_tempdir . '/' . $_; } my $eval_stat; eval { # copy a retrieved message to a file $email_fh = IO::File->new; $email_fh->open($attachment_email_name, O_CREAT|O_EXCL|O_RDWR, 0640) or die "Can't create file $attachment_email_name: $!"; binmode($email_fh,':bytes') or die "Can't cancel :utf8 mode: $!"; for (@$prefix_lines_ref) { $email_fh->print($_) or die "Error writing to $attachment_email_name: $!"; } 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; # copy quarantined mail starting at skip_bytes to $attachment_email_name my $file_position = $msginfo->skip_bytes; if (!defined $msg) { # empty mail } elsif (ref $msg eq 'SCALAR') { # do it in chunks, saves memory, cache friendly while ($file_position < length($$msg)) { $email_fh->print(substr($$msg,$file_position,16384)) or die "Error writing to $attachment_email_name: $!"; $file_position += 16384; # may overshoot, no problem } } elsif ($msg->isa('MIME::Entity')) { die "wrapping a MIME::Entity object is not implemented"; } else { $msg->seek($file_position,0) or die "Can't rewind mail file: $!"; my($nbytes,$buff); while (($nbytes = $msg->read($buff,16384)) > 0) { $email_fh->print($buff) or die "Error writing to $attachment_email_name: $!"; } defined $nbytes or die "Error reading mail file: $!"; undef $buff; # release storage } $email_fh->close or die "Can't close file $attachment_email_name: $!"; undef $email_fh; # create a password-protected archive containing the just prepared file; # no need to shell-protect arguments, as this does not invoke a shell my $password = $msginfo->attachment_password; my(@command) = ( qw(zip -q -j -l), $password eq '' ? () : ('-P', $password), $attachment_outer_name, $attachment_email_name ); # supplying a password on a command line is lame as it shows in ps(1), # but an option -e would require a pseudo terminal, which is really # an overweight cannon unnecessary here: the password is used as a # scrambler only, protecting against accidental opening of a file, # so there is no security issue here $password = 'X' x length($password); # can't hurt to wipe out my($proc_fh,$pid) = run_command(undef,undef,@command); my($r,$status) = collect_results($proc_fh,$pid,'zip',16384,[0]); undef $proc_fh; undef $pid; do_log(2,'archiver said: %s',$$r) if ref $r && $$r ne ''; $status == 0 or die "Error creating an archive: $status, $$r"; my $errn = lstat($attachment_outer_name) ? 0 : 0+$!; if ($errn) { die "Archive $attachment_outer_name is inaccessible: $!" } else { $arch_size = 0 + (-s _) } 1; } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; }; if ($eval_stat ne '' || !$arch_size) { # handle failure my $msg = $eval_stat ne '' ? $eval_stat : sprintf("archive size %d", $arch_size); do_log(-1,'Preparing an archive from a quarantined message failed: %s', $msg); if (defined $email_fh && $email_fh->fileno) { $email_fh->close or do_log(-1,"Can't close %s: %s", $attachment_email_name, $!); } undef $email_fh; if (-e $attachment_email_name) { unlink($attachment_email_name) or do_log(-1,"Can't remove %s: %s", $attachment_email_name, $!); } if (-e $attachment_outer_name) { unlink($attachment_outer_name) or do_log(-1,"Can't remove %s: %s", $attachment_outer_name, $!); } die "Preparing an archive from a quarantined message failed: $msg\n"; } $attachment_outer_name; } # Create a MIME::Entity object. If $mail_as_string_ref points to a string # (multiline mail header with a plain text body) it is added as the first # MIME part. Optionally attach a message header section from original mail, # or attach a complete original message. # sub build_mime_entity($$$$$$$) { my($mail_as_string_ref, $msginfo, $mime_type, $msg_format, $flat, $attach_orig_headers, $attach_orig_message) = @_; $msg_format = '' if !defined $msg_format; if (!defined $mime_type || $mime_type !~ m{^ multipart (?: / | \z)}xsi) { my $multipart_cnt = 0; $multipart_cnt++ if $mail_as_string_ref; $multipart_cnt++ if defined $msginfo && ($attach_orig_headers || $attach_orig_message); $mime_type = 'multipart/mixed' if $multipart_cnt > 1; } my($entity,$m_hdr,$m_body); if (!$mail_as_string_ref) { # no plain text part } elsif ($$mail_as_string_ref eq '') { $m_hdr = $m_body = ''; } elsif (substr($$mail_as_string_ref, 0,1) eq "\n") { # empty header section? $m_hdr = ''; $m_body = substr($$mail_as_string_ref,1); } else { # calling index and substr is much faster than an equiv. split into $1,$2 # by a regular expression: /^( (?!\n) .*? (?:\n|\z))? (?: \n (.*) )? \z/xs my $ind = index($$mail_as_string_ref,"\n\n"); # find header/body separator if ($ind < 0) { # no body $m_hdr = $$mail_as_string_ref; $m_body = ''; } else { # normal mail, nonempty header section and nonempty body $m_hdr = substr($$mail_as_string_ref, 0, $ind+1); $m_body = substr($$mail_as_string_ref, $ind+2); } } safe_encode_utf8_inplace($m_hdr); $m_body = safe_encode(c('bdy_encoding'), $m_body) if defined $m_body; # make sure _our_ source line number is reported in case of failure my $multipart_cnt = 0; $mime_type = 'multipart/mixed' if !defined $mime_type; eval { # RFC 6522: 7bit should always be adequate for multipart/report encoding $entity = MIME::Entity->build( Type => $mime_type, Encoding => '8bit', 'X-Mailer' => undef); 1; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; die $eval_stat; }; if (defined $m_hdr) { # insert header fields into MIME::Head entity; # Mail::Header::modify allows all-or-nothing control over automatic header # fields folding by Mail::Header, which is too bad - we would prefer # to have full control on folding of header fields that are explicitly # inserted here, and let Mail::Header handle the rest. Sorry, can't be # done, so let's just disable folding by Mail::Header (which does a poor # job when presented with few break opportunities), and wrap our header # fields ourselves, hoping the remaining automatically generated header # fields won't be too long. local($1,$2); my $head = $entity->head; $head->modify(0); $m_hdr =~ s/\r?\n(?=[ \t])//gs; # unfold header fields in a template for my $hdr_line (split(/\r?\n/, $m_hdr)) { if ($hdr_line =~ /^([^:]*?)[ \t]*:[ \t]*(.*)\z/s) { my($fhead,$fbody) = ($1,$2); $fbody = safe_decode_mime($fbody); # to logical characters # encode, wrap, ... my $str = hdr($fhead, $fbody, 0, ' ', $msginfo->smtputf8); # re-split the result ($fhead,$fbody) = ($1,$2) if $str =~ /^([^:]*):[ \t]*(.*)\z/s; chomp($fbody); do_log(5, "build_mime_entity %s: %s", $fhead,$fbody); eval { # make sure _our_ source line number is reported on failure $head->replace($fhead,$fbody); 1; } or do { $@ = "errno=$!" if $@ eq ''; chomp $@; die $@ if $@ =~ /^timed out\b/; # resignal timeout die sprintf("%s header field '%s: %s'", ($@ eq '' ? "invalid" : "$@, "), $fhead,$fbody); }; } } } my(@prefix_lines); if (defined $m_body) { if ($flat && $attach_orig_message) { my($pos,$j); # split $m_body into lines, retaining each \n for ($pos=0; ($j=index($m_body,"\n",$pos)) >= 0; $pos = $j+1) { push(@prefix_lines, substr($m_body,$pos,$j-$pos+1)); } push(@prefix_lines, substr($m_body,$pos)) if $pos < length($m_body); } else { my $cnt_8bit = $m_body =~ tr/\x00-\x7F//c; eval { # make sure _our_ source line number is reported on failure $entity->attach( Type => 'text/plain', Data => $m_body, Charset => !$cnt_8bit ? 'us-ascii' : c('bdy_encoding'), Encoding => !$cnt_8bit ? '7bit' : $cnt_8bit < 0.2 * length($m_body) ? 'quoted-printable' : 'base64', ); $multipart_cnt++; 1; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; die $eval_stat; }; } } # prepend a Return-Path to make available the envelope sender address push(@prefix_lines, "\n") if @prefix_lines; # separates text from a message push(@prefix_lines, sprintf("Return-Path: %s\n", $msginfo->sender_smtp)); if (defined $msginfo && $attach_orig_headers && !$attach_orig_message) { # attach a header section only my $hdr_8bit = $msginfo->header_8bit || grep(tr/\x00-\x7F//c, @prefix_lines); my $hdr_utf8 = 1; if ($hdr_8bit) { for (@prefix_lines, @{$msginfo->orig_header}) { if (tr/\x00-\x7F//c && !is_valid_utf_8($_)) { $hdr_utf8 = 0; last } } } # RFC 6522 Encoding considerations for text/rfc822-headers: # 7-bit is sufficient for normal mail headers, however, if the # headers are broken or extended and require encoding to make them # legal 7-bit content, they MAY be encoded with quoted-printable # as defined in [MIME]. # RFC 6532 section 3.5: allows newly defined MIME types to permit # content-transfer-encoding, and it allows content-transfer-encoding # for message/global. # RFC 6533: Note that [RFC6532] relaxed a restriction from MIME [RFC2046] # regarding the use of Content-Transfer-Encoding in new "message" # subtypes. This specification (RFC 6533) explicitly allows the use # of Content-Transfer-Encoding in message/global-headers and # message/global-delivery-status. my $headers_mime_type = $flat ? 'text/plain' : $hdr_8bit && $hdr_utf8 ? 'message/global-headers' # RFC 6533 : 'text/rfc822-headers'; # RFC 6522 # [rt.cpan.org #98737] MIME::Tools 5.505 prohibits quoted-printable # for message/global-headers. Fixed by a later release. # my $headers_mime_encoding = # !$hdr_8bit ? '7bit' : # $headers_mime_type =~ m{^text/}i || MIME::Entity->VERSION > 5.505 # ? 'quoted-printable' : '8bit'; my $headers_mime_encoding = $hdr_8bit ? '8bit' : '7bit'; ll(4) && do_log(4,"build_mime_entity: attaching original ". "header section, MIME type: %s, encoding: %s", $headers_mime_type, $headers_mime_encoding); # RFC 6533 section 6.3. Interoperability considerations: # It is important that message/global-headers media type is not # converted to a charset other than UTF-8. As a result, implementations # MUST NOT include a charset parameter with this media type. eval { # make sure _our_ source line number is reported on failure $entity->attach( Data => [@prefix_lines, @{$msginfo->orig_header}], Type => $headers_mime_type, Encoding => $headers_mime_encoding, Filename => $headers_mime_type eq 'message/global-headers' ? 'header.u8hdr' : 'header.hdr', Disposition => 'inline', Description => 'Message header section', ); $multipart_cnt++; 1; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; die $eval_stat; }; } elsif (defined $msginfo && $attach_orig_message) { # attach a complete message my $password; if ($msg_format eq 'attach') { # not 'arf' and not 'dsn' $password = $msginfo->attachment_password; # already have it? if (!defined $password) { # make one, and store it for later $password = make_password(c('attachment_password'), $msginfo); $msginfo->attachment_password($password); } } if ($msg_format eq 'attach' && # not 'arf' and not 'dsn' defined $password && $password ne '') { # attach as a ZIP archive $password = 'X' x length($password); # can't hurt to wipe out do_log(4, "build_mime_entity: attaching entire original message as zip"); my $archive_fn = wrap_message_into_archive($msginfo,\@prefix_lines); local($1); $archive_fn =~ m{([^/]*)\z}; my $att_filename = $1; eval { # make sure _our_ source line number is reported on failure my $att = $entity->attach( # RFC 2046 Type => 'application/zip', Filename => $att_filename, Path => $archive_fn, Encoding => 'base64', Disposition => 'attachment', Description => 'Original message', ); $multipart_cnt++; 1; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; die $eval_stat; }; } else { # attach as a normal message do_log(4, "build_mime_entity: attaching entire original message, plain"); my $orig_mail_as_body; 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') { # will be handled by ->attach } elsif ($msg->isa('MIME::Entity')) { die "attaching a MIME::Entity object is not implemented"; } else { $orig_mail_as_body = Amavis::MIME::Body::OnOpenFh->new($msginfo->mail_text, \@prefix_lines, $msginfo->skip_bytes); $orig_mail_as_body or die "Can't create Amavis::MIME::Body object: $!"; } # RFC 6532 section 3.7: Internationalized messages in message/global # format MUST only be transmitted as authorized by [RFC6531] # or within a non-SMTP environment that supports these messages. my $message_mime_type = $flat ? 'text/plain' : $msginfo->smtputf8 && $msginfo->header_8bit ? 'message/global' # RFC 6532 : 'message/rfc822'; # [rt.cpan.org #98737] MIME::Tools 5.505 prohibits quoted-printable # for message/global. Fixed by a later release. my $message_mime_encoding = !$msginfo->header_8bit && !$msginfo->body_8bit ? '7bit' : $message_mime_type =~ m{^text/}i || MIME::Entity->VERSION > 5.505 ? 'quoted-printable' : '8bit'; eval { # make sure _our_ source line number is reported on failure my $att = $entity->attach( # RFC 2046, RFC 6532 Type => $message_mime_type, Encoding => $message_mime_encoding, Data => defined $orig_mail_as_body ? [] : !$msginfo->skip_bytes ? $msg : substr($$msg, $msginfo->skip_bytes), # Path => $msginfo->mail_text_fn, $flat ? () : (Disposition => 'attachment', Filename => 'message', Description => 'Original message'), # RFC 6532: File extension ".u8msg" is suggested for message/global ); # direct access to tempfile handle $att->bodyhandle($orig_mail_as_body) if defined $orig_mail_as_body; $multipart_cnt++; 1; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; die $eval_stat; }; } } $entity->make_singlepart if $multipart_cnt < 2; $entity; # return the constructed MIME::Entity } # If $msg_format is 'dsn' generate a delivery status notification according # to RFC 6522 (ex RFC 3462, RFC 1892), RFC 3464 (ex RFC 1894) and RFC 3461 # (ex RFC 1891). # If $msg_format is 'arf' generate an abuse report according to RFC 5965 # - "An Extensible Format for Email Feedback Reports". If $msg_format is # 'attach', generate a report message and attach the original message. # If $msg_format is 'plain', generate a simple (flat) mail with the only # MIME part being the original message (abuse@yahoo.com can't currently # handle attachments in reports). Returns a message object, or undef if # DSN is requested but not needed. # $request_type: dsn, release, requeue, report # $msg_format: dsn, arf, attach, plain, resend # $feedback_type: abuse, dkim, fraud, miscategorized, not-spam, # opt-out, virus, other # sub delivery_status_notification($$$;$$$$) { # ..._or_report my($msginfo,$dsn_per_recip_capable,$builtins_ref, $notif_recips,$request_type,$feedback_type,$msg_format) = @_; my $notification; my $suppressed = 0; my $is_smtputf8 = $msginfo->smtputf8; # UTF-8 allowed if (!defined($msg_format)) { $msg_format = $request_type eq 'dsn' ? 'dsn' : $request_type eq 'report' ? c('report_format') : c('release_format'); } my($is_arf,$is_dsn,$is_attach,$is_plain) = (0) x 4; if ($msg_format eq 'dsn') { $is_dsn = 1 } elsif ($msg_format eq 'arf') { $is_arf = 1 } elsif ($msg_format eq 'attach') { $is_attach = 1 } else { $is_plain = 1 } # 'plain' my $dsn_time = $msginfo->rx_time; # time of dsn creation - same as message # use a reception time for consistency and to be resilient to clock jumps $dsn_time = Time::HiRes::time if !$dsn_time; # now, if missing my $rfc2822_dsn_time = rfc2822_timestamp($dsn_time); my $sender = $msginfo->sender; my $dsn_passed_on = $msginfo->dsn_passed_on; # NOTIFY=SUCCESS passed to MTA my $per_recip_data = $msginfo->per_recip_data; my $all_rejected = 0; if (@$per_recip_data) { $all_rejected = 1; for my $r (@$per_recip_data) { if ($r->recip_destiny != D_REJECT || $r->recip_smtp_response !~ /^5/) { $all_rejected = 0; last } } } my($min_spam_level, $max_spam_level) = minmax(map($_->spam_level, @{$msginfo->per_recip_data})); $min_spam_level = 0 if !defined $min_spam_level; $max_spam_level = 0 if !defined $max_spam_level; my $is_credible = $msginfo->sender_credible || ''; my $os_fingerprint = $msginfo->client_os_fingerprint; my($cutoff_byrecip_maps, $cutoff_bysender_maps); my($dsn_cutoff_level_bysender, $dsn_cutoff_level); if ($is_dsn && $sender ne '') { # for null sender it doesn't matter, as DSN will not be sent regardless if ($is_credible) { do_log(3, "DSN: sender is credible (%s), SA: %.3f, <%s>", $is_credible, $max_spam_level, $sender); $cutoff_byrecip_maps = ca('spam_crediblefrom_dsn_cutoff_level_maps'); $cutoff_bysender_maps = ca('spam_crediblefrom_dsn_cutoff_level_bysender_maps'); } else { do_log(5, "DSN: sender NOT credible, SA: %.3f, <%s>", $max_spam_level, $sender); $cutoff_byrecip_maps = ca('spam_dsn_cutoff_level_maps'); $cutoff_bysender_maps = ca('spam_dsn_cutoff_level_bysender_maps'); } $dsn_cutoff_level_bysender = lookup2(0,$sender,$cutoff_bysender_maps); } my $txt_recip = ''; # per-recipient part of dsn text according to RFC 3464 my($any_succ,$any_fail,$any_delayed) = (0,0,0); local($1); for my $r (!$is_dsn ? () : @$per_recip_data) { # prepare per-recip fields my $recip = $r->recip_addr; my $smtp_resp = $r->recip_smtp_response; my $recip_done = $r->recip_done; # 2=relayed to MTA, 1=faked deliv/quarant my $ccat_name = $r->setting_by_contents_category(\%ccat_display_names); $ccat_name = "NonBlocking:$ccat_name" if !defined($r->blocking_ccat); my $spam_level = $r->spam_level; if (!$recip_done) { my $fwd_m = $r->delivery_method; if (!defined $fwd_m) { do_log(-2,"TROUBLE: recipient not done, undefined delivery_method: ". "<%s> %s", $recip,$smtp_resp); } elsif ($fwd_m eq '') { # e.g. milter # as far as we are concerned all is ok, delivery will be performed # by a helper program or MTA $smtp_resp = "250 2.5.0 Ok, continue delivery"; } else { do_log(-2,"TROUBLE: recipient not done: <%s> %s", $recip,$smtp_resp); } } my $smtp_resp_class = $smtp_resp =~ /^(\d)/ ? $1 : '0'; my $smtp_resp_code = $smtp_resp =~ /^(\d+)/ ? $1 : '0'; my $dsn_notify = $r->dsn_notify; my($notify_on_failure,$notify_on_success,$notify_on_delay,$notify_never) = (0,0,0,0); if (!defined($dsn_notify)) { $notify_on_failure = $notify_on_delay = 1; } else { for (@$dsn_notify) { # validity of the list has already been checked if ($_ eq 'FAILURE') { $notify_on_failure = 1 } elsif ($_ eq 'SUCCESS') { $notify_on_success = 1 } elsif ($_ eq 'DELAY') { $notify_on_delay = 1 } elsif ($_ eq 'NEVER') { $notify_never = 1 } } } if ($notify_never || $sender eq '') { $notify_on_failure = $notify_on_success = $notify_on_delay = 0; } my $dest = $r->recip_destiny; my $remote_or_local = $recip_done==2 ? 'from MTA' : $recip_done==1 ? '.' : # this agent 'status-to-be-passed-back'; # warn_sender is an old relic and does not fit well into DSN concepts; # we'll sneak it in, pretending to cause a DELAY notification my $warn_sender = $notify_on_delay && $smtp_resp_class eq '2' && $recip_done==2 && $r->setting_by_contents_category(cr('warnsender_by_ccat')); ll(5) && do_log(5, "dsn: %s %s %s <%s> -> <%s>: on_succ=%d, on_dly=%d, ". "on_fail=%d, never=%d, warn_sender=%s, DSN_passed_on=%s, ". "destiny=%s, mta_resp: \"%s\"", $remote_or_local, $smtp_resp_code, $ccat_name, $sender, $recip, $notify_on_success, $notify_on_delay, $notify_on_failure, $notify_never, $warn_sender, $dsn_passed_on, $dest, $smtp_resp); # clearly log common cases to facilitate troubleshooting; # first look for some standard reasons for not sending a DSN if ($smtp_resp_class eq '4') { do_log(4, "DSN: TMPFAIL %s %s %s, not to be reported: <%s> -> <%s>", $remote_or_local,$smtp_resp_code,$ccat_name,$sender,$recip); } elsif ($smtp_resp_class eq '5' && $dest==D_REJECT && ($dsn_per_recip_capable || $all_rejected)) { do_log(4, "DSN: FAIL %s %s %s, status propagated back: <%s> -> <%s>", $remote_or_local,$smtp_resp_code,$ccat_name,$sender,$recip); } elsif ($smtp_resp_class eq '5' && !$notify_on_failure) { $suppressed = 1; do_log($recip_done==2 ? 0 : 4, # log level 0 for remotes, RFC 3461 5.2.2d "DSN: FAIL %s %s %s, %s requested to be IGNORED: <%s> -> <%s>", $remote_or_local,$smtp_resp_code,$ccat_name, $notify_never?'explicitly':'implicitly', $sender, $recip); } elsif ($smtp_resp_class eq '2' && !$notify_on_success && !$warn_sender) { my $fmt = $dest==D_DISCARD ? "SUCC (discarded) %s %s %s, destiny=DISCARD" : "SUCC %s %s %s, no DSN requested"; do_log(5, "DSN: $fmt: <%s> -> <%s>", $remote_or_local,$smtp_resp_code,$ccat_name,$sender,$recip); } elsif ($smtp_resp_class eq '2' && $notify_on_success && $dsn_passed_on && !$warn_sender) { do_log(5, "DSN: SUCC %s %s %s, DSN parameters PASSED-ON: <%s> -> <%s>", $remote_or_local,$smtp_resp_code,$ccat_name,$sender,$recip); } elsif ($notify_never || $sender eq '') { # test sender just in case $suppressed = 1; do_log(5, "DSN: NEVER %s %s, <%s> -> %s", $smtp_resp_code,$ccat_name,$sender,$recip); # next, look for some good _excuses_ for not sending a DSN } elsif ($dest==D_DISCARD) { # requested by final_*_destiny $suppressed = 1; do_log(4, "DSN: FILTER %s %s %s, destiny=DISCARD: <%s> -> <%s>", $remote_or_local,$smtp_resp_code,$ccat_name,$sender,$recip); } elsif (defined $r->dsn_suppress_reason) { $suppressed = 1; do_log(3, "DSN: FILTER %s %s, suppress reason: %s, <%s> -> <%s>", $smtp_resp_code, $ccat_name, $r->dsn_suppress_reason, $sender,$recip); } elsif (defined $dsn_cutoff_level_bysender && $spam_level >= $dsn_cutoff_level_bysender) { $suppressed = 1; do_log(3, "DSN: FILTER %s %s, spam level %.3f exceeds cutoff %s%s, ". "<%s> -> <%s>", $smtp_resp_code, $ccat_name, $spam_level, $dsn_cutoff_level_bysender, !$is_credible ? '' : ", (credible: $is_credible)", $sender, $recip); } elsif (defined($cutoff_byrecip_maps) && ( $dsn_cutoff_level=lookup2(0,$recip,$cutoff_byrecip_maps), defined($dsn_cutoff_level) && ( $spam_level >= $dsn_cutoff_level || ( $r->recip_blacklisted_sender && !$r->recip_whitelisted_sender) ) ) ) { $suppressed = 1; do_log(3, "DSN: FILTER %s %s, spam level %.3f exceeds ". "by-recipient cutoff %s%s, <%s> -> <%s>", $smtp_resp_code, $ccat_name, $spam_level, $dsn_cutoff_level, !$is_credible ? '' : ", (credible: $is_credible)", $sender, $recip); } elsif ($msginfo->is_bulk && ccat_maj($r->contents_category) > CC_CLEAN) { $suppressed = 1; do_log(3, "DSN: FILTER %s %s, suppressed, bulk mail (%s), <%s> -> <%s>", $smtp_resp_code,$ccat_name,$msginfo->is_bulk,$sender,$recip); } elsif ($os_fingerprint =~ /^Windows\b/ && # hard-coded limits! !$msginfo->dkim_envsender_sig && # a hack $spam_level >= ($os_fingerprint=~/^Windows XP(?![^(]*\b2000 SP)/ ? 5 : 8)) { $os_fingerprint =~ /^(\S+\s+\S+)/; do_log(3, "DSN: FILTER %s %s, suppressed for mail from %s ". "at %s, score=%s, <%s> -> <%s>", $smtp_resp_code, $ccat_name, $1, $msginfo->client_addr, $spam_level, $sender,$recip); } else { # RFC 3461, section 5.2.8: "A single DSN may describe attempts to deliver # a message to multiple recipients of that message. If a DSN is issued # for some recipients in an SMTP transaction and not for others according # to the rules above, the DSN SHOULD NOT contain information for # recipients for whom DSNs would not otherwise have been issued." $txt_recip .= "\n"; # empty line between groups of per-recipient fields my $dsn_orcpt = $r->dsn_orcpt; if (defined $dsn_orcpt) { # RFC 6533: systems generating a message/global-delivery-status # body part SHOULD use the utf-8-address form of the UTF-8 address # type for all addresses containing characters outside the ASCII # repertoire. These systems SHOULD upconvert the utf-8-addr-xtext # or the utf-8-addr-unitext form of a UTF-8 address type in the # ORCPT parameter to the utf-8-address form of a UTF-8 address type # in the "Original-Recipient:" field. my($addr_type, $addr) = orcpt_encode($dsn_orcpt, $is_smtputf8); $txt_recip .= "Original-Recipient: $addr_type;$addr\n"; # as octets } my $remote_mta = $r->recip_remote_mta; my $final_recip_encoded; { # normalize recipient address (like UTF-8 decoding) my($addr_type, $addr) = orcpt_decode(';'.quote_rfc2821_local($recip)); ($addr_type, $addr) = orcpt_encode($addr_type.';'.$addr, $is_smtputf8); $final_recip_encoded = $addr_type.';'.$addr; } if (defined $dsn_orcpt || $remote_mta eq '' || $r->recip_final_addr eq $recip) { $txt_recip .= "Final-Recipient: $final_recip_encoded\n"; } else { $txt_recip .= "X-NextToLast-Final-Recipient: $final_recip_encoded\n"; # normalize final recipient address (e.g. UTF-8 decoding) my($addr_type, $addr) = orcpt_decode(';'.quote_rfc2821_local($r->recip_final_addr)); ($addr_type, $addr) = orcpt_encode($addr_type.';'.$addr, $is_smtputf8); $txt_recip .= "Final-Recipient: $addr_type;$addr\n"; } my($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg); local($1,$2,$3); if ($smtp_resp =~ /^ (\d{3}) [ \t-] [ \t]* ([245] \. \d{1,3} \. \d{1,3})? \s* (.*) \z/xs) { ($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg) = ($1,$2,$3); } else { $smtp_resp_msg = $smtp_resp; } if ($smtp_resp_enhcode eq '' && $smtp_resp_class =~ /^([245])\z/) { $smtp_resp_enhcode = "$1.0.0"; } my $action; # failed / relayed / delivered / expanded if ($recip_done == 2) { # truly forwarded to MTA $action = $smtp_resp_class eq '5' ? 'failed' # remote reject : $smtp_resp_class ne '2' ? undef # shouldn't happen : !$dsn_passed_on ? 'relayed' # relayed to non-conforming MTA : $warn_sender ? 'delayed' # disguised as a DELAY notification : undef; # shouldn't happen } elsif ($recip_done == 1) { # a faked delivery to bit bucket or to a quarantine $action = $smtp_resp_class eq '5' ? 'failed' # local reject : $smtp_resp_class eq '2' ? 'delivered' # discard / bit bucket : undef; # shouldn't happen } elsif (!defined($recip_done) || $recip_done == 0) { $action = $smtp_resp_class eq '2' ? 'relayed' #???? : undef; # shouldn't happen } defined $action or die "Assert failed: $smtp_resp, $smtp_resp_class, ". "$recip_done, $dsn_passed_on"; if ($action eq 'failed') { $any_fail=1 } elsif ($action eq 'delayed') { $any_delayed=1 } else { $any_succ=1 } $txt_recip .= "Action: $action\n"; $txt_recip .= "Status: $smtp_resp_enhcode\n"; my $rem_smtp_resp = $r->recip_remote_mta_smtp_response; if ($warn_sender && $action eq 'delayed') { $smtp_resp = '250 2.6.0 Bad message, but will be delivered anyway'; } elsif ($remote_mta ne '' && $rem_smtp_resp ne '') { $txt_recip .= "Remote-MTA: dns; $remote_mta\n"; $smtp_resp = $rem_smtp_resp; } elsif ($smtp_resp !~ /\n/ && length($smtp_resp) > 78-23) { # wrap magic # take liberty to wrap our own SMTP responses $smtp_resp = wrap_string("x" x (23-11) . $smtp_resp, 78-11,'','',0); # length(" 554 5.0.0 ") = 11; length("Diagnostic-Code: smtp; ") = 23 # insert and then remove prefix to maintain consistent wrapped size $smtp_resp =~ s/^x{12}//; # wrap response code according to RFC 3461 section 9.2 $smtp_resp = join("\n", @{wrap_smtp_resp($smtp_resp)}); } $smtp_resp =~ s/\n(?![ \t])/\n /gs; $txt_recip .= "Diagnostic-Code: smtp; $smtp_resp\n"; # RFC 6533 adds optional field Localized-Diagnostic $txt_recip .= "Last-Attempt-Date: $rfc2822_dsn_time\n"; my $final_log_id = $msginfo->log_id; $final_log_id .= '/' . $msginfo->mail_id if defined $msginfo->mail_id; $txt_recip .= sprintf("Final-Log-ID: %s\n", $final_log_id); do_log(2, "DSN: NOTIFICATION: Action:%s, %s %s %s, spam level %.3f, ". "<%s> -> <%s>", $action, $recip_done==2 && $action ne 'delayed' ? 'RELAYED' : 'LOCAL', $smtp_resp_code, $ccat_name, $spam_level, $sender, $recip); } } # endfor per_recip_data # prepare a per-message part of a report my $txt_msg = ''; my $myhost = c('myhostname'); # my FQDN (DNS) name, UTF-8 octets $myhost = $is_smtputf8 ? idn_to_utf8($myhost) : idn_to_ascii($myhost); my $dsn_envid = $msginfo->dsn_envid; # ENVID is encoded as xtext: RFC 3461 if ($is_dsn) { # DSN - per-msg part of dsn text according to RFC 3464 my $conn = $msginfo->conn_obj; my $from_mta = $conn->smtp_helo; my $client_ip = $conn->client_ip; $txt_msg .= "Reporting-MTA: dns; $myhost\n"; $txt_msg .= "Received-From-MTA: dns; $from_mta ([$client_ip])\n" if $from_mta ne ''; $txt_msg .= "Arrival-Date: ". rfc2822_timestamp($msginfo->rx_time) ."\n"; my $dsn_envid = $msginfo->dsn_envid; # ENVID is encoded as xtext: RFC 3461 if (defined $dsn_envid) { $dsn_envid = sanitize_str(xtext_decode($dsn_envid)); $txt_msg .= "Original-Envelope-Id: $dsn_envid\n"; } } elsif ($is_arf) { # abuse report format - RFC 5965 # abuse, dkim, fraud, miscategorized, not-spam, opt-out, virus, other $txt_msg .= "Version: 1\n"; # required $txt_msg .= "Feedback-Type: $feedback_type\n"; # required # User-Agent must comply with RFC 2616, section 14.43 my $ua_version = "$myproduct_name/$myversion_id ($myversion_date)"; $txt_msg .= "User-Agent: $ua_version\n"; # required $txt_msg .= "Reporting-MTA: dns; $myhost\n"; # optional fields: # RFC 6692: Report generators that include an Arrival-Date report field # MAY choose to express the value of that date in Universal Coordinated # Time (UTC) to enable simpler correlation with local records at sites # that are following the provisions of RFC 6302. $txt_msg .= 'Arrival-Date: '; $txt_msg .= rfc2822_utc_timestamp($msginfo->rx_time) . "\n"; # $txt_msg .= rfc2822_timestamp($msginfo->rx_time) . "\n"; my $cl_ip_addr = $msginfo->client_addr; if (defined $cl_ip_addr) { $cl_ip_addr = 'IPv6:'.$cl_ip_addr if $cl_ip_addr =~ /:[0-9a-f]*:/i && $cl_ip_addr !~ /^IPv6:/i; $txt_msg .= "Source-IP: $cl_ip_addr\n"; } # RFC 6692 (was: draft-kucherawy-marf-source-ports): my $cl_ip_port = $msginfo->client_port; $txt_msg .= "Source-Port: $cl_ip_port\n" if defined $cl_ip_port; my $dsn_envid = $msginfo->dsn_envid; # ENVID is encoded as xtext: RFC 3461 if (defined $dsn_envid) { $dsn_envid = sanitize_str(xtext_decode($dsn_envid)); $txt_msg .= "Original-Envelope-Id: $dsn_envid\n"; } $txt_msg .= "Original-Mail-From: " . $msginfo->sender_smtp . "\n"; for my $r (@$per_recip_data) { $txt_msg .= "Original-Rcpt-To: " . $r->recip_addr_smtp . "\n"; } my $sigs_ref = $msginfo->dkim_signatures_valid; if ($sigs_ref) { for my $sig (@$sigs_ref) { my $type = $sig->isa('Mail::DKIM::DkSignature') ? 'DK' : 'DKIM'; $txt_msg .= sprintf("Reported-Domain: %s (valid %s signature by)\n", $sig->domain, $type); } } if (c('enable_dkim_verification')) { for (Amavis::DKIM::generate_authentication_results($msginfo,0)) { my $h = $_; $h =~ tr/\n//d; # remove potential folding points $txt_msg .= "Authentication-Results: $h\n"; } } $txt_msg .= "Incidents: 1\n"; # Reported-URI } my($txt_8bit, $txt_utf8); my($delivery_status_mime_type, $delivery_status_mime_subtype); if ($is_dsn || $is_arf) { $txt_8bit = ($txt_msg=~tr/\x00-\x7F//c) + ($txt_recip=~tr/\x00-\x7F//c); $txt_utf8 = !$txt_8bit || (is_valid_utf_8($txt_msg) && is_valid_utf_8($txt_recip)); $delivery_status_mime_subtype = $is_arf ? 'feedback-report' : $txt_utf8 && ($is_smtputf8 || $txt_8bit) ? 'global-delivery-status' : 'delivery-status'; $delivery_status_mime_type = 'message/' . $delivery_status_mime_subtype; } if ( $is_arf || $is_plain || $is_attach || ($is_dsn && ($any_succ || $any_fail || $any_delayed)) ) { my(@hdr_to) = $notif_recips ? qquote_rfc2821_local(@$notif_recips) : map($_->recip_addr_smtp, @$per_recip_data); $_ = mail_addr_idn_to_ascii($_) for @hdr_to; my $hdr_from = $msginfo->setting_by_contents_category( $is_dsn ? cr('hdrfrom_notify_sender_by_ccat') : $request_type eq 'report' ? cr('hdrfrom_notify_report_by_ccat') : cr('hdrfrom_notify_release_by_ccat') ); # make sure it's in octets $hdr_from = expand_variables(safe_encode_utf8($hdr_from)); # use the provided template text my(%mybuiltins) = %$builtins_ref; # make a local copy # not really needed, these header fields are overridden later $mybuiltins{'f'} = safe_decode_utf8($hdr_from); $mybuiltins{'T'} = \@hdr_to; $mybuiltins{'d'} = $rfc2822_dsn_time; $mybuiltins{'report_format'} = $msg_format; $mybuiltins{'feedback_type'} = $feedback_type; # RFC 3461 section 6.2: "If a DSN contains no notifications of # delivery failure, the MTA SHOULD return only the header section." my $dsn_ret = $msginfo->dsn_ret; my $attach_full_msg = !$is_dsn ? 1 : (defined $dsn_ret && $dsn_ret eq 'FULL' && $any_fail); if ($attach_full_msg && $is_dsn) { # apologize in the log, we should have supplied the full message, yet # RFC 3461 section 6.2 gives us an excuse: "However, if the length of the # message is greater than some implementation-specified length, the MTA # MAY return only the headers even if the RET parameter specified FULL." do_log(1, "DSN RET=%s requested, but we'll only attach a header section", $dsn_ret); $attach_full_msg = 0; # override, just attach a header section } my $template_ref = $msginfo->setting_by_contents_category( $is_dsn ? cr('notify_sender_templ_by_ccat') : $request_type eq 'report' ? cr('notify_report_templ_by_ccat') : cr('notify_release_templ_by_ccat') ); my $report_str_ref = expand($template_ref, \%mybuiltins); # 'multipart/report' MIME type is defined in RFC 6522. The report-type # parameter identifies the type of report. The parameter is the MIME # subtype of the second body part of the multipart/report. my $report_entity = build_mime_entity($report_str_ref, $msginfo, !$is_dsn && !$is_arf ? 'multipart/mixed' : "multipart/report; report-type=$delivery_status_mime_subtype", $msg_format, $is_plain, 1, $attach_full_msg); my $head = $report_entity->head; # RFC 3464: The From field of the message header section of the DSN SHOULD # contain the address of a human who is responsible for maintaining the # mail system at the Reporting MTA site (e.g. Postmaster), so that a reply # to the DSN will reach that person. # Override header fields from the template: eval { $head->replace('From', $hdr_from); 1 } or do { chomp $@; die $@ }; eval { $head->replace('To', join(', ',@hdr_to)); 1 } or do { chomp $@; die $@ }; eval { $head->replace('Date', $rfc2822_dsn_time); 1 } or do { chomp $@; die $@ }; if ($is_dsn || $is_arf) { # attach a delivery-status or a feedback-report ll(4) && do_log(4,"dsn: creating mime part %s, %s", $delivery_status_mime_type, !$txt_8bit ? 'us-ascii' : $txt_utf8 ? 'valid UTF-8' : '8bit but *not* UTF-8'); eval { # make sure our source line number is reported in case of failure # RFC 6533: Note that [RFC6532] relaxed a restriction from MIME # [RFC2046] regarding the use of Content-Transfer-Encoding in new # "message" subtypes. This specification explicitly allows the # use of Content-Transfer-Encoding in message/global-headers and # message/global-delivery-status. # RFC 5965: Encoding considerations for message/feedback-report: # "7bit" encoding is sufficient and MUST be used to maintain # readability when viewed by non-MIME mail readers. $report_entity->add_part( MIME::Entity->build( Top => 0, Type => $delivery_status_mime_type, Data => $txt_msg . $txt_recip, $delivery_status_mime_subtype ne 'global-delivery-status' ? () : (Charset => 'UTF-8'), Encoding => $txt_8bit ? '8bit' : '7bit', Disposition => 'inline', Filename => $is_arf ? 'arf_status' : $delivery_status_mime_subtype eq 'global-delivery-status' ? 'dsn_status.u8dsn' : 'dsn_status.dsn', Description => $is_arf ? "\u$feedback_type report" : $any_fail ? 'Delivery error report' : $any_delayed ? 'Delivery delay report' : 'Delivery report', ), 1); # insert as a second mime part (at offset 1) 1; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; die $eval_stat; }; } my $mailfrom = $is_dsn ? '' # DSN envelope sender must be empty : mail_addr_idn_to_ascii( unquote_rfc2821_local( (parse_address_list($hdr_from))[0] )); $notification = Amavis::In::Message->new; $notification->rx_time($dsn_time); $notification->log_id($msginfo->log_id); $notification->partition_tag($msginfo->partition_tag); $notification->parent_mail_id($msginfo->mail_id); $notification->mail_id(scalar generate_mail_id()); $notification->conn_obj($msginfo->conn_obj); $notification->originating( ($request_type eq 'dsn' || $request_type eq 'report') ? 1 : 0); $notification->mail_text($report_entity); $notification->body_type($txt_8bit ? '8BITMIME' : '7BIT'); $notification->add_contents_category(CC_CLEAN,0); my(@recips) = $notif_recips ? @$notif_recips : map($_->recip_addr, @$per_recip_data); if ($request_type eq 'dsn' || $request_type eq 'report') { my $bcc = $msginfo->setting_by_contents_category(cr('dsn_bcc_by_ccat')); push(@recips, $bcc) if defined $bcc && $bcc ne ''; } if (grep( / [^\x00-\x7F] .*? \@ [^@]* \z/sx && is_valid_utf_8($_), ($mailfrom, @recips) )) { # localpart is non-ASCII UTF-8, we must use SMTPUTF8 do_log(2, 'DSN notification requires SMTPUTF8'); $notification->smtputf8(1); } else { $_ = mail_addr_idn_to_ascii($_) for ($mailfrom, @recips); } $notification->sender($mailfrom); $notification->sender_smtp(qquote_rfc2821_local($mailfrom)); $notification->auth_submitter('<>'); $notification->auth_user(c('amavis_auth_user')); $notification->auth_pass(c('amavis_auth_pass')); $notification->recips(\@recips, 1); if (defined $hdr_from) { my(@rfc2822_from) = map(unquote_rfc2821_local($_), parse_address_list($hdr_from)); $notification->rfc2822_from($rfc2822_from[0]); } my $notif_m = c('notify_method'); $_->delivery_method($notif_m) for @{$notification->per_recip_data}; } do_log(5, 'delivery_status_notification: notif %d bytes, suppressed: %s', length($notification), $suppressed ? 'yes' : 'no'); # $suppressed is true if DNS would be needed, but either the sender requested # that DSN is not to be sent, or it is believed the bounce would not reach # the correct sender (faked sender with viruses or spam); # $notification is undef if DSN is not needed ($notification, $suppressed); } # Return a triple of arrayrefs of quoted recipient addresses (the first lists # recipients with successful delivery status, the second lists all the rest), # plus a list of short per-recipient delivery reports for failed deliveries, # that can be used in the first MIME part (the free text format) of delivery # status notifications. # sub delivery_short_report($) { my $msginfo = $_[0]; my(@succ_recips, @failed_recips, @failed_recips_full); for my $r (@{$msginfo->per_recip_data}) { my $remote_mta = $r->recip_remote_mta; my $smtp_resp = $r->recip_smtp_response; my $qrecip_addr = scalar(qquote_rfc2821_local($r->recip_addr)); if ($r->recip_destiny == D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)) { push(@succ_recips, $qrecip_addr); } else { push(@failed_recips, $qrecip_addr); push(@failed_recips_full, sprintf("%s:%s\n %s", $qrecip_addr, (!defined($remote_mta)||$remote_mta eq '' ?'' :" [$remote_mta] said:"), $smtp_resp)); } } (\@succ_recips, \@failed_recips, \@failed_recips_full); } # Build a new MIME::Entity object based on the original mail, but hopefully # safer to mail readers: conventional mail header fields are retained, # original mail becomes an attachment of type 'message/rfc822' or # 'message/global'. Text in $first_part becomes the first MIME part # of type 'text/plain', $first_part may be a scalar string or a ref # to a list of lines # sub defanged_mime_entity($$) { my($msginfo,$first_part) = @_; my $new_entity; $_ = safe_encode(c('bdy_encoding'), $_) for (ref $first_part ? @$first_part : $first_part); eval { # make sure _our_ source line number is reported in case of failure $new_entity = MIME::Entity->build( Type => 'multipart/mixed', 'X-Mailer' => undef); 1; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; die $eval_stat; }; # reinserting some of the original header fields to a new header, sanitized my $hdr_edits = $msginfo->header_edits; if (!$hdr_edits) { $hdr_edits = Amavis::Out::EditHeader->new; $msginfo->header_edits($hdr_edits); } my(%desired_field); for (qw(Received From Sender To Cc Reply-To Date Message-ID Resent-From Resent-Sender Resent-To Resent-Cc Resent-Date Resent-Message-ID In-Reply-To References Subject Comments Keywords Organization Organisation User-Agent X-Mailer DKIM-Signature DomainKey-Signature)) { $desired_field{lc($_)} = 1 }; local($1,$2); for my $curr_head (@{$msginfo->orig_header}) { # array of header fields # obsolete RFC 822 syntax allowed whitespace before colon my($field_name, $field_body) = $curr_head =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s ? ($1, $2) : (undef, $curr_head); if ($desired_field{lc($field_name)}) { # only desired header fields # protect NUL, CR, and characters with codes above \377 $field_body =~ s{ ( [^\001-\014\016-\377] ) } { sprintf(ord($1)>255 ? '\\x{%04x}' : '\\x{%02x}', ord($1)) }xgse; # protect NL in illegal all-whitespace continuation lines $field_body =~ s{\n([ \t]*)(?=\n)}{\\012$1}gs; $field_body =~ s{^(.{995}).{4,}$}{$1...}gm; # truncate lines to 998 chomp($field_body); # note that field body is already folded if (lc($field_name) eq 'subject') { # needs to be inserted directly into new header section so that it # can be subjected to header edits, like inserting ***UNCHECKED*** eval { $new_entity->head->add($field_name,$field_body); 1 } or do {chomp $@; die $@}; } else { $hdr_edits->append_header($field_name,$field_body,2); } } } eval { my $cnt_8bit = $first_part =~ tr/\x00-\x7F//c; $new_entity->attach( Type => 'text/plain', Data => $first_part, Charset => c('bdy_encoding'), Encoding => !$cnt_8bit ? '7bit' : $cnt_8bit > 0.2 * length($first_part) ? 'base64' : 'quoted-printable', ); 1; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; die $eval_stat; }; # prepend a Return-Path to make available the envelope sender address my $rp = sprintf("Return-Path: %s\n", $msginfo->sender_smtp); my $orig_mail_as_body; 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') { # will be handled by ->attach } elsif ($msg->isa('MIME::Entity')) { die "attaching a MIME::Entity object is not implemented"; } else { $orig_mail_as_body = Amavis::MIME::Body::OnOpenFh->new($msginfo->mail_text, [$rp], $msginfo->skip_bytes); $orig_mail_as_body or die "Can't create Amavis::MIME::Body object: $!"; } eval { my $att = $new_entity->attach( # RFC 2046 Type => ($msginfo->smtputf8 && $msginfo->header_8bit ? 'message/global' : 'message/rfc822') . '; x-spam-type=original', Encoding => $msginfo->header_8bit || $msginfo->body_8bit ? '8bit':'7bit', Data => defined $orig_mail_as_body ? [] : !$msginfo->skip_bytes ? $msg : substr($$msg, $msginfo->skip_bytes), # Path => $msginfo->mail_text_fn, Description => 'Original message', Filename => 'message', Disposition => 'attachment', ); # direct access to tempfile handle $att->bodyhandle($orig_mail_as_body) if defined $orig_mail_as_body; 1; } or do { my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; die $eval_stat; }; $new_entity; } # Fill-in a message object with information based on a quarantined mail. # Expects $msginfo->mail_text to be a file handle (not a Mime::Entity object), # leaves it positioned at the beginning of a mail body (not to be relied upon). # If given a BSMTP file, expects that it contains a single message only. # sub msg_from_quarantine($$$) { my($msginfo,$request_type,$feedback_type) = @_; my $fh = $msginfo->mail_text; my $sender_override = $msginfo->sender; my $recips_data_override = $msginfo->per_recip_data; my $quarantine_id = $msginfo->parent_mail_id; $quarantine_id = '' if !defined $quarantine_id; my $reporting = $request_type eq 'report'; my $release_m; if ($request_type eq 'requeue') { $release_m = c('requeue_method'); $release_m ne '' or die "requeue_method is unspecified"; } else { # 'release' or 'report' $release_m = c('release_method'); $release_m = c('notify_method') if !defined $release_m || $release_m eq ''; $release_m ne '' or die "release_method and notify_method are unspecified"; } $msginfo->originating(1); # (also enables DKIM signing) $msginfo->add_contents_category(CC_CLEAN,0); $msginfo->auth_submitter('<>'); $msginfo->auth_user(c('amavis_auth_user')); $msginfo->auth_pass(c('amavis_auth_pass')); $fh->seek($msginfo->skip_bytes, 0) or die "Can't rewind mail file: $!"; my $bsmtp = 0; # message stored in an RFC 2442 format? my($qid,$sender,@recips_all,@recips_blocked); my $have_recips_blocked = 0; my $curr_head; my $ln; my $eof = 0; my $position = 0; my $offset_bytes = 0; # file position just past the prefixed header fields # extract envelope information from the quarantine file do_log(4, "msg_from_quarantine: releasing %s", $quarantine_id); for (;;) { if ($eof) { $ln = "\n" } else { $! = 0; $ln = $fh->getline; if (!defined($ln)) { $eof = 1; $ln = "\n"; # fake a missing header/body separator line $! == 0 or die "Error reading file ".$msginfo->mail_text_fn.": $!"; } } if ($ln =~ /^[ \t]/) { $curr_head .= $ln } else { my $next_head = $ln; local($1,$2); local($_) = $curr_head; chomp; s/\n(?=[ \t])//gs; # unfold if (!defined($curr_head)) { # first time } elsif (/^(?:EHLO|HELO)(?: |$)/i) { $bsmtp = 1; } elsif (/^MAIL FROM:[ \t]*(<.*>)/i) { $bsmtp = 1; $sender = $1; $sender = unquote_rfc2821_local($sender); } elsif ( $bsmtp && /^RCPT TO:[ \t]*(<.*>)/i) { push(@recips_all, unquote_rfc2821_local($1)); } elsif ( $bsmtp && /^(?:DATA|NOOP)$/i) { } elsif ( $bsmtp && /^RSET$/i) { $sender = undef; @recips_all = (); @recips_blocked = (); $qid = undef; } elsif ( $bsmtp && /^QUIT$/i) { last; } elsif (!$bsmtp && /^Delivered-To:/si) { } elsif (!$bsmtp && /^(Return-Path|X-Envelope-From):[ \t]*(.*)$/si) { if (!defined $sender) { my(@addr_list) = parse_address_list($2); @addr_list >= 1 or die "Address missing in $1"; @addr_list <= 1 or die "More than one address in $1"; $sender = mail_addr_idn_to_ascii(unquote_rfc2821_local($addr_list[0])); } } elsif (!$bsmtp && /^X-Envelope-To:[ \t]*(.*)$/si) { my(@addr_list) = parse_address_list($1); push(@recips_all, map(mail_addr_idn_to_ascii(unquote_rfc2821_local($_)), @addr_list)); } elsif (!$bsmtp && /^X-Envelope-To-Blocked:[ \t]*(.*)$/si) { my(@addr_list) = parse_address_list($1); push(@recips_blocked, map(mail_addr_idn_to_ascii(unquote_rfc2821_local($_)), @addr_list)); $have_recips_blocked = 1; } elsif (/^X-Quarantine-ID:[ \t]*(.*)$/si) { $qid = $1; $qid = $1 if $qid =~ /^<(.*)>\z/s; } elsif (!$reporting && /^X-Amavis-(?:Hold|Alert|Modified|PenPals| PolicyBank|OS-Fingerprint):/xsi) { # skip } elsif (!$reporting && /^(?:X-Spam|X-CRM114)-.+:/si) { # skip header fields inserted by us } else { last; # end of known header fields, to be marked as 'skip_bytes' } last if $next_head eq "\n"; # end-of-header-section reached $offset_bytes = $position; # move past last processed header field $curr_head = $next_head; } $position += length($ln); } @recips_blocked = @recips_all if !$have_recips_blocked; # pre-2.6.0 compatib my(@except); if (@recips_blocked < @recips_all) { for my $rec (@recips_all) { push(@except,$rec) if !grep($rec eq $_, @recips_blocked) } } my $sender_smtp = qquote_rfc2821_local($sender); do_log(0,"Quarantined message %s (%s): %s %s -> %s%s", $request_type, $feedback_type, $quarantine_id, $sender_smtp, join(',', qquote_rfc2821_local(@recips_blocked)), !@except ? '' : (", (excluded: ". join(',', qquote_rfc2821_local(@except)) . " )" )); my(@m); if (!defined($qid)) { push(@m, 'missing X-Quarantine-ID') } elsif ($qid ne $quarantine_id) { push(@m, sprintf("stored quar. ID '%s' does not match requested ID '%s'", $qid,$quarantine_id)); } push(@m, 'missing '.($bsmtp?'MAIL FROM':'X-Envelope-From or Return-Path')) if !defined $sender; push(@m, 'missing '.($bsmtp?'RCPT TO' :'X-Envelope-To')) if !@recips_all; do_log(0, "Quarantine %s %s: %s", $request_type, $quarantine_id, join("; ",@m)) if @m; if ($qid ne $quarantine_id) { die "Stored quarantine ID '$qid' does not match ". "requested ID '$quarantine_id'" } if ($bsmtp) { die "Releasing messages in BSMTP format not yet supported ". "(dot de-stuffing not implemented)" } $msginfo->sender($sender); $msginfo->sender_smtp($sender_smtp); $msginfo->recips(\@recips_all); $_->delivery_method($release_m) for @{$msginfo->per_recip_data}; # mark a file location past prefixed header fields where orig message starts $msginfo->skip_bytes($offset_bytes); my $msg_format = $request_type eq 'dsn' ? 'dsn' : $request_type eq 'report' ? c('report_format') : c('release_format'); my $hdr_edits = Amavis::Out::EditHeader->new; $msginfo->header_edits($hdr_edits); if ($msg_format eq 'resend') { if (!defined($recips_data_override)) { $msginfo->recips(\@recips_blocked); # override 'all' by 'blocked' recips } else { # recipients specified in the request override stored info ll(5) && do_log(5, 'overriding recips %s by %s', join(',', qquote_rfc2821_local(@recips_blocked)), join(',', map($_->recip_addr_smtp, @$recips_data_override))); $msginfo->per_recip_data($recips_data_override); } $_->delivery_method($release_m) for @{$msginfo->per_recip_data}; } else { # collect more information from a quarantined message, making it available # to a report generator and to macros during template expansion Amavis::get_body_digest($msginfo, c('mail_digest_algorithm')); Amavis::collect_some_info($msginfo); if (defined($recips_data_override) && ll(5)) { do_log(5, 'overriding recips %s by %s', join(',', qquote_rfc2821_local(@recips_blocked)), join(',', map($_->recip_addr_smtp, @$recips_data_override))); } my($notification,$suppressed) = delivery_status_notification( $msginfo, 0, \%Amavis::builtins, !defined($recips_data_override) ? \@recips_blocked : [ map($_->recip_addr, @$recips_data_override) ], $request_type, $feedback_type, undef); # pushes original quarantined message into an attachment of a notification $msginfo = $notification; } if (defined $sender_override) { # sender specified in the request, overrides stored info do_log(5, "overriding sender %s by %s", $sender, $sender_override); $msginfo->sender($sender_override); $msginfo->sender_smtp(qquote_rfc2821_local($sender_override)); } if ($msg_format eq 'resend') { # keep quarantined message at a top MIME level # Resent-* header fields must precede corresponding Received header field # "Resent-From:" and "Resent-Date:" are required fields! my $hdrfrom_recip = $msginfo->setting_by_contents_category( cr('hdrfrom_notify_recip_by_ccat')); # make sure it's in octets $hdrfrom_recip = expand_variables(safe_encode_utf8($hdrfrom_recip)); if ($msginfo->requested_by eq '') { $hdr_edits->add_header('Resent-From', $hdrfrom_recip); } else { $hdr_edits->add_header('Resent-From', qquote_rfc2821_local($msginfo->requested_by)); $hdr_edits->add_header('Resent-Sender', $hdrfrom_recip) if $hdrfrom_recip ne ''; } my $prd = $msginfo->per_recip_data; $hdr_edits->add_header('Resent-To', $prd && @$prd==1 ? $prd->[0]->recip_addr_smtp : 'undisclosed-recipients:;'); $hdr_edits->add_header('Resent-Date', # time of the release rfc2822_timestamp($msginfo->rx_time)); my $myhost = c('myhostname'); # my FQDN (DNS) name, UTF-8 octets $myhost = $msginfo->smtputf8 ? idn_to_utf8($myhost) :idn_to_ascii($myhost); $hdr_edits->add_header('Resent-Message-ID', sprintf('<%s-%s@%s>', $msginfo->parent_mail_id||'', $msginfo->mail_id||'', $myhost) ); } $hdr_edits->add_header('Received', make_received_header_field($msginfo,1),1); my $bcc = $msginfo->setting_by_contents_category(cr('always_bcc_by_ccat')); if (defined $bcc && $bcc ne '' && $request_type ne 'report') { my $recip_obj = Amavis::In::Message::PerRecip->new; $recip_obj->recip_addr_modified($bcc); # leave recip_addr and recip_addr_smtp undefined to hide it from the log? $recip_obj->recip_addr($bcc); $recip_obj->recip_addr_smtp(qquote_rfc2821_local($bcc)); #**** $recip_obj->recip_is_local( lookup2(0, $bcc, ca('local_domains_maps')) ? 1 : 0); $recip_obj->recip_destiny(D_PASS); $recip_obj->dsn_notify(['NEVER']); $recip_obj->delivery_method(c('notify_method')); $recip_obj->add_contents_category(CC_CLEAN,0); $msginfo->per_recip_data([@{$msginfo->per_recip_data}, $recip_obj]); do_log(2,"adding recipient - always_bcc: %s, delivery method %s", $bcc, $recip_obj->delivery_method); } $msginfo; } 1;