Server IP : 85.214.239.14 / Your IP : 3.145.35.234 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/root/proc/3/task/3/cwd/proc/2/task/2/cwd/usr/share/perl5/Amavis/SpamControl/ |
Upload File : |
# SPDX-License-Identifier: GPL-2.0-or-later package Amavis::SpamControl::SpamAssassin; 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); } use subs @EXPORT_OK; use Errno qw(ENOENT EACCES EAGAIN EBADF); use FileHandle; use Mail::SpamAssassin; use Amavis::Conf qw(:platform :confvars :sa $daemon_user c cr ca); use Amavis::IO::FileHandle; use Amavis::Lookup qw(lookup lookup2); use Amavis::ProcControl qw(exit_status_str proc_status_ok kill_proc run_command run_as_subprocess collect_results collect_results_structured); use Amavis::rfc2821_2822_Tools; use Amavis::Timing qw(section_time get_rusage); use Amavis::Util qw(ll do_log do_log_safe sanitize_str prolong_timer add_entropy min max minmax get_deadline safe_encode_utf8_inplace); use vars qw(@sa_debug_fac); sub getCommonSAModules { my $self = $_[0]; my(@modules) = qw( Mail::SpamAssassin::Locker Mail::SpamAssassin::Locker::Flock Mail::SpamAssassin::Locker::UnixNFSSafe Mail::SpamAssassin::PersistentAddrList Mail::SpamAssassin::DBBasedAddrList Mail::SpamAssassin::AutoWhitelist Mail::SpamAssassin::BayesStore Mail::SpamAssassin::BayesStore::DBM Mail::SpamAssassin::PerMsgLearner Net::DNS::RR::SOA Net::DNS::RR::NS Net::DNS::RR::MX Net::DNS::RR::A Net::DNS::RR::AAAA Net::DNS::RR::PTR Net::DNS::RR::CNAME Net::DNS::RR::DNAME Net::DNS::RR::OPT Net::DNS::RR::TXT Net::DNS::RR::SPF Net::DNS::RR::NAPTR Net::DNS::RR::RP Net::DNS::RR::HINFO Net::DNS::RR::AFSDB Net::CIDR::Lite URI URI::Escape URI::Heuristic URI::QueryParam URI::Split URI::URL URI::WithBase URI::_foreign URI::_generic URI::_ldap URI::_login URI::_query URI::_segment URI::_server URI::_userpass URI::_idna URI::_punycode URI::data URI::ftp URI::gopher URI::http URI::https URI::ldap URI::ldapi URI::ldaps URI::mailto URI::mms URI::news URI::nntp URI::pop URI::rlogin URI::rsync URI::rtsp URI::rtspu URI::sip URI::sips URI::snews URI::ssh URI::telnet URI::tn3270 URI::urn URI::urn::oid URI::file URI::file::Base URI::file::Unix URI::file::Win32 ); # DBD::mysql # DBI::Const::GetInfo::ANSI DBI::Const::GetInfo::ODBC DBI::Const::GetInfoType # Mail::SpamAssassin::BayesStore::SQL # Mail::SpamAssassin::SQLBasedAddrList # ??? ArchiveIterator Reporter Getopt::Long Sys::Syslog lib @modules; } sub getSA2Modules { qw(Mail::SpamAssassin::UnixLocker Mail::SpamAssassin::BayesStoreDBM ); # Mail::SpamAssassin::SpamCopURI } sub getSA31Modules { qw( ); # Mail::SpamAssassin::BayesStore::MySQL # Mail::SpamAssassin::BayesStore::PgSQL } sub getSA32Modules { qw(Mail::SpamAssassin::Bayes Mail::SpamAssassin::Bayes::CombineChi Mail::SpamAssassin::Locales Encode::Detect ); # Mail::SpamAssassin::BayesStore::MySQL # Mail::SpamAssassin::BayesStore::PgSQL # /var/db/spamassassin/compiled/.../Mail/SpamAssassin/CompiledRegexps/body_0.pm } sub getSAPlugins { my $self = $_[0]; my(@modules); my $sa_version_num = $self->{version_num}; push(@modules, qw(Hashcash RelayCountry SPF URIDNSBL)) if $sa_version_num>=3; push(@modules, qw(DKIM)) if $sa_version_num >= 3.001002; if ($sa_version_num >= 3.001000) { push(@modules, qw( AWL AccessDB AntiVirus AutoLearnThreshold DCC MIMEHeader Pyzor Razor2 ReplaceTags TextCat URIDetail WhiteListSubject)); # 'DomainKeys' plugin fell out of fashion with SA 3.2.0, don't load it # 'SpamCop' loads Net::SMTP and Net::Cmd, not needed otherwise } if ($sa_version_num >= 3.002000) { push(@modules, qw( BodyEval DNSEval HTMLEval HeaderEval MIMEEval RelayEval URIEval WLBLEval ASN Bayes BodyRuleBaseExtractor Check HTTPSMismatch OneLineBodyRuleType ImageInfo Rule2XSBody Shortcircuit VBounce)); } if ($sa_version_num >= 3.004000) { push(@modules, qw(AskDNS)); } $_ = 'Mail::SpamAssassin::Plugin::'.$_ for @modules; my(%mod_names) = map(($_,1), @modules); # add supporting modules push(@modules, qw(Razor2::Client::Agent)) if $mod_names{'Mail::SpamAssassin::Plugin::Razor2'}; # push(@modules, qw(IP::Country::Fast)) # if $mod_names{'Mail::SpamAssassin::Plugin::RelayCountry'}; push(@modules, qw(Mail::DKIM Mail::DKIM::Verifier Net::DNS::Resolver)) if $mod_names{'Mail::SpamAssassin::Plugin::DKIM'}; push(@modules, qw(Image::Info Image::Info::GIF Image::Info::JPEG Image::Info::PNG Image::Info::BMP Image::Info::TIFF)) if $mod_names{'Mail::SpamAssassin::Plugin::ImageInfo'}; if ($mod_names{'Mail::SpamAssassin::Plugin::SPF'}) { if ($sa_version_num < 3.002000) { # only the old Mail::SPF::Query was supported push(@modules, qw(Mail::SPF::Query)); } else { # SA 3.2.0 supports both the newer Mail::SPF and the old Mail::SPF::Query # but we won't be loading the Mail::SPF::Query push(@modules, qw( Mail::SPF Mail::SPF::Server Mail::SPF::Request Mail::SPF::Mech Mail::SPF::Mech::A Mail::SPF::Mech::PTR Mail::SPF::Mech::All Mail::SPF::Mech::Exists Mail::SPF::Mech::IP4 Mail::SPF::Mech::IP6 Mail::SPF::Mech::Include Mail::SPF::Mech::MX Mail::SPF::Mod Mail::SPF::Mod::Exp Mail::SPF::Mod::Redirect Mail::SPF::SenderIPAddrMech Mail::SPF::v1::Record Mail::SPF::v2::Record NetAddr::IP NetAddr::IP::Util auto::NetAddr::IP::_compV6 auto::NetAddr::IP::short auto::NetAddr::IP::InetBase::inet_any2n auto::NetAddr::IP::InetBase::inet_n2ad auto::NetAddr::IP::InetBase::inet_n2dx auto::NetAddr::IP::InetBase::inet_ntoa auto::NetAddr::IP::InetBase::ipv6_aton auto::NetAddr::IP::InetBase::ipv6_ntoa )); } } if ($mod_names{'Mail::SpamAssassin::Plugin::DomainKeys'} || $mod_names{'Mail::SpamAssassin::Plugin::DKIM'}) { push(@modules, qw( Crypt::OpenSSL::RSA auto::Crypt::OpenSSL::RSA::new_public_key auto::Crypt::OpenSSL::RSA::new_key_from_parameters auto::Crypt::OpenSSL::RSA::get_key_parameters auto::Crypt::OpenSSL::RSA::import_random_seed Digest::SHA Error)); } # HTML/HeadParser.pm # do_log(5, "getSAPlugins %s: %s", $sa_version_num, join(', ',@modules)); @modules; } # invoked by a parent process before forking and chrooting # sub loadSpamAssassinModules { my $self = $_[0]; my $sa_version_num = $self->{version_num}; my @modules; # modules to be loaded before chroot takes place push(@modules, $self->getCommonSAModules); if (!defined($sa_version_num)) { die "loadSpamAssassinModules: unknown version of Mail::SpamAssassin"; } elsif ($sa_version_num < 3) { push(@modules, $self->getSA2Modules); } elsif ($sa_version_num >= 3.001 && $sa_version_num < 3.002) { push(@modules, $self->getSA31Modules); } elsif ($sa_version_num >= 3.002) { push(@modules, $self->getSA32Modules); } push(@modules, $self->getSAPlugins); my $missing; $missing = Amavis::Boot::fetch_modules('PRE-COMPILE OPTIONAL MODULES', 0, @modules) if @modules; do_log(2, 'INFO: SA version: %s, %.6f, no optional modules: %s', $self->{version}, $sa_version_num, join(' ',@$missing)) if ref $missing && @$missing; } # invoked by a parent process before forking but after chrooting # sub initializeSpamAssassinLogger { my $self = $_[0]; local($1,$2,$3,$4,$5,$6); # just in case if (!Mail::SpamAssassin::Logger->UNIVERSAL::can('add')) { # old SA? } elsif (!Mail::SpamAssassin::Logger::add(method => 'Amavislog', debug => $sa_debug )) { do_log(-1,"Mail::SpamAssassin::Logger::add failed"); } else { # successfully rigged SpamAssassin with our logger Mail::SpamAssassin::Logger::remove('stderr'); # remove a default SA logger if (defined $sa_debug && $sa_debug =~ /[A-Za-z_,-]/) { # looks like a list of SA debug facilities push(@sa_debug_fac, split(/[ \t]*,[ \t]*/, $sa_debug)); } else { unshift(@sa_debug_fac, 'info', $sa_debug ? 'all' : () ); } } } # invoked by a parent process before forking but after chrooting # sub new_SpamAssassin_instance { my($self,$running_as_parent) = @_; # pick next available number as an instance name my $sa_instance_name = sprintf('%s', scalar @{$self->{instances}}); do_log(1, "initializing Mail::SpamAssassin (%s)", $sa_instance_name); my $sa_version_num = $self->{version_num}; my(@new_sa_debug_fac); for my $fac (@sa_debug_fac) { # handle duplicates and negation: foo,nofoo,x,x my $bfac = $fac; $bfac =~ s/^none\z/noall/i; $bfac =~ s/^no(?=.)//si; @new_sa_debug_fac = grep(!/^(no)?\Q$bfac\E\z/si, @new_sa_debug_fac); push(@new_sa_debug_fac, $fac); } do_log(2,"SpamAssassin debug facilities: %s", join(',',@sa_debug_fac)); my $sa_args = { debug => !@sa_debug_fac ? undef : \@sa_debug_fac, save_pattern_hits => grep(lc($_) eq 'all', @sa_debug_fac) ? 1 : 0, dont_copy_prefs => 1, require_rules => 1, stop_at_threshold => 0, need_tags => 'TIMING,LANGUAGES,RELAYCOUNTRY,ASN,ASNCIDR', local_tests_only => $sa_local_tests_only, home_dir_for_helpers => $helpers_home, rules_filename => $sa_configpath, site_rules_filename => $sa_siteconfigpath, userprefs_filename => $sa_userprefs_file, skip_prng_reseeding => 1, # we'll do it ourselves (SA 3.4.0) # PREFIX => '/usr/local', # DEF_RULES_DIR => '/usr/local/share/spamassassin', # LOCAL_RULES_DIR => '/etc/mail/spamassassin', # LOCAL_STATE_DIR => '/var/lib/spamassassin', #see Mail::SpamAssassin man page for other options }; if ($sa_version_num < 3.001005 && !defined $sa_args->{LOCAL_STATE_DIR}) { $sa_args->{LOCAL_STATE_DIR} = '/var/lib' } # don't ignore sa-update rules local($1,$2,$3,$4,$5,$6); # avoid Perl bug, $1 gets tainted in compile_now my $spamassassin_obj = Mail::SpamAssassin->new($sa_args); # $Mail::SpamAssassin::DEBUG->{rbl}=-3; # $Mail::SpamAssassin::DEBUG->{rulesrun}=4+64; if ($running_as_parent) { # load SA config files and rules, try to preload most modules $spamassassin_obj->compile_now; $spamassassin_obj->call_plugins("prefork_init"); # since SA 3.4.0 } if (ll(2) && !@{$self->{instances}}) { # created the first/main/only SA instance if ($spamassassin_obj->UNIVERSAL::can('get_loaded_plugins_list')) { my(@plugins) = $spamassassin_obj->get_loaded_plugins_list; do_log(2, "SpamAssassin loaded plugins: %s", join(', ', sort map { my $n = ref $_; $n =~ s/^Mail::SpamAssassin::Plugin:://; $n } @plugins)); # printf STDOUT ("%s\n", join(", ",@plugins)); # not in use: AccessDB AntiVirus TextCat; ASN BodyRuleBaseExtractor # OneLineBodyRuleType Rule2XSBody Shortcircuit } } # provide a default username my $sa_uname = $spamassassin_obj->{username}; if (!defined $sa_uname || $sa_uname eq '') { $spamassassin_obj->{username} = $sa_uname = $daemon_user } $self->{default_username} = $sa_uname if !defined $self->{default_username}; my $sa_instance = { instance_name => $sa_instance_name, spamassassin_obj => $spamassassin_obj, loaded_user_name => $sa_uname, loaded_user_config => '', conf_backup => undef, conf_backup_additional => {}, }; # remember some initial settings, like %msa_backup in spamd for (qw(username user_dir userstate_dir learn_to_journal)) { if (exists $spamassassin_obj->{$_}) { $sa_instance->{conf_backup_additional}{$_} = $spamassassin_obj->{$_}; } } push(@{$self->{instances}}, $sa_instance); alarm(0); # seems like SA forgets to clear alarm in some cases umask($self->{saved_umask}); # restore our umask, SA clobbered it section_time('SA new'); $sa_instance; } sub new { my($class, $scanner_name,$module,@args) = @_; my(%options) = @args; my $self = bless { scanner_name => $scanner_name, options => \%options }, $class; $self->{initialized_stage} = 1; $self->{saved_umask} = umask; my $sa_version = Mail::SpamAssassin->Version; local($1,$2,$3); my $sa_version_num; # turn '3.1.8-pre1' into 3.001008 $sa_version_num = sprintf("%d.%03d%03d", $1,$2,$3) if $sa_version =~ /^(\d+)\.(\d+)(?:\.(\d+))/; # ignore trailing non-digits $self->{version} = $sa_version; $self->{version_num} = $sa_version_num; $self->{default_username} = undef; $self->{instances} = []; $self; } sub init_pre_chroot { my $self = $_[0]; $self->{initialized_stage} == 1 or die "Wrong initialization sequence: " . $self->{initialized_stage}; $self->loadSpamAssassinModules; $self->{initialized_stage} = 2; } sub init_pre_fork { my $self = $_[0]; $self->{initialized_stage} == 2 or die "Wrong initialization sequence: " . $self->{initialized_stage}; $self->initializeSpamAssassinLogger; $self->new_SpamAssassin_instance(1) for (1 .. max(1,$sa_num_instances)); $self->{initialized_stage} = 3; } sub init_child { my $self = $_[0]; $self->{initialized_stage} == 3 or die "Wrong initialization sequence: " . $self->{initialized_stage}; for my $sa_instance (@{$self->{instances}}) { my $spamassassin_obj = $sa_instance->{spamassassin_obj}; next if !$spamassassin_obj; $spamassassin_obj->call_plugins("spamd_child_init"); umask($self->{saved_umask}); # restore our umask, SA may have clobbered it } $self->{initialized_stage} = 4; } sub rundown_child { my $self = $_[0]; for my $sa_instance (@{$self->{instances}}) { my $spamassassin_obj = $sa_instance->{spamassassin_obj}; next if !$spamassassin_obj; do_log(3,'SA rundown_child (%s)', $sa_instance->{instance_name}); $spamassassin_obj->call_plugins("spamd_child_post_connection_close"); umask($self->{saved_umask}); # restore our umask, SA may have clobbered it } $self->{initialized_stage} = 5; } sub call_spamassassin($$$$) { my($self,$msginfo,$lines,$size_limit) = @_; my(@result); my($mail_obj,$per_msg_status); my $which_section = 'SA prepare'; my $saved_pid = $$; my $sa_version_num = $self->{version_num}; my $msg = $msginfo->mail_text; # a file handle or a string ref my $msg_str_ref = $msginfo->mail_text_str; # have an in-memory copy? $msg = $msg_str_ref if ref $msg_str_ref; # pass data to SpamAssassin as ARRAY or GLOB or STRING or STRING_REF my $data_representation = ref($msg) eq 'SCALAR' ? 'STRING' : 'GLOB'; $data_representation = 'STRING_REF' if $data_representation eq 'STRING' && $sa_version_num >= 3.004000; my $data; # this will be passed to SpamAssassin's parser local(*F); if ($data_representation eq 'STRING' || $data_representation eq 'STRING_REF') { $which_section = 'SA msg read'; $data = join('', @$lines); # a string to be passed to SpamAssassin if (!defined $msg) { # empty mail } elsif (ref $msg eq 'SCALAR') { $data .= $$msg; } elsif ($msg->isa('MIME::Entity')) { die "passing a MIME::Entity object to SpamAssassin is not implemented"; } else { # read message into memory, yuck my $file_position = $msginfo->skip_bytes; $msg->seek($file_position, 0) or die "Can't rewind mail file: $!"; my $nbytes; while ( $nbytes=$msg->sysread($data, 32768, length $data) ) { $file_position += $nbytes; last if defined $size_limit && length($data) > $size_limit; } defined $nbytes or die "Error reading: $!"; } if (defined $size_limit && length($data) > $size_limit) { substr($data,$size_limit) = "[...]\n"; } section_time($which_section); } elsif ($data_representation eq 'ARRAY') { # read message into memory, yuck - even worse: line-by-line $which_section = 'SA msg read'; my $ln; my $len = 0; if (defined $size_limit) { $len += length($_) for @$lines } $msg->seek($msginfo->skip_bytes, 0) or die "Can't rewind mail file: $!"; for ($! = 0; defined($ln=<$msg>); $! = 0) { # header section push(@$lines,$ln); if (defined $size_limit) { $len += length($ln); last if $len > $size_limit } last if $ln eq "\n"; } defined $ln || $! == 0 or # returning EBADF at EOF is a perl bug $! == EBADF ? do_log(0,"Error reading mail header section: %s", $!) : die "Error reading mail header section: $!"; if (!defined $size_limit) { for ($! = 0; defined($ln=<$msg>); $! = 0) { push(@$lines,$ln) } # body } else { for ($! = 0; defined($ln=<$msg>); $! = 0) { # body push(@$lines,$ln); $len += length($ln); last if $len > $size_limit; } } defined $ln || $! == 0 or # returning EBADF at EOF is a perl bug $! == EBADF ? do_log(1,"Error reading mail body: %s", $!) : die "Error reading mail body: $!"; $data = $lines; # array of lines to be passed to SpamAssassin section_time($which_section); } my($rusage_self_before, $rusage_children_before, @sa_cpu_usage); my $eval_stat; $which_section = 'SA prelim'; eval { if ($data_representation eq 'GLOB') { # pass mail as a GLOB to SpamAssassin ref($msg) ne 'SCALAR' # expects $msg to be a file handle or die "panic: data_representation is GLOB, but message is in memory"; do_log(2,"truncating a message passed to SA at %d bytes, orig %d", $size_limit, $msginfo->msg_size) if defined $size_limit; # present a virtual file to SA, an original mail file prefixed by @$lines tie(*F,'Amavis::IO::FileHandle'); open(F, $msg,$lines,$size_limit) or die "Can't open SA virtual file: $!"; binmode(F) or die "Can't set binmode on a SA virtual file: $!"; $data = \*F; # a GLOB to be passed to SpamAssassin } $which_section = 'SA userconf'; my $sa_default_username = $self->{default_username}; my $per_recip_data = $msginfo->per_recip_data; $per_recip_data = [] if !$per_recip_data; my $uconf_maps_ref = ca('sa_userconf_maps'); my $uname_maps_ref = ca('sa_username_maps'); $uconf_maps_ref = [] if !$uconf_maps_ref; $uname_maps_ref = [] if !$uname_maps_ref; my(%uconf_filename_available); my(%sa_configs_hash); # collects distinct config names and usernames my $uconf_unsupported = 0; my $r_ind = 0; for my $r (@$per_recip_data) { my($uconf,$uname); my $recip_addr = $r->recip_addr; $uconf = lookup2(0, $recip_addr, $uconf_maps_ref) if @$uconf_maps_ref; $uname = lookup2(0, $recip_addr, $uname_maps_ref) if @$uname_maps_ref; $uconf = '' if !defined $uconf; $uname = $sa_default_username if !defined $uname || $uname eq ''; if ($uconf =~ /^sql:/i) { $uconf = $uname eq $sa_default_username ? '' : 'sql:'.$uname; } if ($uconf =~ /^ldap:/i) { $uconf = $uname eq $sa_default_username ? '' : 'ldap:'.$uname; } if ($sa_version_num < 3.003000 && $uconf ne '') { $uconf = ''; $uconf_unsupported = 1; } if ($uconf eq '') { # ok, no special config required, just using a default } elsif ($uconf =~ /^sql:/i) { # assume data is in SQL, possibly an empty set } elsif ($uconf =~ /^ldap:/i) { # assume data is in LDAP, possibly an empty set } else { $uconf = "$MYHOME/$uconf" if $uconf !~ m{^/}; if ($uconf_filename_available{$uconf}) { # good, already checked and is available, keep it } elsif (defined $uconf_filename_available{$uconf}) { # defined but false, already checked and failed, use a default config $uconf = ''; } else { # check for existence of a SA user configuration/preferences file my(@stat_list) = stat($uconf); # symlinks-friendly my $errn = @stat_list ? 0 : 0+$!; my $msg = $errn == ENOENT ? "does not exist" : $errn ? "is inaccessible: $!" : -d _ ? "is a directory" : !-f _ ? "is not a regular file" : !-r _ ? "is not readable" : undef; if (defined $msg) { do_log(1,'SA user config file "%s" %s, ignoring it', $uconf,$msg); $uconf_filename_available{$uconf} = 0; # defined but false $uconf = ''; # ignoring it, use a default config } else { $uconf_filename_available{$uconf} = 1; } } } # collect lists of recipient indices for each unique config/user pair # the %sa_configs_hash is a two-level hash: on $uconf and $uname my $p = $sa_configs_hash{$uconf}; if (!$p) { $sa_configs_hash{$uconf} = $p = {} } if (!exists $p->{$uname}) { $p->{$uname} = $r_ind } else { $p->{$uname} .= ',' . $r_ind } $r_ind++; } if ($uconf_unsupported) { do_log(5,'SA user config loading unsupported for SA older than 3.3.0'); } # refresh $sa_instance->{loaded_user_name}, just in case for my $sa_instance (@{$self->{instances}}) { my $spamassassin_obj = $sa_instance->{spamassassin_obj}; next if !$spamassassin_obj; my $sa_uname = $spamassassin_obj->{username}; $sa_instance->{loaded_user_name} = defined $sa_uname ? $sa_uname : ''; } my $sa_instance = $self->{instances}[0]; my $curr_conf = $sa_instance->{loaded_user_config}; my $curr_user = $sa_instance->{loaded_user_name}; # switching config files is the most expensive, sort to minimize switching my(@conf_names); # a list of config names for which SA needs to be called; # sorted: current first, baseline second, then the rest push(@conf_names, $curr_conf) if exists $sa_configs_hash{$curr_conf}; push(@conf_names, '') if $curr_conf ne '' && exists $sa_configs_hash{''}; push(@conf_names, grep($_ ne '' && $_ ne $curr_conf, keys %sa_configs_hash)); # call SA checking for each distinct SA userprefs config filename and user for my $conf_user_pair (map { my $c = $_; map([$c,$_], keys %{$sa_configs_hash{$c}}) } @conf_names) { my($uconf,$uname) = @$conf_user_pair; # comma-separated list of recip indices which use this SA config my $rind_list = $sa_configs_hash{$uconf}{$uname}; if (ll(5)) { do_log(5, "SA user config: \"%s\", username: \"%s\", %s, %s", $uconf, $uname, $rind_list, join(', ', map("($_)" . $per_recip_data->[$_]->recip_addr, split(/,/,$rind_list)))); } my $sa_instance; if (@{$self->{instances}} <= 1) { # pick the only choice $sa_instance = $self->{instances}[0]; } else { # choosing a suitably-matching SpamAssassin instance my(@sa_instances_matching_uconf, @sa_instances_matching_both, @sa_instances_available); for my $sa_instance (@{$self->{instances}}) { next if !$sa_instance->{spamassassin_obj}; push(@sa_instances_available, $sa_instance); if ($sa_instance->{loaded_user_config} eq $uconf) { push(@sa_instances_matching_uconf, $sa_instance); if ($sa_instance->{loaded_user_name} eq $uname) { push(@sa_instances_matching_both, $sa_instance); } } } my $fit_descr; if (@sa_instances_matching_both) { # just pick the first $sa_instance = $sa_instances_matching_both[0]; $fit_descr = sprintf('exact fit, %d choices', scalar @sa_instances_matching_both); } elsif (@sa_instances_matching_uconf) { # picking one at random my $j = @sa_instances_matching_uconf <= 1 ? 0 : int(rand(scalar(@sa_instances_matching_uconf))); $sa_instance = $sa_instances_available[$j]; $fit_descr = sprintf('good fit: same config, other user, %d choices', scalar @sa_instances_matching_uconf); } elsif ($uconf eq '') { # the first instance is a good choice for switching to a dflt config $sa_instance = $self->{instances}[0]; $fit_descr = 'need a default config, picking first'; } elsif (@sa_instances_available <= 1) { $sa_instance = $sa_instances_available[0]; $fit_descr = 'different config, picking the only one available'; } elsif (@sa_instances_available == 2) { $sa_instance = $sa_instances_available[1]; $fit_descr = 'different config, picking the second one'; } else { # picking one at random, preferably not the first my $j = 1+int(rand(scalar(@sa_instances_available)-1)); $sa_instance = $sa_instances_available[$j]; $fit_descr = 'different config, picking one at random'; } do_log(2,'SA user config: instance chosen (%s), %s', $sa_instance->{instance_name}, $fit_descr); } my $curr_conf = $sa_instance->{loaded_user_config}; my $curr_user = $sa_instance->{loaded_user_name}; my $spamassassin_obj = $sa_instance->{spamassassin_obj}; if ($curr_conf ne '' && $curr_conf ne $uconf) { # revert SA configuration to its initial state $which_section = 'revert_config'; ref $sa_instance->{conf_backup} or die "panic, no conf_backup available"; for (qw(username user_dir userstate_dir learn_to_journal)) { if (exists $sa_instance->{conf_backup_additional}{$_}) { $spamassassin_obj->{$_} = $sa_instance->{conf_backup_additional}{$_}; } else { delete $spamassassin_obj->{$_}; } } # config leaks fixed in SpamAssassin 3.3.0, SA bug 6205, 6003, 4179 $spamassassin_obj->copy_config($sa_instance->{conf_backup}, undef) or die "copy_config: failed to restore"; $sa_instance->{loaded_user_config} = $curr_conf = ''; do_log(5,"SA user config reverted to a saved copy"); section_time($which_section); } if ($uconf ne '' && $uconf ne $curr_conf) { # load SA user configuration/preferences if (!defined $sa_instance->{conf_backup}) { $which_section = 'save_config'; do_log(5,"SA user config: saving SA user config"); $sa_instance->{conf_backup} = {}; $spamassassin_obj->copy_config(undef, $sa_instance->{conf_backup}) or die "copy_config: failed to save configuration"; section_time($which_section); } $which_section = 'load_config'; # User preferences include scoring options, scores, whitelists # and blacklists, etc, but do not include rule definitions, # privileged settings, etc. unless allow_user_rules is enabled; # and they never include administrator settings if ($uconf =~ /^sql:/) { $uconf eq 'sql:'.$uname or die "panic: loading SA config mismatch: $uname <-> $uconf"; do_log(5,"loading SA user config from SQL %s", $uname); $spamassassin_obj->load_scoreonly_sql($uname); } elsif ($uconf =~ /^ldap:/) { $uconf eq 'ldap:'.$uname or die "panic: loading SA config mismatch: $uname <-> $uconf"; do_log(5,"loading SA user config from LDAP %s", $uname); $spamassassin_obj->load_scoreonly_ldap($uname); } else { do_log(5,"loading SA user config file %s", $uconf); $spamassassin_obj->read_scoreonly_config($uconf); } $sa_instance->{loaded_user_config} = $curr_conf = $uconf; section_time($which_section); } if ($uname ne $curr_user) { $which_section = 'SA switch_user'; do_log(5,'SA user config: switching SA (%s) username "%s" -> "%s"', $sa_instance->{instance_name}, $curr_user, $uname); $spamassassin_obj->signal_user_changed({ username => $uname }); $sa_instance->{loaded_user_name} = $curr_user = $uname; section_time($which_section); } ll(3) && do_log(3, "calling SA parse (%s), SA vers %s, %.6f, ". "data as %s, recips_ind [%s]%s%s", $sa_instance->{instance_name}, $self->{version}, $sa_version_num, $data_representation, $rind_list, ($uconf eq '' ? '' : ", conf: \"$uconf\""), ($uname eq '' ? '' : ", user: \"$uname\"") ); if ($data_representation eq 'GLOB') { seek(F,0,0) or die "Can't rewind a SA virtual file: $!"; } $spamassassin_obj->timer_reset if $spamassassin_obj->UNIVERSAL::can('timer_reset'); $which_section = 'SA parse'; my($remaining_time, $deadline) = get_deadline('SA check', 1, 5); my(@mimepart_digests); for (my(@traversal_stack) = $msginfo->parts_root; my $part = pop @traversal_stack; ) { # pre-order tree traversal my $digest = $part->digest; push(@mimepart_digests, $digest) if defined $digest; push(@traversal_stack, reverse @{$part->children}) if $part->children; } do_log(5,'mimepart digest: %s', $_) for @mimepart_digests; my(%suppl_attrib) = ( 'skip_prng_reseed' => 1, # do not call srand(), we already did it 'return_path' => $msginfo->sender_smtp, 'recipients' => [ map(qquote_rfc2821_local($_->recip_addr), @$per_recip_data[split(/,/, $rind_list)]) ], 'originating' => $msginfo->originating ? 1 : 0, 'message_size' => $msginfo->msg_size, 'body_size' => $msginfo->orig_body_size, !@mimepart_digests ? () : ('mimepart_digests' => \@mimepart_digests), !c('enable_dkim_verification') ? () : ('dkim_signatures' => $msginfo->dkim_signatures_all), !defined $deadline ? () : ('master_deadline' => $deadline), 'rule_hits' => [ # known attributes: rule, area, score, value, ruletype, tflags, descr # { rule=>'AM:TEST1', score=>0.11 }, # { rule=>'TESTTEST', defscore=>0.22, descr=>'my test' }, !defined $size_limit ? () : { rule=>'__TRUNCATED', score=>-0.1, area=>'RAW: ', tflags=>'nice', descr=>"Message size truncated to $size_limit B" }, ], 'amavis_policy_bank_path' => c('policy_bank_path'), ); ($rusage_self_before, $rusage_children_before) = get_rusage(); $mail_obj = $sa_version_num < 3 ? Mail::SpamAssassin::NoMailAudit->new(data=>$data, add_From_line=>0) : $spamassassin_obj->parse( $data_representation eq 'STRING_REF' ? \$data : $data, 0, \%suppl_attrib); section_time($which_section); $which_section = 'SA check'; if (@conf_names <= 1) { do_log(4,"CALLING SA check (%s)", $sa_instance->{instance_name}); } else { do_log(4,"CALLING SA check (%s) for recips: %s", $sa_instance->{instance_name}, join(", ", @{$suppl_attrib{'recipients'}})); } { local($1,$2,$3,$4,$5,$6); # avoid Perl 5.8.x bug, $1 gets tainted $per_msg_status = $spamassassin_obj->check($mail_obj); } do_log(4,"DONE SA check (%s)", $sa_instance->{instance_name}); section_time($which_section); $which_section = 'SA collect'; my($spam_level,$spam_report,$spam_summary,%supplementary_info); { local($1,$2,$3,$4,$5,$6); # avoid Perl 5.8.x taint bug if ($sa_version_num < 3) { $spam_level = $per_msg_status->get_hits; $supplementary_info{'TESTSSCORES'} = $supplementary_info{'TESTS'} = $per_msg_status->get_names_of_tests_hit; } else { $spam_level = $per_msg_status->get_score; for my $t (qw(VERSION SUBVERSION RULESVERSION TESTS TESTSSCORES ADDEDHEADERHAM ADDEDHEADERSPAM AUTOLEARN AUTOLEARNSCORE SC SCRULE SCTYPE LANGUAGES RELAYCOUNTRY ASN ASNCIDR DCCB DCCR DCCREP DKIMDOMAIN DKIMIDENTITY AWLSIGNERMEAN HAMMYTOKENS SPAMMYTOKENS SUBJPREFIX CRM114STATUS CRM114SCORE CRM114CACHEID)) { my $tag_value = $per_msg_status->get_tag($t); if (defined $tag_value) { # for some reason tags ASN and ASNCIDR have UTF8 flag on; # encode any character strings to UTF-8 octets for consistency safe_encode_utf8_inplace($tag_value); # to octets if not already $supplementary_info{$t} = $tag_value; } } } { # fudge my $crm114_status = $supplementary_info{'CRM114STATUS'}; my $crm114_score = $supplementary_info{'CRM114SCORE'}; if (defined $crm114_status && defined $crm114_score) { $supplementary_info{'CRM114STATUS'} = sprintf("%s ( %s )", $crm114_status,$crm114_score); } } # get_report() taints $1 and $2 ! $spam_summary = $per_msg_status->get_report; # $spam_summary = $per_msg_status->get_tag('SUMMARY'); $spam_report = $per_msg_status->get_tag('REPORT'); safe_encode_utf8_inplace($spam_summary); # to octets (if not already) safe_encode_utf8_inplace($spam_report); # to octets (if not already) # fetch the TIMING tag last: $supplementary_info{'TIMING'} = $per_msg_status->get_tag('TIMING'); $supplementary_info{'RUSAGE-SA'} = \@sa_cpu_usage; # filled-in later } # section_time($which_section); # don't bother reporting separately, short $which_section = 'SA check finish'; if (defined $per_msg_status) { $per_msg_status->finish; undef $per_msg_status } if (defined $mail_obj) { $mail_obj->finish if $sa_version_num >= 3; undef $mail_obj } # section_time($which_section); # don't bother reporting separately, short # returning the result as a data structure instead of modifying # the $msginfo objects directly is used to make it possible to run # this subroutine as a subprocess; modifications to $msginfo objects # would be lost if done in a context of a spawned process push(@result, { recip_ind_list => $rind_list, user_config => $uconf, spam_level => $spam_level, spam_report => $spam_report, spam_summary => $spam_summary, supplementary_info => \%supplementary_info, }); } 1; } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" }; $which_section = 'SA finish'; if (defined $per_msg_status) # just in case { $per_msg_status->finish; undef $per_msg_status } if (defined $mail_obj) # just in case { $mail_obj->finish if $sa_version_num >= 3; undef $mail_obj } if ($data_representation eq 'GLOB') { close(F) or die "Can't close SA virtual file: $!"; untie(*F); } umask($self->{saved_umask}); # restore our umask, SA may have clobbered it if ($$ != $saved_pid) { do_log_safe(-2,"PANIC, SA checking produced a clone process ". "of [%s], CLONE [%s] SELF-TERMINATING", $saved_pid,$$); POSIX::_exit(3); # SIGQUIT, avoid END and destructor processing # POSIX::_exit(6); # SIGABRT, avoid END and destructor processing } if ($rusage_self_before && $rusage_children_before) { my($rusage_self_after, $rusage_children_after) = get_rusage(); @sa_cpu_usage = ( $rusage_self_after->{ru_utime} - $rusage_self_before->{ru_utime}, $rusage_self_after->{ru_stime} - $rusage_self_before->{ru_stime}, $rusage_children_after->{ru_utime} - $rusage_children_before->{ru_utime}, $rusage_children_after->{ru_stime} - $rusage_children_before->{ru_stime} ); } # section_time($which_section); if (defined $eval_stat) { chomp $eval_stat; die $eval_stat } # resignal \@result; } sub check { my($self,$msginfo) = @_; $self->{initialized_stage} == 4 or die "Wrong initialization sequence: " . $self->{initialized_stage}; my $scanner_name = $self->{scanner_name}; my $which_section; my $prefix = ''; my($spam_level,$sa_tests,$spam_report,$spam_summary,$supplementary_info_ref); my $hdr_edits = $msginfo->header_edits; my $size_limit; my $mbsl = $self->{options}->{'mail_body_size_limit'}; $mbsl = c('sa_mail_body_size_limit') if !defined $mbsl; if (defined $mbsl) { $size_limit = min(64*1024, $msginfo->orig_header_size) + 1 + min($mbsl, $msginfo->orig_body_size); # don't bother if slightly oversized, it's faster without size checks undef $size_limit if $msginfo->msg_size < $size_limit + 5*1024; } # fake a local delivery agent by inserting a Return-Path $prefix .= sprintf("Return-Path: %s\n", $msginfo->sender_smtp); $prefix .= sprintf("X-Envelope-To: %s\n", join(",\n ",qquote_rfc2821_local(@{$msginfo->recips}))); my $os_fp = $msginfo->client_os_fingerprint; $prefix .= sprintf("X-Amavis-OS-Fingerprint: %s\n", sanitize_str($os_fp)) if defined($os_fp) && $os_fp ne ''; my(@av_tests); for my $r (@{$msginfo->per_recip_data}) { my $spam_tests = $r->spam_tests; push(@av_tests, grep(/^AV[.:].+=/, split(/,/, join(',',map($$_,@$spam_tests))))) if $spam_tests; } $prefix .= sprintf("X-Amavis-AV-Status: %s\n", sanitize_str(join(',',@av_tests))) if @av_tests; $prefix .= sprintf("X-Amavis-PolicyBank: %s\n", c('policy_bank_path')); $prefix .= sprintf("X-Amavis-MessageSize: %d%s\n", $msginfo->msg_size, !defined $size_limit ? '' : ", TRUNCATED to $size_limit"); for my $hf_name (qw( X-CRM114-Status X-CRM114-CacheID X-CRM114-Notice X-CRM114-Action X-DSPAM-Result X-DSPAM-Class X-DSPAM-Signature X-DSPAM-Processed X-DSPAM-Confidence X-DSPAM-Probability X-DSPAM-User X-DSPAM-Factors)) { my $suppl_attr_val = $msginfo->supplementary_info($hf_name); if (defined $suppl_attr_val && $suppl_attr_val ne '') { chomp $suppl_attr_val; $prefix .= sprintf("%s: %s\n", $hf_name, sanitize_str($suppl_attr_val)); } } $which_section = 'SA call'; my($proc_fh,$pid); my $eval_stat; my $results_aref; eval { # NOTE ON TIMEOUTS: SpamAssassin may use timer for its own purpose, # disabling it before returning. It seems it only uses timer when # external tests are enabled. local $SIG{ALRM} = sub { my $s = Carp::longmess("SA TIMED OUT, backtrace:"); # crop at some rather arbitrary limit substr($s,900-3) = '[...]' if length($s) > 900; do_log(-1,"%s",$s); }; prolong_timer('spam_scan_sa_pre', 1, 4); # restart the timer # # note: array @lines at this point contains only prepended synthesized # header fields, but may be extended in sub call_spamassassin() by # reading-in the rest of the message; this may or may not happen in # a separate process (called through run_as_subprocess or directly); # each line must be terminated by a \n character, which must be the # only \n in a line; # my(@lines) = split(/^/m, $prefix, -1); $prefix = undef; if (!$sa_spawned) { $results_aref = call_spamassassin($self,$msginfo,\@lines,$size_limit); } else { ($proc_fh,$pid) = run_as_subprocess(\&call_spamassassin, $self,$msginfo,\@lines,$size_limit); my($results,$child_stat) = collect_results_structured($proc_fh,$pid,'spawned SA',200*1024); $results_aref = $results->[0] if defined $results; } 1; } or do { $eval_stat = $@ ne '' ? $@ : "errno=$!" }; section_time($which_section) if $sa_spawned; $which_section = 'SA done'; prolong_timer('spam_scan_sa'); # restart the timer if ($results_aref) { # for each group of recipients using the same SA userconf file for my $h (@$results_aref) { my $rind_list = $h->{recip_ind_list}; my(@r_list) = @{$msginfo->per_recip_data}[split(/,/,$rind_list)]; my $uconf = $h->{user_config}; $spam_level = $h->{spam_level}; $spam_report = $h->{spam_report}; $spam_summary = $h->{spam_summary}; $supplementary_info_ref = $h->{supplementary_info}; $supplementary_info_ref = {} if !$supplementary_info_ref; $sa_tests = $supplementary_info_ref->{'TESTSSCORES'}; add_entropy($spam_level,$sa_tests); my $score_factor = $self->{options}->{'score_factor'}; if (defined $spam_level && defined $score_factor) { $spam_level *= $score_factor; } do_log(3,"spam_scan: score=%s autolearn=%s tests=[%s] recips=%s", $spam_level, $supplementary_info_ref->{'AUTOLEARN'}, $sa_tests, $rind_list); my(%sa_tests_h); if (defined $sa_tests && $sa_tests ne 'none') { for my $t (split(/,[ \t]*/, $sa_tests)) { my($test_name,$score) = split(/=/, $t, 2); $sa_tests_h{$test_name} = $score; } } my $dkim_adsp_suppress; if (exists $sa_tests_h{'DKIM_ADSP_DISCARD'}) { # must honour ADSP 'discardable', suppress a bounce do_log(2,"spam_scan: dsn_suppress_reason DKIM_ADSP_DISCARD"); $dkim_adsp_suppress = 1; } $msginfo->supplementary_info('SCORE-'.$scanner_name, $spam_level); $msginfo->supplementary_info('VERDICT-'.$scanner_name, $spam_level >= 5 ? 'Spam' : $spam_level < 1 ? 'Ham' : 'Unknown'); for my $r (@r_list) { $r->spam_level( ($r->spam_level || 0) + $spam_level ); $r->spam_report($spam_report); $r->spam_summary($spam_summary); if (!$r->spam_tests) { $r->spam_tests([ \$sa_tests ]); } else { # comes last: here we use push, unlike elsewhere where may do unshift push(@{$r->spam_tests}, \$sa_tests); } if ($dkim_adsp_suppress) { $r->dsn_suppress_reason('DKIM_ADSP_DISCARD' . !defined $_ ? '' : ", $_") for $r->dsn_suppress_reason; } } } } if (defined($msginfo->spam_report) || defined($msginfo->spam_summary)) { $spam_report = $msginfo->spam_report . ', ' . $spam_report if $msginfo->spam_report ne ''; $spam_summary = $msginfo->spam_summary . "\n\n" . $spam_summary if $msginfo->spam_summary ne ''; } $msginfo->spam_report($spam_report); $msginfo->spam_summary($spam_summary); for (keys %$supplementary_info_ref) { $msginfo->supplementary_info($_, $supplementary_info_ref->{$_}); } if (defined $eval_stat) { # SA timed out? kill_proc($pid,'a spawned SA',1,$proc_fh,$eval_stat) if defined $pid; undef $proc_fh; undef $pid; chomp $eval_stat; do_log(-2, "SA failed: %s", $eval_stat); # die "$eval_stat\n" if $eval_stat !~ /timed out\b/; } 1; } 1;