Server IP : 85.214.239.14 / Your IP : 18.117.230.165 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 : /usr/share/perl5/Amavis/Out/ |
Upload File : |
# SPDX-License-Identifier: GPL-2.0-or-later package Amavis::Out::Local; 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_OK = qw(&mail_to_local_mailbox); } use Errno qw(ENOENT EACCES); use Fcntl qw(:flock); #use File::Spec; use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL); use Amavis::Conf qw(:platform c cr ca $QUARANTINEDIR $quarantine_subdir_levels); use Amavis::Out::EditHeader; use Amavis::rfc2821_2822_Tools; use Amavis::Timing qw(section_time); use Amavis::Util qw(snmp_count ll do_log untaint unique_list collect_equal_delivery_recips); use subs @EXPORT_OK; # Deliver to local mailboxes only, ignore the rest: either to directory # (maildir style), or file (Unix mbox). (normally used as a quarantine method) # sub mail_to_local_mailbox(@) { my($msginfo, $initial_submission, $dsn_per_recip_capable, $filter) = @_; # note that recipients of a message being delivered to a quarantine # are typically not the original envelope recipients, but are pseudo # address provided to do_quarantine() based on @quarantine_to_maps; # nevertheless, we do the usual collect_equal_delivery_recips() ritual # here too for consistency # my $logmsg = sprintf("%s via LOCAL: %s", ($initial_submission?'SEND':'FWD'), $msginfo->sender_smtp); my($per_recip_data_ref, $proto_sockname) = collect_equal_delivery_recips($msginfo, $filter, qr/^local:/i); if (!$per_recip_data_ref || !@$per_recip_data_ref) { do_log(5, "%s, nothing to do", $logmsg); return 1; } my(@per_recip_data) = @$per_recip_data_ref; undef $per_recip_data_ref; $proto_sockname = $proto_sockname->[0] if ref $proto_sockname; ll(4) && do_log(4, "delivering to %s, %s -> %s", $proto_sockname, $logmsg, join(',', qquote_rfc2821_local( map($_->recip_final_addr, @per_recip_data)) )); # just use the first one, ignoring failover alternatives local($1); $proto_sockname =~ /^local:(.*)\z/si or die "Bad local method syntax: ".$proto_sockname; my $via_arg = $1; my(@snmp_vars) = !$initial_submission ? ('', 'Relay', 'ProtoLocal', 'ProtoLocalRelay') : ('', 'Submit','ProtoLocal', 'ProtoLocalSubmit', 'Submit'.$initial_submission); snmp_count('OutMsgs'.$_) for @snmp_vars; my $sender = $msginfo->sender; for my $r (@per_recip_data) { # determine a mailbox file for each recipient # each recipient gets his own copy; these are not the original message's # recipients but are mailbox addresses, typically telling where a message # to be quarantined is to be stored my $recip = $r->recip_final_addr; # %local_delivery_aliases emulates aliases map - this would otherwise # be done by MTA's local delivery agent if we gave the message to MTA. # This way we keep interface compatible with other mail delivery # methods. The hash value may be a ref to a pair of fixed strings, # or a subroutine ref (which must return such pair) to allow delayed # (lazy) evaluation when some part of the pair is not yet known # at initialization time. # If no matching entry is found quarantining is skipped. my($mbxname, $suggested_filename); my($localpart,$domain) = split_address($recip); my $ldar = cr('local_delivery_aliases'); # a ref to a hash my $alias = $ldar->{$localpart}; if (ref($alias) eq 'ARRAY') { ($mbxname, $suggested_filename) = @$alias; } elsif (ref($alias) eq 'CODE') { # lazy (delayed) evaluation ($mbxname, $suggested_filename) = &$alias; } elsif ($alias ne '') { ($mbxname, $suggested_filename) = ($alias, undef); } elsif (!exists $ldar->{$localpart}) { do_log(3, "no key '%s' in %s, using a default", $localpart, '%local_delivery_aliases'); ($mbxname, $suggested_filename) = ($QUARANTINEDIR, undef); } if (!defined($mbxname) || $mbxname eq '' || $recip eq '') { my $why = !exists $ldar->{$localpart} ? 1 : $alias eq '' ? 2 : 3; do_log(2, "skip local delivery(%s): <%s> -> <%s>", $why,$sender,$recip); my $smtp_response = "250 2.6.0 Ok, skip local delivery($why)"; $smtp_response .= ", id=" . $msginfo->log_id; $r->recip_smtp_response($smtp_response); $r->recip_done(2); next; } my $ux; # is it a UNIX-style mailbox? my $errn = stat($mbxname) ? 0 : 0+$!; if ($errn == ENOENT) { $ux = 1; # $mbxname is a UNIX-style mailbox (new file) } elsif ($errn != 0) { die "Can't access a mailbox file or directory $mbxname: $!"; } elsif (-f _) { $ux = 1; # $mbxname is a UNIX-style mailbox (existing file) } elsif (!-d _) { die "Mailbox is neither a file nor a directory: $mbxname"; } else { # a directory $ux = 0; # $mbxname is a directory (amavis/maildir style mailbox) my $explicitly_suggested_filename = $suggested_filename ne ''; if ($suggested_filename eq '') { $suggested_filename = $via_arg ne '' ? $via_arg : '%m' } my $mail_id = $msginfo->mail_id; if (!defined($mail_id)) { do_log(-1, "mail_to_local_mailbox: mail_id still undefined!?"); $mail_id = ''; } $suggested_filename =~ 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; # $mbxname = File::Spec->catfile($mbxname, $suggested_filename); $mbxname = "$mbxname/$suggested_filename"; if ($quarantine_subdir_levels>=1 && !$explicitly_suggested_filename) { # using a subdirectory structure to disperse quarantine files local($1,$2); my $subdir = substr($mail_id, 0, 1); $subdir=~/^[A-Z0-9]\z/i or die "Unexpected first char: $subdir"; $mbxname =~ m{^ (.*/)? ([^/]+) \z}xs; my($path,$fname) = ($1,$2); # $mbxname = File::Spec->catfile($path, $subdir, $fname); $mbxname = "$path$subdir/$fname"; # resulting full filename my $errn = stat("$path$subdir") ? 0 : 0+$!; # only test for ENOENT, other errors will be detected later on access if ($errn == ENOENT) { # check/prepare a set of subdirectories do_log(2, "checking/creating quarantine subdirs under %s", $path); for my $d ('A'..'Z','a'..'z','0'..'9') { $errn = stat("$path$d") ? 0 : 0+$!; if ($errn == ENOENT) { mkdir("$path$d", 0750) or die "Can't create dir $path$d: $!"; } } } } } # save location where mail should be stored, prepend a mailbox style $r->recip_mbxname(($ux ? 'mbox' : 'maildir') . ':' . $mbxname); } # # now go ahead and store a message to predetermined files in recip_mbxname; # iterate by groups of recipients with the same mailbox name # @per_recip_data = grep(!$_->recip_done, @per_recip_data); while (@per_recip_data) { my $mbxname = $per_recip_data[0]->recip_mbxname; # first mailbox name # collect all recipient which have the same mailbox file as the first one my(@recips_with_same_mbx) = grep($_->recip_mbxname eq $mbxname, @per_recip_data); @per_recip_data = grep($_->recip_mbxname ne $mbxname, @per_recip_data); # retrieve mailbox style and a filename local($1,$2); $mbxname =~ /^([^:]*):(.*)\z/; my $ux = $1 eq 'mbox' ? 1 : 0; $mbxname = $2; my(@recips) = map($_->recip_final_addr, @recips_with_same_mbx); @recips = unique_list(\@recips); my $smtp_response; { # a block is used as a 'switch' statement - 'last' will exit from it do_log(1,"local delivery: %s -> %s, mbx=%s", $msginfo->sender_smtp, join(", ",@recips), $mbxname); my $eval_stat; my($mp,$pos); my $errn = stat($mbxname) ? 0 : 0+$!; section_time('stat-mbx'); local $SIG{CHLD} = 'DEFAULT'; local $SIG{PIPE} = 'IGNORE'; # don't signal on a write to a widowed pipe eval { # try to open the mailbox file for writing if (!$ux) { # one mail per file, will create specified file if ($errn == ENOENT) { # good, no file, as expected } elsif ($errn != 0) { die "File $mbxname not accessible, refuse to write: $!"; } elsif (!-f _) { die "File $mbxname is not a regular file, refuse to write"; } else { die "File $mbxname already exists, refuse to overwrite"; } if ($mbxname =~ /\.gz\z/) { $mp = Amavis::IO::Zlib->new; # ?how to request an exclusive access? $mp->open($mbxname,'wb') or die "Can't create gzip file $mbxname: $!"; } else { $mp = IO::File->new; # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502] $mp->open($mbxname, untaint(O_CREAT|O_EXCL|O_WRONLY), 0640) or die "Can't create file $mbxname: $!"; binmode($mp,':bytes') or die "Can't cancel :utf8 mode: $!"; } } else { # append to a UNIX-style mailbox # deliver only to non-executable regular files if ($errn == ENOENT) { # if two processes try creating the same new UNIX-style mailbox # file at the same time, one will tempfail at this point, with # its mail delivery to be retried later by MTA $mp = IO::File->new; # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502] $mp->open($mbxname, untaint(O_CREAT|O_EXCL|O_APPEND|O_WRONLY),0640) or die "Can't create file $mbxname: $!"; } elsif ($errn==0 && !-f _) { die "Mailbox $mbxname is not a regular file, refuse to deliver"; } elsif (-x _ || -X _) { die "Mailbox file $mbxname is executable, refuse to deliver"; } else { $mp = IO::File->new; # O_WRONLY etc. can become tainted in Perl5.8.9 [perlbug #62502] $mp->open($mbxname, untaint(O_APPEND|O_WRONLY), 0640) or die "Can't append to $mbxname: $!"; } binmode($mp,':bytes') or die "Can't cancel :utf8 mode: $!"; flock($mp,LOCK_EX) or die "Can't lock mailbox file $mbxname: $!"; $mp->seek(0,2) or die "Can't position mailbox file to its tail: $!"; $pos = $mp->tell; # remember where we started } section_time('open-mbx'); 1; } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; $smtp_response = $eval_stat =~ /^timed out\b/ ? "450 4.4.2" : "451 4.5.0"; $smtp_response .= " Local delivery(1) to $mbxname failed: $eval_stat"; die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout }; last if defined $eval_stat; # exit block, not the loop my $failed = 0; $eval_stat = undef; eval { # if things fail from here on, try to restore mailbox state if ($ux) { # a null return path may not appear in the 'From ' delimiter line my $snd = $sender eq '' ? 'MAILER-DAEMON' # as in sendmail & Postfix : $msginfo->sender_smtp; # if the envelope sender contains spaces, tabs, or newlines, # the program (like qmail-local) replaces them with hyphens $snd =~ s/[ \t\n]/-/sg; # date/time in asctime (ctime) format, English month names! # RFC 4155 and qmail-local require UTC time, no timezone name $mp->printf("From %s %s\n", $snd, scalar gmtime($msginfo->rx_time) ) or die "Can't write mbox separator line to $mbxname: $!"; } my $hdr_edits = $msginfo->header_edits; if (!$hdr_edits) { $hdr_edits = Amavis::Out::EditHeader->new; $msginfo->header_edits($hdr_edits); } $hdr_edits->delete_header('Return-Path'); $hdr_edits->prepend_header('Delivered-To', join(', ',@recips)); $hdr_edits->prepend_header('Return-Path', $msginfo->sender_smtp); my($received_cnt,$file_position) = $hdr_edits->write_header($msginfo,$mp,!$initial_submission); if ($received_cnt > 110) { # 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"; } 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 (!$ux) { # do it in blocks for speed if we can if (!defined $msg) { # empty mail } elsif (ref $msg eq 'SCALAR') { $mp->print(substr($$msg,$file_position)) or die "Can't write to $mbxname: $!"; } elsif ($msg->isa('MIME::Entity')) { die "quarantining a MIME::Entity object is not implemented"; } else { my($nbytes,$buff); while (($nbytes = $msg->read($buff,32768)) > 0) { $mp->print($buff) or die "Can't write to $mbxname: $!"; } defined $nbytes or die "Error reading: $!"; } } else { # for UNIX-style mailbox file delivery: escape 'From ' # mail(1) and elm(1) recognize /^From / as a message delimiter # only after a blank line, which is correct. Other MUAs like mutt, # thunderbird, kmail and pine need all /^From / lines escaped. # See also http://en.wikipedia.org/wiki/Mbox and RFC 4155. if (!defined $msg) { # empty mail } elsif (ref $msg eq 'SCALAR') { my $buff = substr($$msg,$file_position); # $buff =~ s/^From />From /gm; # mboxo format $buff =~ s/^(?=\>*From )/>/gm; # mboxrd format $mp->print($buff) or die "Can't write to $mbxname: $!"; } elsif ($msg->isa('MIME::Entity')) { die "quarantining a MIME::Entity object is not implemented"; } else { my $ln; my $blank_line = 1; # need to copy line-by-line, slow for ($! = 0; defined($ln=$msg->getline); $! = 0) { # see wikipedia and RFC 4155 for "From " escaping conventions $mp->print('>') or die "Can't write to $mbxname: $!" if $ln =~ /^(?:>*)From /; # escape all "From " lines # if $blank_line && $ln =~ /^(?:>*)From /; # only after blankline $mp->print($ln) or die "Can't write to $mbxname: $!"; $blank_line = $ln eq "\n"; } defined $ln || $! == 0 or die "Error reading: $!"; } } # must append an empty line for a Unix mailbox format $mp->print("\n") or die "Can't write to $mbxname: $!" if $ux; 1; } or do { # trouble $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; if ($ux && defined($pos)) { $mp->flush or die "Can't flush file $mbxname: $!"; $can_truncate or do_log(-1, "Truncating a mailbox file will most likely fail"); # try to restore UNIX-style mailbox to previous size; # Produces a fatal error if truncate isn't implemented on the system $mp->truncate($pos) or die "Can't truncate file $mbxname: $!"; } $failed = 1; die $eval_stat if $eval_stat =~ /^timed out\b/; # resignal timeout }; # if ($ux) { # # explicit unlocking is unnecessary, close will do a flush & unlock # $mp->flush or die "Can't flush mailbox file $mbxname: $!"; # flock($mp,LOCK_UN) or die "Can't unlock mailbox $mbxname: $!"; # } $mp->close or die "Error closing $mbxname: $!"; undef $mp; if (!$failed) { $smtp_response = "250 2.6.0 Ok, delivered to $mbxname"; snmp_count('OutMsgsDelivers'); my $size = $msginfo->msg_size; snmp_count( ['OutMsgsSize'.$_, $size, 'C64'] ) for @snmp_vars; } elsif ($@ =~ /^timed out\b/) { $smtp_response = "450 4.4.2 Local delivery to $mbxname timed out"; snmp_count('OutMsgsAttemptFails'); } elsif ($@ =~ /too many hops\b/i) { $smtp_response = "554 5.4.6 Rejected delivery to mailbox $mbxname: $@"; snmp_count('OutMsgsRejects'); } else { $smtp_response = "451 4.5.0 Local delivery to mailbox $mbxname ". "failed: $@"; snmp_count('OutMsgsAttemptFails'); } } # end of block, 'last' within the block brings us here do_log(-1, "%s", $smtp_response) if $smtp_response !~ /^2/; $smtp_response .= ", id=" . $msginfo->log_id; for my $r (@recips_with_same_mbx) { $r->recip_smtp_response($smtp_response); $r->recip_done(2); $r->recip_mbxname($smtp_response =~ /^2/ ? $mbxname : undef); } } section_time('save-to-local-mailbox'); } 1;