package Amavis::Boot;
use strict;
use re 'taint';
sub fetch_modules($$@) {
my($reason, $required, @modules) = @_;
my(@missing);
for my $m (@modules) {
local($_) = $m;
$_ .= /^auto::/ ? '.al' : '.pm';
s[::][/]g;
eval { require $_ } or push(@missing, $m);
}
die "ERROR: MISSING $reason:\n" . join('', map { " $_\n" } @missing)
if $required && @missing;
}
BEGIN {
fetch_modules('REQUIRED BASIC MODULES', 1, qw(
Exporter POSIX Fcntl Socket Errno Carp Time::HiRes
IO::Handle IO::File IO::Socket IO::Socket::UNIX IO::Socket::INET
IO::Wrap IO::Stringy Digest::MD5 Unix::Syslog File::Basename File::Copy
Mail::Field Mail::Address Mail::Header Mail::Internet
MIME::Base64 MIME::QuotedPrint MIME::Words
MIME::Head MIME::Body MIME::Entity MIME::Parser
Net::Cmd Net::SMTP Net::Server Net::Server::PreForkSimple
MIME::Decoder::Base64 MIME::Decoder::Binary MIME::Decoder::Gzip64
MIME::Decoder::NBit MIME::Decoder::QuotedPrint MIME::Decoder::UU
));
fetch_modules('OPTIONAL BASIC MODULES', 0, qw(
Carp::Heavy auto::POSIX::setgid auto::POSIX::setuid
));
}
1;
package Amavis::Conf;
use strict;
use re 'taint';
sub D_REJECT();
sub D_BOUNCE();
sub D_DISCARD();
sub D_PASS();
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
@EXPORT = ();
@EXPORT_OK = ();
%EXPORT_TAGS = (
'dynamic_confvars' => [qw(
$policy_bank_name $protocol @inet_acl
$log_level $log_templ $log_recip_templ $forward_method $notify_method
$amavis_auth_user $amavis_auth_pass $auth_reauthenticate_forwarded
$auth_required_out $auth_required_inp @auth_mech_avail
$local_client_bind_address
$localhost_name $smtpd_greeting_banner $smtpd_quit_banner
$smtpd_message_size_limit
$final_virus_destiny $final_spam_destiny
$final_banned_destiny $final_bad_header_destiny
$warnvirussender $warnspamsender $warnbannedsender $warnbadhsender
$warn_offsite
@av_scanners @av_scanners_backup $first_infected_stops_scan
$bypass_decode_parts
$defang_virus $defang_banned $defang_spam
$defang_bad_header $defang_undecipherable $defang_all
$undecipherable_subject_tag
$sa_spam_report_header $sa_spam_level_char
$sa_mail_body_size_limit
$localpart_is_case_sensitive
$recipient_delimiter $replace_existing_extension
$hdr_encoding $bdy_encoding $hdr_encoding_qb
$notify_xmailer_header $X_HEADER_TAG $X_HEADER_LINE
$remove_existing_x_scanned_headers $remove_existing_spam_headers
$hdrfrom_notify_sender $hdrfrom_notify_recip
$hdrfrom_notify_admin $hdrfrom_notify_spamadmin
$mailfrom_notify_sender $mailfrom_notify_recip
$mailfrom_notify_admin $mailfrom_notify_spamadmin
$mailfrom_to_quarantine
$virus_quarantine_method $spam_quarantine_method
$banned_files_quarantine_method $bad_header_quarantine_method
%local_delivery_aliases
$notify_sender_templ
$notify_virus_sender_templ $notify_spam_sender_templ
$notify_virus_admin_templ $notify_spam_admin_templ
$notify_virus_recips_templ $notify_spam_recips_templ
$banned_namepath_re
$per_recip_whitelist_sender_lookup_tables
$per_recip_blacklist_sender_lookup_tables
@local_domains_maps @mynetworks_maps
@bypass_virus_checks_maps @bypass_spam_checks_maps
@bypass_banned_checks_maps @bypass_header_checks_maps
@virus_lovers_maps @spam_lovers_maps
@banned_files_lovers_maps @bad_header_lovers_maps
@warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
@virus_admin_maps @spam_admin_maps @virus_quarantine_to_maps
@banned_quarantine_to_maps @bad_header_quarantine_to_maps
@spam_quarantine_to_maps @spam_quarantine_bysender_to_maps
@banned_filename_maps
@spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps
@spam_dsn_cutoff_level_maps @spam_modifies_subj_maps
@spam_subject_tag_maps @spam_subject_tag2_maps
@whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
@message_size_limit_maps
@addr_extension_virus_maps @addr_extension_spam_maps
@addr_extension_banned_maps @addr_extension_bad_header_maps
@debug_sender_maps
)],
'confvars' => [qw(
$myproduct_name $myversion_id $myversion_id_numeric $myversion_date
$myversion $myhostname
$MYHOME $TEMPBASE $QUARANTINEDIR
$daemonize $pid_file $lock_file $db_home
$enable_db $enable_global_cache
$daemon_user $daemon_group $daemon_chroot_dir $path
$DEBUG $DO_SYSLOG $SYSLOG_LEVEL $LOGFILE
$max_servers $max_requests $child_timeout
%current_policy_bank %policy_bank %interface_policy
$unix_socketname $inet_socket_port $inet_socket_bind
$insert_received_line $relayhost_is_client $smtpd_recipient_limit
$MAXLEVELS $MAXFILES
$MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR
$MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR
@lookup_sql_dsn
$sql_select_policy $sql_select_white_black_list
$virus_check_negative_ttl $virus_check_positive_ttl
$spam_check_negative_ttl $spam_check_positive_ttl
$enable_ldap $default_ldap $virus_lovers_ldap $spam_lovers_ldap
$banned_files_lovers_ldap $bad_header_lovers_ldap
$bypass_virus_checks_ldap $bypass_spam_checks_ldap
$bypass_banned_checks_ldap $bypass_header_checks_ldap
$spam_tag_level_ldap $spam_tag2_level_ldap $spam_kill_level_ldap
$spam_modifies_subj_ldap $local_domains_ldap
$spam_quarantine_to_ldap $virus_quarantine_to_ldap
$banned_quarantine_to_ldap $bad_header_quarantine_to_ldap
$spam_whitelist_sender_ldap $spam_blacklist_sender_ldap
@keep_decoded_original_maps @map_full_type_to_short_type_maps
@viruses_that_fake_sender_maps
)],
'unpack' => [qw(
$file $arc $gzip $bzip2 $lzop $lha $unarj $uncompress $unfreeze
$unrar $zoo $cpio $ar $rpm2cpio $cabextract
)],
'sa' => [qw(
$helpers_home $dspam
$sa_local_tests_only $sa_auto_whitelist $sa_timeout $sa_debug
)],
'platform' => [qw(
$can_truncate $unicode_aware $eol
&D_REJECT &D_BOUNCE &D_DISCARD &D_PASS
)],
'hidden_confvars' => [qw(
$mydomain
)],
'legacy_confvars' => [qw(
%local_domains @local_domains_acl $local_domains_re @mynetworks
%bypass_virus_checks @bypass_virus_checks_acl $bypass_virus_checks_re
%bypass_spam_checks @bypass_spam_checks_acl $bypass_spam_checks_re
%bypass_banned_checks @bypass_banned_checks_acl $bypass_banned_checks_re
%bypass_header_checks @bypass_header_checks_acl $bypass_header_checks_re
%virus_lovers @virus_lovers_acl $virus_lovers_re
%spam_lovers @spam_lovers_acl $spam_lovers_re
%banned_files_lovers @banned_files_lovers_acl $banned_files_lovers_re
%bad_header_lovers @bad_header_lovers_acl $bad_header_lovers_re
%virus_admin %spam_admin $virus_admin $spam_admin
$warnvirusrecip $warnbannedrecip $warnbadhrecip
$virus_quarantine_to $banned_quarantine_to $bad_header_quarantine_to
$spam_quarantine_to $spam_quarantine_bysender_to
$keep_decoded_original_re $map_full_type_to_short_type_re
$banned_filename_re $viruses_that_fake_sender_re
$sa_tag_level_deflt $sa_tag2_level_deflt $sa_kill_level_deflt
$sa_dsn_cutoff_level $sa_spam_modifies_subj
$sa_spam_subject_tag1 $sa_spam_subject_tag
%whitelist_sender @whitelist_sender_acl $whitelist_sender_re
%blacklist_sender @blacklist_sender_acl $blacklist_sender_re
$addr_extension_virus $addr_extension_spam
$addr_extension_banned $addr_extension_bad_header
$gets_addr_in_quoted_form @debug_sender_acl
)],
);
Exporter::export_tags qw(dynamic_confvars confvars unpack sa platform
hidden_confvars legacy_confvars);
}
use POSIX qw(uname);
use Carp ();
use Errno qw(ENOENT);
use vars @EXPORT;
sub c($); sub cr($); sub ca($); use subs qw(c cr ca); BEGIN { push(@EXPORT,qw(c cr ca)) }
{ for my $tag (@EXPORT_TAGS{'dynamic_confvars'}) {
for my $v (@$tag) {
if ($v !~ /^([%\$\@])(.*)\z/) { die "Unsupported variable type: $v" }
else {
no strict 'refs'; my($type,$name) = ($1,$2);
$current_policy_bank{$name} = $type eq '$' ? \${"Amavis::Conf::$name"}
: $type eq '@' ? \@{"Amavis::Conf::$name"}
: $type eq '%' ? \%{"Amavis::Conf::$name"}
: undef;
}
}
}
$current_policy_bank{'policy_bank_name'} = ''; $policy_bank{''} = { %current_policy_bank }; }
sub c($) {
my($name) = @_;
if (!exists $current_policy_bank{$name}) {
Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
$name, $current_policy_bank{'policy_bank_name'}));
}
my($var) = $current_policy_bank{$name}; my($r) = ref($var);
!$r ? $var : $r eq 'SCALAR' ? $$var
: $r eq 'ARRAY' ? @$var : $r eq 'HASH' ? %$var : $var;
}
sub cr($) {
my($name) = @_;
if (!exists $current_policy_bank{$name}) {
Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
$name, $current_policy_bank{'policy_bank_name'}));
}
my($var) = $current_policy_bank{$name};
!defined($var) ? undef : !ref($var) ? \$var : $var;
}
sub ca($) {
my($name) = @_;
if (!exists $current_policy_bank{$name}) {
Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
$name, $current_policy_bank{'policy_bank_name'}));
}
my($var) = $current_policy_bank{$name};
!defined($var) ? [] : !ref($var) ? [$var] : $var;
}
$myproduct_name = 'amavisd-new';
$myversion_id = '2.2.0'; $myversion_date = '20041102';
$myversion = "$myproduct_name-$myversion_id ($myversion_date)";
$myversion_id_numeric = sprintf("%8.6f", $1 + ($2 + $3/1000)/1000)
if $myversion_id =~ /^(\d+)(?:\.(\d*)(?:\.(\d*))?)?(.*)$/;
$eol = "\n"; $unicode_aware = $]>=5.008 && length("\x{263a}")==1 && eval { require Encode };
$MYHOME = '/var/amavis';
$mydomain = '!change-mydomain-variable!.example.com';
$DEBUG = 0;
$daemonize = 0;
$max_servers = 2; $max_requests = 10;
$child_timeout = 8*60;
$can_truncate = 1;
$virus_check_negative_ttl= 3*60; $virus_check_positive_ttl= 30*60; $spam_check_negative_ttl = 30*60; $spam_check_positive_ttl = 30*60;
$SYSLOG_LEVEL = 'mail.debug';
$enable_db = 0; $enable_global_cache = 0;
$sql_select_policy =
'SELECT *,users.id FROM users,policy'
. ' WHERE (users.policy_id=policy.id) AND (users.email IN (%k))'
. ' ORDER BY users.priority DESC';
$sql_select_white_black_list =
'SELECT wb FROM wblist,mailaddr'
. ' WHERE (wblist.rid=?) AND (wblist.sid=mailaddr.id)'
. ' AND (mailaddr.email IN (%k))'
. ' ORDER BY mailaddr.priority DESC';
$inet_socket_bind = '127.0.0.1';
@inet_acl = qw( 127.0.0.1 ::1 ); @mynetworks = qw( 127.0.0.0/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 );
$notify_method = 'smtp:[127.0.0.1]:10025';
$forward_method = 'smtp:[127.0.0.1]:10025';
$virus_quarantine_method = 'local:virus-%i-%n';
$spam_quarantine_method = 'local:spam-%b-%i-%n';
$banned_files_quarantine_method = 'local:banned-%i-%n';
$bad_header_quarantine_method = 'local:badh-%i-%n';
$insert_received_line = 1; $remove_existing_x_scanned_headers = 0;
$remove_existing_spam_headers = 1;
$hdr_encoding = 'iso-8859-1'; $bdy_encoding = 'iso-8859-1';
$hdr_encoding_qb = 'Q';
$smtpd_recipient_limit = 1100;
$myhostname = (uname)[1];
$smtpd_greeting_banner = '${helo-name} ${protocol} ${product} service ready';
$smtpd_quit_banner = '${helo-name} ${product} closing transmission channel';
$localhost_name = 'localhost';
$virus_quarantine_to = 'virus-quarantine'; $banned_quarantine_to = 'banned-quarantine'; $bad_header_quarantine_to = 'bad-header-quarantine'; $spam_quarantine_to = 'spam-quarantine';
$spam_quarantine_bysender_to = undef;
$QUARANTINEDIR = undef;
$undecipherable_subject_tag = '***UNCHECKED*** ';
$sa_spam_modifies_subj = 1; $sa_spam_level_char = '*'; $sa_local_tests_only = 0;
$sa_debug = 0;
$sa_timeout = 30;
$MIN_EXPANSION_FACTOR = 5; $MAX_EXPANSION_FACTOR = 500;
sub D_REJECT () { -3 }
sub D_BOUNCE () { -2 }
sub D_DISCARD() { 0 }
sub D_PASS () { 1 }
$final_virus_destiny = D_DISCARD; $final_banned_destiny = D_BOUNCE; $final_spam_destiny = D_BOUNCE; $final_bad_header_destiny = D_PASS;
$replace_existing_extension = 1;
$localpart_is_case_sensitive = 0;
$map_full_type_to_short_type_re = Amavis::Lookup::RE->new(
[qr/^empty\z/ => 'empty'],
[qr/^directory\z/ => 'dir'],
[qr/^can't (stat|read)\b/ => 'dat'], # file(1) diagnostics
[qr/^cannot open\b/ => 'dat'], # file(1) diagnostics
[qr/^ERROR: Corrupted\b/ => 'dat'], # file(1) diagnostics
[qr/can't read magic file|couldn't find any magic files/ => 'dat'],
[qr/^data\z/ => 'dat'],
[qr/^ISO-8859.*\btext\b/ => 'txt'],
[qr/^Non-ISO.*ASCII\b.*\btext\b/ => 'txt'],
[qr/^Unicode\b.*\btext\b/i => 'txt'],
[qr/^'diff' output text\b/ => 'txt'],
[qr/^GNU message catalog\b/ => 'mo'],
[qr/^PGP encrypted data\b/ => 'pgp'],
[qr/^PGP armored data( signed)? message\b/ => ['pgp','pgp.asc'] ],
[qr/^PGP armored\b/ => ['pgp','pgp.asc'] ],
### 'file' is a bit too trigger happy to claim something is 'mail text'
# [qr/^RFC 822 mail text\b/ => 'mail'],
[qr/^(ASCII|smtp|RFC 822) mail text\b/ => 'txt'],
[qr/^JPEG image data\b/ => 'jpg'],
[qr/^GIF image data\b/ => 'gif'],
[qr/^PNG image data\b/ => 'png'],
[qr/^TIFF image data\b/ => 'tif'],
[qr/^PCX\b.*\bimage data\b/ => 'pcx'],
[qr/^PC bitmap data\b/ => 'bmp'],
[qr/^MP3\b/ => 'mp3'],
[qr/^MPEG\b.*\bstream data\b/ => 'mpeg'],
[qr/^RIFF\b.*\bAVI\b/ => 'avi'],
[qr/^RIFF\b.*\bWAVE audio\b/ => 'wav'],
[qr/^Macromedia Flash data\b/ => 'swf'],
[qr/^HTML document text\b/ => 'html'],
[qr/^XML document text\b/ => 'xml'],
[qr/^exported SGML document text\b/ => 'sgml'],
[qr/^PostScript document text\b/ => 'ps'],
[qr/^PDF document\b/ => 'pdf'],
[qr/^Rich Text Format data\b/ => 'rtf'],
[qr/^Microsoft Office Document\b/ => 'doc'],
[qr/^LaTeX\b.*\bdocument text\b/ => 'lat'],
[qr/^TeX DVI file\b/ => 'dvi'],
[qr/\bdocument text\b/ => 'txt'],
[qr/^compiled Java class data\b/ => 'java'],
[qr/^MS Windows 95 Internet shortcut text\b/ => 'url'],
[qr/^frozen\b/ => 'F'],
[qr/^gzip compressed\b/ => 'gz'],
[qr/^bzip compressed\b/ => 'bz'],
[qr/^bzip2 compressed\b/ => 'bz2'],
[qr/^lzop compressed\b/ => 'lzo'],
[qr/^compress'd/ => 'Z'],
[qr/^Zip archive\b/i => 'zip'],
[qr/^RAR archive\b/i => 'rar'],
[qr/^LHa.*\barchive\b/i => 'lha'], [qr/^ARC archive\b/i => 'arc'],
[qr/^ARJ archive\b/i => 'arj'],
[qr/^Zoo archive\b/i => 'zoo'],
[qr/^(?:GNU |POSIX )?tar archive\b/i=> 'tar'],
[qr/^Debian binary package\b/i => 'deb'], [qr/^current ar archive\b/i => 'a'], [qr/^(?:ASCII )?cpio archive\b/i => 'cpio'],
[qr/^RPM\b/ => 'rpm'],
[qr/^(Transport Neutral Encapsulation Format|TNEF)\b/i => 'tnef'],
[qr/^Microsoft cabinet file\b/ => 'cab'],
[qr/^(uuencoded|xxencoded)\b/i => 'uue'],
[qr/^binhex\b/i => 'hqx'],
[qr/^(ASCII|text)\b/i => 'asc'],
[qr/^Emacs.*byte-compiled Lisp data/i => 'asc'], [qr/\bscript text executable\b/ => 'txt'],
[qr/^MS-DOS\b.*\bexecutable\b/ => ['exe','exe-ms'] ],
[qr/^MS Windows\b.*\bexecutable\b/ => ['exe','exe-ms'] ],
[qr/^PA-RISC.*\bexecutable\b/ => ['exe','exe-unix'] ],
[qr/^ELF .*\bexecutable\b/ => ['exe','exe-unix'] ],
[qr/^COFF format .*\bexecutable\b/ => ['exe','exe-unix'] ],
[qr/^executable \(RISC System\b/ => ['exe','exe-unix'] ],
[qr/^VMS\b.*\bexecutable\b/ => ['exe','exe-vms'] ],
[qr/\bexecutable\b/i => 'exe'],
[qr/^MS Windows\b.*\bDLL\b/ => 'dll'],
[qr/\bshared object, \b/i => 'so'],
[qr/\brelocatable, \b/i => 'o'],
[qr/\btext\b/i => 'asc'],
[qr/.*/ => 'dat'],
);
*read_text = \&Amavis::Util::read_text;
*read_l10n_templates = \&Amavis::Util::read_l10n_templates;
*read_hash = \&Amavis::Util::read_hash;
*ask_daemon = \&Amavis::AV::ask_daemon;
*sophos_savi = \&Amavis::AV::ask_sophos_savi;
*ask_clamav = \&Amavis::AV::ask_clamav;
sub new_RE { Amavis::Lookup::RE->new(@_) }
sub build_default_maps() {
@local_domains_maps = (
\%local_domains, \@local_domains_acl, \$local_domains_re);
@mynetworks_maps = (\@mynetworks);
@bypass_virus_checks_maps = (
\%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);
@bypass_spam_checks_maps = (
\%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);
@bypass_banned_checks_maps = (
\%bypass_banned_checks, \@bypass_banned_checks_acl, \$bypass_banned_checks_re);
@bypass_header_checks_maps = (
\%bypass_header_checks, \@bypass_header_checks_acl, \$bypass_header_checks_re);
@virus_lovers_maps = (
\%virus_lovers, \@virus_lovers_acl, \$virus_lovers_re);
@spam_lovers_maps = (
\%spam_lovers, \@spam_lovers_acl, \$spam_lovers_re);
@banned_files_lovers_maps = (
\%banned_files_lovers, \@banned_files_lovers_acl, \$banned_files_lovers_re);
@bad_header_lovers_maps = (
\%bad_header_lovers, \@bad_header_lovers_acl, \$bad_header_lovers_re);
@warnvirusrecip_maps = (\$warnvirusrecip);
@warnbannedrecip_maps = (\$warnbannedrecip);
@warnbadhrecip_maps = (\$warnbadhrecip);
@virus_admin_maps = (\%virus_admin, \$virus_admin);
@spam_admin_maps = (\%spam_admin, \$spam_admin);
@virus_quarantine_to_maps = (\$virus_quarantine_to);
@banned_quarantine_to_maps = (\$banned_quarantine_to);
@bad_header_quarantine_to_maps = (\$bad_header_quarantine_to);
@spam_quarantine_to_maps = (\$spam_quarantine_to);
@spam_quarantine_bysender_to_maps = (\$spam_quarantine_bysender_to);
@keep_decoded_original_maps = (\$keep_decoded_original_re);
@map_full_type_to_short_type_maps = (\$map_full_type_to_short_type_re);
@banned_filename_maps = (\$banned_filename_re);
@viruses_that_fake_sender_maps = (\$viruses_that_fake_sender_re, 1);
@spam_tag_level_maps = (\$sa_tag_level_deflt);
@spam_tag2_level_maps = (\$sa_tag2_level_deflt);
@spam_kill_level_maps = (\$sa_kill_level_deflt);
@spam_dsn_cutoff_level_maps = (\$sa_dsn_cutoff_level);
@spam_modifies_subj_maps = (\$sa_spam_modifies_subj);
@spam_subject_tag_maps = (\$sa_spam_subject_tag1); @spam_subject_tag2_maps = (\$sa_spam_subject_tag); @whitelist_sender_maps = (
\%whitelist_sender, \@whitelist_sender_acl, \$whitelist_sender_re);
@blacklist_sender_maps = (
\%blacklist_sender, \@blacklist_sender_acl, \$blacklist_sender_re);
@score_sender_maps = (); @message_size_limit_maps = (); @addr_extension_virus_maps = (\$addr_extension_virus);
@addr_extension_spam_maps = (\$addr_extension_spam);
@addr_extension_banned_maps = (\$addr_extension_banned);
@addr_extension_bad_header_maps = (\$addr_extension_bad_header);
@debug_sender_maps = (\@debug_sender_acl);
}
sub label_default_maps() {
for my $varname (qw(
@local_domains_maps @mynetworks_maps
@bypass_virus_checks_maps @bypass_spam_checks_maps
@bypass_banned_checks_maps @bypass_header_checks_maps
@virus_lovers_maps @spam_lovers_maps
@banned_files_lovers_maps @bad_header_lovers_maps
@warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
@virus_admin_maps @spam_admin_maps @virus_quarantine_to_maps
@banned_quarantine_to_maps @bad_header_quarantine_to_maps
@spam_quarantine_to_maps @spam_quarantine_bysender_to_maps
@keep_decoded_original_maps @map_full_type_to_short_type_maps
@banned_filename_maps @viruses_that_fake_sender_maps
@spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps
@spam_dsn_cutoff_level_maps @spam_modifies_subj_maps
@spam_subject_tag_maps @spam_subject_tag2_maps
@whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
@message_size_limit_maps
@addr_extension_virus_maps @addr_extension_spam_maps
@addr_extension_banned_maps @addr_extension_bad_header_maps
@debug_sender_maps ))
{
my($g) = $varname; $g =~ s{\@}{Amavis::Conf::}; my($label) = $varname; $label=~s/^\@//; $label=~s/_maps$//;
{ no strict 'refs';
unshift(@$g, Amavis::Lookup::Label->new($label)) if @$g; }
}
}
sub read_config($) {
my($config_file) = @_;
my($msg);
my($errn) = stat($config_file) ? 0 : 0+$!;
if ($errn == ENOENT) { $msg = "does not exist" }
elsif ($errn) { $msg = "inaccessible: $!" }
elsif (!-f _) { $msg = "not a regular file" }
elsif (!-r _) { $msg = "not readable" }
elsif ($> && -o _) { $msg = "is owned by EUID $>, should be owned by root" }
elsif ($> && -w _) { $msg = "is writable by EUID $>, EGID $)" }
if (defined $msg) { die "Config file $config_file $msg" }
do $config_file;
if ($@ ne '') { die "Error in config file $config_file: $@" }
$TEMPBASE = $MYHOME if !defined $TEMPBASE;
$helpers_home = $MYHOME if !defined $helpers_home;
$db_home = "$MYHOME/db" if !defined $db_home;
$lock_file = "$MYHOME/amavisd.lock" if !defined $lock_file;
$pid_file = "$MYHOME/amavisd.pid" if !defined $pid_file;
$X_HEADER_TAG = 'X-Virus-Scanned' if !defined $X_HEADER_TAG;
$X_HEADER_LINE= "$myproduct_name at $mydomain" if !defined $X_HEADER_LINE;
my($pname) = "\"Content-filter at $myhostname\"";
$hdrfrom_notify_sender = "$pname <postmaster\@$myhostname>"
if !defined $hdrfrom_notify_sender;
$hdrfrom_notify_recip = $mailfrom_notify_recip ne ''
? "$pname <$mailfrom_notify_recip>"
: $hdrfrom_notify_sender if !defined $hdrfrom_notify_recip;
$hdrfrom_notify_admin = $mailfrom_notify_admin ne ''
? "$pname <$mailfrom_notify_admin>"
: $hdrfrom_notify_sender if !defined $hdrfrom_notify_admin;
$hdrfrom_notify_spamadmin = $mailfrom_notify_spamadmin ne ''
? "$pname <$mailfrom_notify_spamadmin>"
: $hdrfrom_notify_sender if !defined $hdrfrom_notify_spamadmin;
for ($final_virus_destiny, $final_banned_destiny, $final_spam_destiny) {
if ($_ > 0) { $_ = D_PASS }
elsif ($_ < 0 && $_ != D_BOUNCE && $_ != D_REJECT) { $_ = c('forward_method') eq '' ? D_REJECT : D_BOUNCE;
}
}
if ($final_virus_destiny == D_DISCARD && c('warnvirussender') )
{ $final_virus_destiny = D_BOUNCE }
if ($final_spam_destiny == D_DISCARD && c('warnspamsender') )
{ $final_spam_destiny = D_BOUNCE }
if ($final_banned_destiny == D_DISCARD && c('warnbannedsender') )
{ $final_banned_destiny = D_BOUNCE }
if ($final_bad_header_destiny == D_DISCARD && c('warnbadhsender') )
{ $final_bad_header_destiny = D_BOUNCE }
}
1;
package Amavis::Lock;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
@EXPORT = qw(&lock &unlock);
}
use Fcntl qw(LOCK_SH LOCK_EX LOCK_UN);
use subs @EXPORT;
sub lock($) {
my($file_handle) = @_;
flock($file_handle, LOCK_EX) or die "Can't lock $file_handle: $!";
}
sub unlock($) {
my($file_handle) = @_;
flock($file_handle, LOCK_UN) or die "Can't unlock $file_handle: $!";
}
1;
package Amavis::Log;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = ();
@EXPORT_OK = qw(&init &write_log);
}
use subs @EXPORT_OK;
use POSIX qw(locale_h strftime);
use Unix::Syslog qw(:macros :subs);
use IO::File ();
use File::Basename;
BEGIN {
import Amavis::Conf qw(:platform $myversion $myhostname $daemon_user);
import Amavis::Lock;
}
use vars qw($loghandle); use vars qw($myname);
use vars qw($syslog_facility $syslog_priority %syslog_priority);
use vars qw($log_to_stderr $do_syslog $logfile);
sub init($$$$$) {
my($ident, $syslog_level);
($ident, $log_to_stderr, $do_syslog, $syslog_level, $logfile) = @_;
$myname = $0;
if ($syslog_level =~ /^\s*([a-z0-9]+)\.([a-z0-9]+)\s*\z/i) {
$syslog_facility = eval("LOG_\U$1");
$syslog_priority = eval("LOG_\U$2");
}
$syslog_facility = LOG_DAEMON if $syslog_facility !~ /^\d+\z/;
$syslog_priority = LOG_WARNING if $syslog_priority !~ /^\d+\z/;
if ($do_syslog) {
openlog($ident, LOG_PID, $syslog_facility);
} elsif ($logfile eq '') {
die 'No $LOGFILE is specified (and not logging via syslog)';
} else {
$loghandle = IO::File->new($logfile,'>>')
or die "Failed to open log file $logfile: $!";
$loghandle->autoflush(1);
my($uid) = $daemon_user=~/^(\d+)$/ ? $1 : (getpwnam($daemon_user))[2];
if ($> == 0 && $uid) {
chown($uid,-1,$logfile)
or die "Can't chown logfile $logfile to $uid: $!";
}
}
my($msg) = "starting. $myname at $myhostname $myversion";
$msg .= ", eol=\"$eol\"" if $eol ne "\n";
$msg .= ", Unicode aware" if $unicode_aware;
$msg .= ", LC_ALL=$ENV{LC_ALL}" if $ENV{LC_ALL} ne '';
$msg .= ", LC_TYPE=$ENV{LC_TYPE}" if $ENV{LC_TYPE} ne '';
$msg .= ", LC_CTYPE=$ENV{LC_CTYPE}" if $ENV{LC_CTYPE} ne '';
$msg .= ", LANG=$ENV{LANG}" if $ENV{LANG} ne '';
write_log(0, $msg, undef);
}
sub write_log($$$) {
my($level,$errmsg,$am_id) = @_;
my($old_locale) = setlocale(LC_TIME,"C"); my($really_log_to_stderr) = $log_to_stderr || (!$do_syslog && !$loghandle);
my($prefix) = '';
if ($really_log_to_stderr || !$do_syslog) { $prefix = sprintf("%s %s %s[%s]: ",
strftime("%b %e %H:%M:%S", localtime), $myhostname, $myname, $$);
}
$am_id = "($am_id) " if defined $am_id;
$errmsg = Amavis::Util::sanitize_str($errmsg);
if ($really_log_to_stderr) {
print STDERR $prefix, $am_id, $errmsg, $eol;
} elsif ($do_syslog) {
my($prio) = $syslog_priority; if ($level <= -3) { $prio = LOG_CRIT if $prio > LOG_CRIT }
elsif ($level <= -2) { $prio = LOG_ERR if $prio > LOG_ERR }
elsif ($level <= -1) { $prio = LOG_WARNING if $prio > LOG_WARNING }
elsif ($level <= 0) { $prio = LOG_NOTICE if $prio > LOG_NOTICE }
elsif ($level <= 2) { $prio = LOG_INFO if $prio > LOG_INFO }
else { $prio = LOG_DEBUG if $prio > LOG_DEBUG }
my($pre) = '';
my($logline_size) = 980; while (length($am_id . $pre . $errmsg) > $logline_size) {
my($avail) = $logline_size - length($am_id . $pre . "...");
syslog($prio, "%s", $am_id . $pre . substr($errmsg,0,$avail) . "...");
$pre = "...";
$errmsg = substr($errmsg, $avail);
}
syslog($prio, "%s", $am_id . $pre . $errmsg);
} else {
lock($loghandle);
seek($loghandle,0,2) or die "Can't position log file to its tail: $!";
print $loghandle $prefix, $am_id, $errmsg, $eol;
unlock($loghandle);
}
setlocale(LC_TIME, $old_locale);
}
1;
package Amavis::Timing;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = ();
@EXPORT_OK = qw(&init §ion_time &report &get_time_so_far);
}
use subs @EXPORT_OK;
use Time::HiRes ();
use vars qw(@timing);
sub init() {
@timing = (); section_time('init');
}
sub section_time($) {
push(@timing,shift,Time::HiRes::time);
}
sub report() {
section_time('rundown');
my($notneeded, $t0) = (shift(@timing), shift(@timing));
my($total) = $t0 <= 0 ? 0 : $timing[$ if ($total < 0.0000001) { $total = 0.0000001 }
my(@sections);
while (@timing) {
my($section, $t) = (shift(@timing), shift(@timing));
my($dt) = $t-$t0;
$dt = 0 if $dt < 0; my($dtp) = $dt > $total ? 100 : $dt*100.0/$total;
push(@sections, sprintf("%s: %.0f (%.0f%%)", $section, $dt*1000, $dtp));
$t0 = $t;
}
sprintf("TIMING [total %.0f ms] - %s", $total * 1000, join(", ",@sections));
}
sub get_time_so_far() {
my($notneeded, $t0) = @timing;
my($total) = $t0 <= 0 ? 0 : Time::HiRes::time - $t0;
$total < 0 ? 0 : $total;
}
use vars qw($t_was_busy $t_busy_cum $t_idle_cum $t0);
sub idle_proc(@) {
my($t1) = Time::HiRes::time;
if (defined $t0) {
($t_was_busy ? $t_busy_cum : $t_idle_cum) += $t1 - $t0;
Amavis::Util::ll(5) && Amavis::Util::do_log(5,
sprintf("idle_proc, @_: was %s, %.1f ms, total idle %.3f s, busy %.3f s",
$t_was_busy ? "busy" : "idle", 1000 * ($t1 - $t0),
$t_idle_cum, $t_busy_cum));
}
$t0 = $t1;
}
sub go_idle(@) {
if ($t_was_busy) { idle_proc(@_); $t_was_busy = 0 }
}
sub go_busy(@) {
if (!$t_was_busy) { idle_proc(@_); $t_was_busy = 1 }
}
sub report_load() {
return if $t_busy_cum + $t_idle_cum <= 0;
Amavis::Util::do_log(3, sprintf(
"load: %.0f %%, total idle %.3f s, busy %.3f s",
100*$t_busy_cum / ($t_busy_cum + $t_idle_cum), $t_idle_cum, $t_busy_cum));
}
1;
package Amavis::Util;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = ();
@EXPORT_OK = qw(&untaint &min &max &safe_encode &q_encode
&snmp_count &snmp_counters_init &snmp_counters_get
&am_id &new_am_id &ll &do_log &debug_oneshot
&retcode &exit_status_str &prolong_timer &sanitize_str
&strip_tempdir &rmdir_recursively
&read_text &read_l10n_templates &read_hash
&run_command &run_command_consumer);
}
use subs @EXPORT_OK;
use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
WEXITSTATUS WTERMSIG WSTOPSIG);
use Errno qw(ENOENT);
use Digest::MD5;
BEGIN {
import Amavis::Conf qw(:platform $DEBUG c cr ca);
import Amavis::Log qw(write_log);
import Amavis::Timing qw(section_time);
}
sub untaint($) {
no re 'taint';
my($str);
local($1); $str = $1 if (ref($_[0]) ? ${$_[0]} : $_[0]) =~ /^(.*)\z/s;
$str;
}
sub min(@) {
my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ < $m) }
$m;
}
sub max(@) {
my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ > $m) }
$m;
}
sub safe_encode($$;$) {
if (!$unicode_aware) { $_[1] } else {
my($encoding,$str,$check) = @_;
$check = 0 if !defined($check);
my($taint) = substr($str,0,0); $taint . Encode::encode($encoding,untaint($str),$check); }
}
sub q_encode($$$) {
my($octets,$encoding,$charset) = @_;
my($prefix) = '=?' . $charset . '?' . $encoding . '?';
my($suffix) = '?='; local($1,$2,$3);
$octets =~ /^ ( [\001-\011\013\014\016-\177]* [ \t] )? (.*?)
( [ \t] [\001-\011\013\014\016-\177]* )? \z/sx;
my($head,$rest,$tail) = ($1,$2,$3);
$rest =~ s{([^ 0-9a-zA-Z!*/+-])}{sprintf('=%02X',ord($1))}egs;
$rest =~ tr/ /_/; my($s) = $head; my($len) = 75 - (length($prefix)+length($suffix)) - 2;
while ($rest ne '') {
$s .= ' ' if $s !~ /[ \t]\z/; $rest =~ /^ ( .{0,$len} [^=] (?: [^=] | \z ) ) (.*) \z/sx;
$s .= $prefix.$1.$suffix; $rest = $2;
}
$s.$tail;
}
use vars qw($amavis_task_id);
sub am_id(;$) {
if (@_) { $amavis_task_id = shift;
$0 = "amavisd ($amavis_task_id)";
}
$amavis_task_id; }
sub new_am_id($;$$) {
my($str, $cnt, $seq) = @_;
my($id);
$id = defined $str ? $str : sprintf("%05d", $$);
$id .= sprintf("-%02d", $cnt) if defined $cnt;
$id .= "-$seq" if $seq > 1;
am_id($id);
}
use vars qw(@counter_names);
sub snmp_counters_init() { @counter_names = () }
sub snmp_count(@) { push(@counter_names, @_) }
sub snmp_counters_get() { \@counter_names }
use vars qw($debug_oneshot);
sub debug_oneshot(;$$) {
if (@_) {
my($new_debug_oneshot) = shift;
if (($new_debug_oneshot ? 1 : 0) != ($debug_oneshot ? 1 : 0)) {
do_log(0, "DEBUG_ONESHOT: TURNED ".($new_debug_oneshot ? "ON" : "OFF"));
do_log(0, shift) if @_; }
$debug_oneshot = $new_debug_oneshot;
}
$debug_oneshot;
}
sub ll($) {
my($level) = @_;
$level = 0 if $level > 0 && ($DEBUG || $debug_oneshot);
$level <= c('log_level');
}
sub do_log($$) {
my($level, $errmsg) = @_;
if (ll($level)) {
$level = 0 if $level > 0 && ($DEBUG || $debug_oneshot);
write_log($level, $errmsg, am_id());
}
}
sub retcode($) { my $code = shift;
return WEXITSTATUS($code) if WIFEXITED($code);
return 128 + WTERMSIG($code) if WIFSIGNALED($code);
return 255;
}
sub exit_status_str($;$) {
my($stat,$err) = @_; my($str);
if (WIFEXITED($stat)) {
$str = sprintf("exit %d", WEXITSTATUS($stat));
} elsif (WIFSTOPPED($stat)) {
$str = sprintf("stopped, signal %d", WSTOPSIG($stat));
} else {
$str = sprintf("DIED on signal %d (%04x)", WTERMSIG($stat),$stat);
}
$str .= ', '.$err if $err ne '';
$str;
}
sub prolong_timer($;$) {
my($which_section, $child_remaining_time) = @_;
if (!defined($child_remaining_time)) {
$child_remaining_time = alarm(0); }
do_log(4, "prolong_timer after $which_section: "
. "remaining time = $child_remaining_time s");
$child_remaining_time = 60 if $child_remaining_time < 60;
alarm($child_remaining_time); }
sub sanitize_str {
my($str, $keep_eol) = @_;
my(%map) = ("\r" => '\\r', "\n" => '\\n', "\f" => '\\f', "\t" => '\\t',
"\b" => '\\b', "\e" => '\\e', "\\" => '\\\\');
if ($keep_eol) {
$str =~ s/([^\012\040-\133\135-\176])/ exists($map{$1}) ? $map{$1} :
sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg;
} else {
$str =~ s/([^\040-\133\135-\176])/ exists($map{$1}) ? $map{$1} :
sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg;
}
$str;
}
sub check_tempdir($) {
my($dir) = shift;
local(*DIR); my($f);
opendir(DIR,$dir) or die "Can't open directory $dir: $!";
while (defined($f = readdir(DIR))) {
if (!-d ("$dir/$f")) {
die "Unexpected file $dir/$f" if $f ne 'email.txt';
} elsif ($f eq '.' || $f eq '..' || $f eq 'parts') {
} else {
die "Unexpected subdirectory $dir/$f";
}
}
closedir(DIR) or die "Can't close directory $dir: $!";
1;
}
sub strip_tempdir($) {
my($dir) = shift;
do_log(4, "strip_tempdir: $dir");
my($errn) = lstat("$dir/parts") ? 0 : 0+$!;
if ($errn != ENOENT) {
if ( -l _) { die "strip_tempdir: $dir/parts is a symbolic link" }
elsif (!-d _) { die "strip_tempdir: $dir/parts is not a directory" }
rmdir_recursively("$dir/parts", 1);
}
check_tempdir($dir);
1;
}
sub rmdir_recursively($;$); sub rmdir_recursively($;$) {
my($dir, $exclude_itself) = @_;
do_log(4, "rmdir_recursively: $dir, excl=$exclude_itself");
local(*DIR); my($f); my($cnt) = 0;
opendir(DIR,$dir) or die "Can't open directory $dir: $!";
while (defined($f = readdir(DIR))) {
my($msg);
my($errn) = lstat("$dir/$f") ? 0 : 0+$!;
if ($errn == ENOENT) { $msg = "does not exist" }
elsif ($errn) { $msg = "inaccessible: $!" }
if (defined $msg) { die "rmdir_recursively: \"$dir/$f\" $msg" }
next if ($f eq '.' || $f eq '..') && -d _;
$f = untaint($f);
if (-d _) {
rmdir_recursively("$dir/$f", 0);
} else {
$cnt++;
unlink("$dir/$f") or die "Can't remove file $dir/$f: $!";
}
}
closedir(DIR) or die "Can't close directory $dir: $!";
section_time("unlink-$cnt-files");
if (!$exclude_itself) {
rmdir($dir) or die "Can't remove directory $dir: $!";
section_time('rmdir');
}
1;
}
sub read_text($;$) {
my($filename, $encoding) = @_;
my($inp) = IO::File->new;
$inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
if ($unicode_aware && $encoding ne '') {
binmode($inp, ":encoding($encoding)")
or die "Can't set :encoding($encoding) on file $filename: $!";
}
my($str) = ''; while (<$inp>) { $str .= $_ }
$inp->close or die "Can't close $filename: $!";
$str;
}
sub read_l10n_templates($;$) {
my($dir) = @_;
if (@_ > 1) { my($l10nlang, $l10nbase) = @_; $dir = "$l10nbase/$l10nlang" }
my($file_chset) = Amavis::Util::read_text("$dir/charset");
if ($file_chset =~ m{^(?: $file_chset = untaint($1);
} else {
die "Invalid charset $file_chset\n";
}
$Amavis::Conf::notify_sender_templ =
Amavis::Util::read_text("$dir/template-dsn.txt", $file_chset);
$Amavis::Conf::notify_virus_sender_templ =
Amavis::Util::read_text("$dir/template-virus-sender.txt", $file_chset);
$Amavis::Conf::notify_virus_admin_templ =
Amavis::Util::read_text("$dir/template-virus-admin.txt", $file_chset);
$Amavis::Conf::notify_virus_recips_templ =
Amavis::Util::read_text("$dir/template-virus-recipient.txt", $file_chset);
$Amavis::Conf::notify_spam_sender_templ =
Amavis::Util::read_text("$dir/template-spam-sender.txt", $file_chset);
$Amavis::Conf::notify_spam_admin_templ =
Amavis::Util::read_text("$dir/template-spam-admin.txt", $file_chset);
}
sub read_hash(@) {
unshift(@_,{}) if !ref $_[0]; my($hashref, $filename, $keep_case) = @_;
my($lpcs) = c('localpart_is_case_sensitive');
my($inp) = IO::File->new;
$inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
while (<$inp>) { chomp;
my($lhs) = ''; my($rhs) = ''; my($at_rhs) = 0;
for my $t ( /\G ( " (?: \\. | [^"\\] )* " |
[^#" \t]+ | [ \t]+ | . )/gcsx) {
last if $t eq '#';
if (!$at_rhs && $t =~ /^[ \t]+\z/) { $at_rhs = 1 }
else { ($at_rhs ? $rhs : $lhs) .= $t }
}
$rhs =~ s/[ \t]+\z//; # trim trailing whitespace
next if $lhs eq '' && $rhs eq '';
my($addr) = Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($lhs);
my($localpart,$domain) = Amavis::rfc2821_2822_Tools::split_address($addr);
$localpart = lc($localpart) if !$lpcs;
$addr = $localpart . lc($domain);
$hashref->{$addr} = $rhs eq '' ? 1 : $rhs;
}
$inp->close or die "Can't close $filename: $!";
$hashref;
}
sub run_command($$@) {
my($stdin_from, $stderr_to, $cmd, @args) = @_;
my($cmd_text) = join(' ', $cmd, @args);
$stdin_from = '/dev/null' if $stdin_from eq '';
my($msg) = join(' ', $cmd, @args, "<$stdin_from");
$msg .= " 2>$stderr_to" if $stderr_to ne '';
my($pid); my($proc_fh) = IO::File->new;
eval { $pid = $proc_fh->open('-|') }; if ($@ ne '') { chomp($@); die "run_command (open pipe): $@" }
defined($pid) or die "run_command: can't fork: $!";
if (!$pid) { eval { close(STDIN) or die "Can't close STDIN: $!";
close(main::stdin) or die "Can't close main::stdin: $!";
open(STDIN, "<$stdin_from")
or die "Can't reopen STDIN on $stdin_from: $!";
fileno(STDIN) == 0 or die ("run_command: STDIN not fd0: ".fileno(STDIN));
if ($stderr_to ne '') {
close(STDERR) or die "Can't close STDERR: $!";
open(STDERR, ">$stderr_to")
or die "Can't open STDERR to $stderr_to: $!";
fileno(STDERR) == 2
or die ("run_command: STDERR not fd2: ".fileno(STDERR));
}
{ no warnings;
exec {$cmd} ($cmd,@args) or die "Failed to exec $cmd_text: $!";
}
};
chomp($@);
do_log(-2,"run_command: child process [$$]: $@\n");
{ no warnings;
kill('TERM',$$) or do_log(-3,"run_command: TROUBLE - Panic1, can't die: $!");
exec('/usr/bin/false'); exec('/bin/false'); exec('false'); exec('true');
do_log(-3,"run_command: TROUBLE - Panic2, can't die");
exit 1; }
}
do_log(5,"run_command: [$pid] $msg");
binmode($proc_fh) or die "Can't set pipe to binmode: $!"; ($proc_fh, $pid); }
sub run_command_consumer($$@) {
my($stdout_to, $stderr_to, $cmd, @args) = @_;
my($cmd_text) = join(' ', $cmd, @args);
$stdout_to = '/dev/null' if $stdout_to eq '';
my($msg) = join(' ', $cmd, @args, ">$stdout_to");
$msg .= " 2>$stderr_to" if $stderr_to ne '';
my($pid); my($proc_fh) = IO::File->new;
eval { $pid = $proc_fh->open('|-') }; if ($@ ne '') { chomp($@); die "run_command_consumer (open pipe): $@" }
defined($pid) or die "run_command_consumer: can't fork: $!";
if (!$pid) { eval { close(main::stderr) or die "Can't close main::stderr: $!";
close(main::stdout) or die "Can't close main::stdout: $!";
close(main::STDOUT) or die "Can't close main::STDOUT: $!";
open(STDOUT, ">$stdout_to")
or die "Can't reopen STDOUT on $stdout_to: $!";
fileno(STDOUT) == 1
or die ("run_command_consumer: STDOUT not fd1: ".fileno(STDOUT));
if ($stderr_to ne '') {
close(STDERR) or die "Can't close STDERR: $!";
open(STDERR, ">$stderr_to")
or die "Can't open STDERR to $stderr_to: $!";
fileno(STDERR) == 2
or die ("run_command_consumer: STDERR not fd2: ".fileno(STDERR));
}
{ no warnings;
exec {$cmd} ($cmd,@args) or die "Failed to exec $cmd_text: $!";
}
};
chomp($@);
do_log(-2,"run_command_consumer: child process [$$]: $@\n");
{ no warnings;
kill('TERM',$$) or do_log(-3,"run_command_consumer: TROUBLE - Panic1, can't die: $!");
exec('/usr/bin/false'); exec('/bin/false'); exec('false'); exec('true');
do_log(-3,"run_command_consumer: TROUBLE - Panic2, can't die");
exit 1; }
}
do_log(5,"run_command_consumer: [$pid] $msg");
binmode($proc_fh) or die "Can't set pipe to binmode: $!"; ($proc_fh, $pid); }
1;
package Amavis::rfc2821_2822_Tools;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = qw(
&iso8601_timestamp &iso8601_utc_timestamp &rfc2822_timestamp
&received_line &parse_received
&fish_out_ip_from_received &split_address &split_localpart &make_query_keys
"e_rfc2821_local &qquote_rfc2821_local &unquote_rfc2821_local
&one_response_for_all
&EX_OK &EX_NOUSER &EX_UNAVAILABLE &EX_TEMPFAIL &EX_NOPERM);
}
use subs @EXPORT;
use POSIX qw(locale_h strftime);
BEGIN {
eval { require 'sysexits.ph' }; do { sub EX_OK() {0} } unless defined(&EX_OK);
do { sub EX_NOUSER() {67} } unless defined(&EX_NOUSER);
do { sub EX_UNAVAILABLE() {69} } unless defined(&EX_UNAVAILABLE);
do { sub EX_TEMPFAIL() {75} } unless defined(&EX_TEMPFAIL);
do { sub EX_NOPERM() {77} } unless defined(&EX_NOPERM);
}
BEGIN {
import Amavis::Conf qw(:platform $myhostname c cr ca);
import Amavis::Util qw(ll do_log);
}
sub get_zone_offset($) {
my($t) = @_;
my($d) = 0; for (1..3) { my($r) = sprintf("%04d%02d%02d", (localtime($t))[5, 4, 3]) cmp
sprintf("%04d%02d%02d", (gmtime($t + $d))[5, 4, 3]);
if ($r == 0) { last } else { $d += $r * 24 * 3600 }
}
my($sl,$su) = (0,0);
for ((localtime($t))[2,1,0]) { $sl = $sl * 60 + $_ }
for ((gmtime($t + $d))[2,1,0]) { $su = $su * 60 + $_ }
$d += $sl - $su; my($sign) = $d >= 0 ? '+' : '-';
$d = -$d if $d < 0;
$d = int(($d + 30) / 60.0); sprintf("%s%02d%02d", $sign, int($d / 60), $d % 60);
}
sub iso8601_timestamp($;$) {
my($t,$suppress_zone) = @_;
my($s) = strftime("%Y%m%dT%H%M%S", localtime($t));
$s .= get_zone_offset($t) unless $suppress_zone;
$s;
}
sub iso8601_utc_timestamp($;$) {
my($t,$suppress_zone) = @_;
my($s) = strftime("%Y%m%dT%H%M%S", gmtime($t));
$s .= 'Z' unless $suppress_zone;
$s;
}
sub rfc2822_timestamp($) {
my($t) = @_;
my(@lt) = localtime($t);
my($old_locale) = setlocale(LC_TIME,"C"); my($zone_name) = strftime("%Z",@lt);
my($s) = strftime("%a, %e %b %Y %H:%M:%S ", @lt);
$s .= get_zone_offset($t);
$s .= " (" . $zone_name . ")" if $zone_name !~ /^\s*\z/;
setlocale(LC_TIME, $old_locale); $s;
}
sub received_line($$$$) {
my($conn, $msginfo, $id, $folded) = @_;
my($smtp_proto, $recips) = ($conn->smtp_proto, $msginfo->recips);
my($client_ip) = $conn->client_ip;
if ($client_ip =~ /:/ && $client_ip !~ /^IPv6:/i) {
$client_ip = 'IPv6:' . $client_ip;
}
my($s) = sprintf("from %s%s\n by %s%s (amavisd-new, %s)",
($conn->smtp_helo eq '' ? 'unknown' : $conn->smtp_helo),
($client_ip eq '' ? '' : " ([$client_ip])"),
c('localhost_name'),
($conn->socket_ip eq '' ? ''
: sprintf(" (%s [%s])", $myhostname, $conn->socket_ip) ),
($conn->socket_port eq '' ? 'unix socket' : "port ".$conn->socket_port) );
$s .= "\n with $smtp_proto" if $smtp_proto=~/^(ES|S|L)MTPS?A?\z/i; $s .= "\n id $id" if $id ne '';
$s .= "\n for " . qquote_rfc2821_local(@$recips) if @$recips == 1;
$s .= ";\n " . rfc2822_timestamp($msginfo->rx_time);
$s =~ s/\n//g if !$folded;
$s;
}
sub parse_received($) {
my($received) = @_;
local($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11);
$received =~ s/\n([ \t])/$1/g; $received =~ s/[\n\r]//g; # delete remaining newlines if any
my(%fields);
while ($received =~ m{\G\s*
( \b(from|by) \s+ ( (?: \[ (?: \\. | [^\]\\] )* \] | [^;\s\[] )+ )
(?: \s* \( (?: ( [^\s\[]+ ) \s+ )?
\[ ( (?: \\. | [^\]\\] )* ) \] \s*
\) )?
(?: .*? ) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) | \b(via|with|id|for) \s+
( (?: " (?: \\. | [^"\\] )* "
| \[ (?: \\. | [^\]\\] )* \]
| \\. | .
)+? (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) )
| (;) \s* ( .*? ) \s* \z # time
| (.*?) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) # junk
) ( (?: \s+ | (?: \( (?: \\. | [^)\\] )* \) ) )* ) }xgcsi)
{
my($v1, $v2, $v3, $comment) = ('') x 4;
my($item, $field) = ($1, lc($2 || $6 || $8));
if ($field eq 'from' || $field eq 'by') {
($v1, $v2, $v3, $comment) = ($3, $4, $5, $11);
} elsif ($field eq ';') { # time
($v1, $comment) = ($9, $11);
} elsif ($10 eq '') { # via|with|id|for
($v1, $comment) = ($7, $11);
} else { # junk
($v1, $comment) = ($10, $11);
}
$comment =~ s/^\s+//;
$comment =~ s/\s+\z//;
$item =~ s/^\Q$field\E\s*//i;
if (!exists $fields{$field}) {
$fields{$field} = [$item, $v1, $v2, $v3, $comment];
do_log(5, "parse_received: $field = $item/$v1/$v2/$v3") if $field ne '';
}
}
\%fields;
}
sub fish_out_ip_from_received($) {
my($received) = @_;
my($ip);
my($fields_ref) = parse_received($received);
if (defined $fields_ref && exists $fields_ref->{'from'}) {
my($item, $v1, $v2, $v3, $comment) = @{$fields_ref->{'from'}};
for ($v3, $v2, $v1, $comment, $item) {
if (/ \[ (\d{1,3} (?: \. \d{1,3}){3}) \] /x) {
$ip = $1; last;
} elsif (/ (\d{1,3} (?: \. \d{1,3}){3}) (?!\d) /x) {
$ip = $1; last;
} elsif (/ \[ (IPv6:)? ( ([0-9a-zA-Z]* : ){2,} [0-9a-zA-Z:.]* ) \] /xi) {
$ip = $2; last;
}
}
do_log(5, "fish_out_ip_from_received: $ip, $item");
}
!defined($ip) ? undef : $ip; # undef need not be tainted
}
# Splits unquoted fully qualified e-mail address, or an address
# with missing domain part. Returns a pair: (localpart, domain).
# The domain part (if nonempty) includes the '@' as the first character.
# If the syntax is badly broken, everything ends up as the localpart.
# The domain part can be an address literal, as specified by rfc2822.
# Does not handle explicit route paths.
#
sub split_address($) {
my($mailbox) = @_;
$mailbox =~ /^ (.*?) ( \@ (?: \[ (?: \\. | [^\]\\] )* \]
| [^@"<>\[\]\\\s] )*
) \z/xs ? ($1, $2) : ($mailbox, '');
}
sub split_localpart($$) {
my($localpart, $delimiter) = @_;
my($owner_request_special) = 1; my($extension);
if ($localpart =~ /^(postmaster|mailer-daemon|double-bounce)\z/i) {
} elsif ($delimiter eq '-' && $owner_request_special &&
$localpart =~ /^owner-|-request\z/i) {
} elsif ($localpart =~ /^(.+?)\Q$delimiter\E(.*)\z/s) {
($localpart, $extension) = ($1, $2);
}
($localpart, $extension);
}
sub make_query_keys($$$) {
my($addr,$at_with_user,$include_bare_user) = @_;
my($localpart,$domain) = split_address($addr); $domain = lc($domain);
my($saved_full_localpart) = $localpart;
$localpart = lc($localpart) if !c('localpart_is_case_sensitive');
$domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s;
my($extension); my($delim) = c('recipient_delimiter');
if ($delim ne '') {
($localpart,$extension) = split_localpart($localpart,$delim);
}
my($append_to_user,$prepend_to_domain) = $at_with_user ? ('@','') : ('','@');
my(@keys); push(@keys, $addr); push(@keys, $localpart.$delim.$extension.'@'.$domain)
if $extension ne ''; push(@keys, $localpart.'@'.$domain); if ($include_bare_user) { push(@keys, $localpart.$delim.$extension.$append_to_user)
if $extension ne ''; push(@keys, $localpart.$append_to_user); }
push(@keys, $prepend_to_domain.$domain); if ($domain =~ /\[/) { push(@keys, $prepend_to_domain.'.'); } else {
my(@dkeys); my($d) = $domain;
for (;;) { push(@dkeys, $prepend_to_domain.'.'.$d);
last if $d eq '';
$d = ($d =~ /^([^.]*)\.(.*)\z/s) ? $2 : '';
}
if (@dkeys > 10) { @dkeys = @dkeys[$ push(@keys,@dkeys);
}
my($keys_ref) = []; for my $k (@keys) { push(@$keys_ref,$k) if !grep {$k eq $_} @$keys_ref }
ll(5) && do_log(5,"query_keys: ".join(', ',@$keys_ref));
my($rhs) = [ $addr, $saved_full_localpart, $localpart, $delim.$extension, $domain, ];
($keys_ref, $rhs);
}
sub quote_rfc2821_local($) {
my($mailbox) = @_;
my($atext) = "a-zA-Z0-9!#\$%&'*/=?^_`{|}~+-";
my($localpart,$domain) = split_address($mailbox);
if ($localpart !~ /^[$atext]+(\.[$atext]+)*\z/so) { $localpart =~ s/(["\\])/\\$1/g; # quoted-pair
$localpart = '"' . $localpart . '"'; # make a qcontent out of it
}
$domain = '' if $domain eq '@'; # strip off empty domain entirely
$localpart . $domain;
}
# wraps the result of quote_rfc2821_local into angle brackets <...> ;
# If given a list, it returns a list (possibly converted to
# comma-separated scalar), quoting each element;
#
sub qquote_rfc2821_local(@) {
my(@r) = map { $_ eq '' ? '<>' : ('<' . quote_rfc2821_local($_) . '>') } @_;
wantarray ? @r : join(', ', @r);
}
# unquote_rfc2821_local() strips away the quoting from the local part
# of an external (quoted) mailbox address, and returns internal (unquoted)
# mailbox address, as per rfc2821.
#
# Internal (unquoted) form is used internally by amavisd-new and other mail sw,
# external (quoted) form is used in SMTP commands and message headers.
#
sub unquote_rfc2821_local($) {
my($mailbox) = @_;
# the angle-bracket stripping is not really a duty of this subroutine,
# as it should have been already done elsewhere, but for the time being
# we do it here:
$mailbox = $1 if $mailbox =~ /^ \s* < ( .* ) > \s* \z/xs;
my($localpart,$domain) = split_address($mailbox);
$localpart =~ s/ " | \\ (.) | \\ \z /$1/xsg; $localpart . $domain;
}
sub one_response_for_all($$$) {
my($msginfo, $dsn_per_recip_capable, $am_id) = @_;
my($smtp_resp, $exit_code, $dsn_needed);
my($delivery_method) = $msginfo->delivery_method;
my($sender) = $msginfo->sender;
my($per_recip_data) = $msginfo->per_recip_data;
my($any_not_done) = scalar(grep { !$_->recip_done } @$per_recip_data);
if ($delivery_method ne '' && $any_not_done)
{ die "Explicit forwarding, but not all recips done" }
if (!@$per_recip_data) { $smtp_resp = "250 2.5.0 Ok, id=$am_id"; $exit_code = EX_OK;
do_log(5, "one_response_for_all <$sender>: no recipients, '$smtp_resp'");
}
if (!defined $smtp_resp) {
for my $r (@$per_recip_data) { if ($r->recip_smtp_response =~ /^4/) { $smtp_resp = $r->recip_smtp_response; last }
}
if (!defined $smtp_resp) {
for my $r (@$per_recip_data) { if ($r->recip_done && $r->recip_smtp_response !~ /^[245]/) {
$smtp_resp = '451 4.5.0 Bad SMTP response code??? "'
. $r->recip_smtp_response . '"';
last; }
}
}
if (defined $smtp_resp) {
$exit_code = EX_TEMPFAIL;
do_log(5, "one_response_for_all <$sender>: 4xx found, '$smtp_resp'");
}
}
if (!defined $smtp_resp) {
my($notall);
for my $r (@$per_recip_data) {
if ($r->recip_destiny == D_DISCARD) { $smtp_resp = $r->recip_smtp_response if !defined $smtp_resp }
else { $notall++; last } }
if ($notall) { $smtp_resp = undef }
if (defined $smtp_resp) {
$exit_code = $delivery_method eq '' ? 99 : EX_OK;
do_log(5, "one_response_for_all <$sender>: all DISCARD, '$smtp_resp'");
}
}
if (!defined $smtp_resp) {
my($notall, $done_level);
my($bounce_cnt) = 0;
for my $r (@$per_recip_data) {
my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response);
if ($dest == D_DISCARD) {
} elsif ($resp =~ /^5/ && $dest != D_BOUNCE) {
if (!defined $smtp_resp || $r->recip_done > $done_level)
{ $smtp_resp = $resp; $done_level = $r->recip_done }
} else { $notall++; last } }
if ($notall) { $smtp_resp = undef }
if (defined $smtp_resp) {
$exit_code = EX_UNAVAILABLE;
do_log(5, "one_response_for_all <$sender>: REJECTs, '$smtp_resp'");
}
}
if (!defined $smtp_resp) {
my($rej_cnt) = 0; my($bounce_cnt) = 0; my($drop_cnt) = 0;
for my $r (@$per_recip_data) {
my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response);
if ($resp =~ /^2/ && $dest == D_PASS) { $smtp_resp = $resp if !defined $smtp_resp }
$drop_cnt++ if $dest == D_DISCARD;
if ($resp =~ /^5/)
{ if ($dest == D_BOUNCE) { $bounce_cnt++ } else { $rej_cnt++ } }
}
$exit_code = EX_OK;
if (!defined $smtp_resp) { $smtp_resp = "250 2.5.0 Ok, id=$am_id";
if ($any_not_done) { $smtp_resp .= ", continue delivery" }
elsif ($delivery_method eq '') { $exit_code = 99 } }
if ($rej_cnt + $bounce_cnt + $drop_cnt > 0) {
$smtp_resp .= ", ";
$smtp_resp .= "but " if $rej_cnt+$bounce_cnt+$drop_cnt<@$per_recip_data;
$smtp_resp .= join ", and ",
map { my($cnt, $nm) = @$_;
!$cnt ? () : $cnt == @$per_recip_data ? $nm : "$cnt $nm"
} ([$rej_cnt,'REJECT'], [$bounce_cnt,'BOUNCE'], [$drop_cnt,'DISCARD']);
}
$dsn_needed =
($bounce_cnt > 0 || ($rej_cnt > 0 && !$dsn_per_recip_capable)) ? 1 : 0;
ll(5) && do_log(5,"one_response_for_all <$sender>: "
. ($rej_cnt + $bounce_cnt + $drop_cnt > 0 ? 'mixed' : 'success')
. ", dsn_needed=$dsn_needed, '$smtp_resp'");
}
($smtp_resp, $exit_code, $dsn_needed);
}
1;
package Amavis::Lookup::RE;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
BEGIN { import Amavis::Util qw(ll do_log) }
sub new($$) { my($class) = shift; bless [@_], $class }
sub lookup_re($$;$) {
my($self, $addr,$get_all) = @_;
local($1,$2,$3,$4); my(@matchingkey,@result);
for my $e (@$self) {
my($key,$r);
if (ref($e) eq 'ARRAY') { ($key,$r) = ($e->[0], @$e < 2 ? 1 : $e->[1]);
} else { ($key,$r) = ($e, 1);
}
my(@rhs) = $addr =~ /$key/; if (@rhs) { if (!ref($r) && $r=~/\$/) {
my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
{ my($j)=$2+$3+$4; $j<1 ? '' : $rhs[$j-1] }gxse;
$r .= substr($addr,0,0) if $any;
}
push(@result,$r); push(@matchingkey,$key);
last if !$get_all;
}
}
if (!ll(5)) {
} elsif (!@result) {
do_log(5,"lookup_re($addr), no matches");
} else { my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
e => "\e", a => "\a", t => "\t");
my(@mk) = @matchingkey;
for my $mk (@mk) { $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : $1 }egsx }
if (!$get_all) { do_log(5,sprintf('lookup_re(%s) matches key "%s", result=%s',
$addr,$mk[0],$result[0]));
} else { do_log(5,"lookup_re($addr) matches keys: ".
join(', ', map {sprintf('"%s"=>%s',$mk[$_],$result[$_])}
(0..$ }
}
if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
else { !wantarray ? \@result : (\@result, \@matchingkey) }
}
1;
package Amavis::Lookup::Label;
use strict;
use re 'taint';
sub new($$) { my($class) = shift; my($str) = shift; bless \$str, $class }
sub display($) { my($self) = shift; $$self }
1;
package Amavis::Lookup;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = ();
@EXPORT_OK = qw(&lookup &lookup_ip_acl);
}
use subs @EXPORT_OK;
BEGIN {
import Amavis::Util qw(ll do_log);
import Amavis::Conf qw(:platform c cr ca);
import Amavis::Timing qw(section_time);
import Amavis::rfc2821_2822_Tools qw(split_address make_query_keys);
}
sub lookup_hash($$;$) {
my($addr, $hash_ref,$get_all) = @_;
(ref($hash_ref) eq 'HASH')
or die "lookup_hash: arg2 must be a hash ref: $hash_ref";
local($1,$2,$3,$4); my(@matchingkey,@result);
my($keys_ref,$rhs_ref) = make_query_keys($addr,1,1);
for my $key (@$keys_ref) { if (defined $$hash_ref{$key}) { push(@result,$$hash_ref{$key}); push(@matchingkey,$key);
last if !$get_all;
}
}
for my $r (@result) { if (!ref($r) && $r=~/\$/) { my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
{ my($j)=$2+$3+$4; $j<1 ? '' : $rhs_ref->[$j-1] }gxse;
$r .= substr($addr,0,0) if $any;
}
}
if (!ll(5)) {
} elsif (!@result) {
do_log(5,"lookup_hash($addr), no matches");
} elsif (!$get_all) { do_log(5,sprintf('lookup_hash(%s) matches key "%s", result=%s',
$addr,$matchingkey[0],$result[0]));
} else { do_log(5,"lookup_hash($addr) matches keys: ".
join(', ', map {sprintf('"%s"=>%s',$matchingkey[$_],$result[$_])}
(0..$ }
if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
else { !wantarray ? \@result : (\@result, \@matchingkey) }
}
sub lookup_acl($$) {
my($addr, $acl_ref) = @_;
(ref($acl_ref) eq 'ARRAY')
or die "lookup_acl: arg2 must be a list ref: $acl_ref";
return undef if !@$acl_ref; my($lpcs) = c('localpart_is_case_sensitive');
my($localpart,$domain) = split_address($addr); $domain = lc($domain);
$localpart = lc($localpart) if !$lpcs;
local($1,$2);
$domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s;
my($lcaddr) = $localpart . '@' . $domain;
my($found, $matchingkey, $result);
for my $e (@$acl_ref) {
$result = 1; $matchingkey = $e; my($key) = $e;
if ($key =~ /^(!+)(.*)\z/s) { $key = $2;
$result = 1-$result if (length($1) & 1); }
if ($key =~ /^(.*?)\@([^@]*)\z/s) { $found++ if $localpart eq ($lpcs?$1:lc($1)) && $domain eq lc($2);
} elsif ($key =~ /^\.(.*)\z/s) { my($key_t) = lc($1);
$found++ if $domain eq $key_t || $domain =~ /(\.|\z)\Q$key_t\E\z/s;
} else { $found++ if $domain eq lc($key);
}
last if $found;
}
$matchingkey = $result = undef if !$found;
do_log(5, "lookup_acl($addr)".
(!$found?", no match":" matches key \"$matchingkey\", result=$result"));
!wantarray ? $result : ($result, $matchingkey);
}
sub lookup($$@) {
my($get_all, $addr, @tables) = @_;
my($label, @result,@matchingkey);
for my $tb (@tables) {
my($t) = ref($tb) eq 'REF' ? $$tb : $tb; if (!ref($t) || ref($t) eq 'SCALAR') { my($r) = ref($t) ? $$t : $t; if (defined $r) {
do_log(5,"lookup: (scalar) matches, result=\"$r\"");
push(@result,$r); push(@matchingkey,"(constant:$r)");
}
} elsif (ref($t) eq 'HASH') {
my($r,$mk) = lookup_hash($addr,$t,$get_all);
if (!defined $r) {}
elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
} elsif (ref($t) eq 'ARRAY') {
my($r,$mk) = lookup_acl($addr,$t);
if (defined $r) { push(@result,$r); push(@matchingkey,$mk) }
} elsif ($t->isa('Amavis::Lookup::Label')) { $label = $t->display; } elsif ($t->isa('Amavis::Lookup::RE')) {
my($r,$mk) = $t->lookup_re($addr,$get_all);
if (!defined $r) {}
elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
} elsif ($t->isa('Amavis::Lookup::SQL')) {
my($r,$mk) = $t->lookup_sql($addr,$get_all);
if (!defined $r) {}
elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
} elsif ($t->isa('Amavis::Lookup::SQLfield')) {
my($r,$mk) = $t->lookup_sql_field($addr,$get_all);
if (!defined $r) {}
elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
} elsif ($t->isa('Amavis::Lookup::LDAP')) {
my($r,$mk) = $t->lookup_ldap($addr,$get_all);
if (!defined $r) {}
elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
} elsif ($t->isa('Amavis::Lookup::LDAPattr')) {
my($r,$mk) = $t->lookup_ldap_attr($addr,$get_all);
if (!defined $r) {}
elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) }
elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) }
} else {
die "TROUBLE: lookup table is an unknown object: " . ref($t);
}
last if @result && !$get_all;
}
if ($label ne '') { $label = " ($label)" }
if (!ll(4)) {
} elsif (!@tables) {
do_log(4,"lookup$label => undef, \"$addr\", no lookup tables");
} elsif (!@result) {
do_log(4,"lookup$label => undef, \"$addr\" does not match");
} elsif (!$get_all) { do_log(4,sprintf(
'lookup%s => %-6s "%s" matches, result=%s, matching_key="%s"',
$label, $result[0]?'true,':'false,', $addr,
(ref $result[0] ne 'ARRAY' ? '"'.$result[0].'"'
: '('.join(',',@{$result[0]}).')'),
$matchingkey[0]));
} else { do_log(4,sprintf('lookup%s, %d matches for "%s", results: %s',
$label, scalar(@result), $addr,
join(', ', map {sprintf('"%s"=>%s', $matchingkey[$_],
(ref $result[$_] ne 'ARRAY'
? '"'.$result[$_].'"'
: '('.join(',',@{$result[$_]}).')') )}
(0..$ }
if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
else { !wantarray ? \@result : (\@result, \@matchingkey) }
}
sub ip_to_vec($;$) {
my($ip,$allow_mask) = @_;
my($ip_len); my(@ip_fields);
local($1,$2,$3,$4,$5,$6);
$ip =~ s/^[ \t]+//; $ip =~ s/[ \t\n]+\z//s; # trim
my($ipa) = $ip;
($ipa,$ip_len) = ($1,$2) if $allow_mask && $ip =~ m{^([^/]*)/(.*)\z}s;
if ($ipa =~ m{^(IPv6:)?(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z}si){
!grep {$_ > 255} ($3,$4,$5,$6)
or die "Invalid decimal field value in IPv6 address: $ip";
$ipa = $2 . sprintf("%02X%02X:%02X%02X", $3,$4,$5,$6);
} elsif ($ipa =~ m{^\d{1,3}(?:\.\d{1,3}){0,3}\z}) { my(@d) = split(/\./,$ipa,-1);
!grep {$_ > 255} @d or die "Invalid field value in IPv4 address: $ip";
defined($ip_len) || @d==4
or die "IPv4 address $ip contains fewer than 4 fields";
$ipa = '::FFFF:' . sprintf("%02X%02X:%02X%02X", @d); if (!defined($ip_len)) { $ip_len = 32; } elsif ($ip_len =~ /^\d{1,9}\z/) { } elsif ($ip_len =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
!grep {$_ > 255} ($1,$2,$3,$4)
or die "Illegal field value in IPv4 mask: $ip";
my($mask1) = pack('C4',$1,$2,$3,$4); my($len) = unpack("%b*",$mask1); my($mask2) = pack('B32', '1' x $len); $mask1 eq $mask2
or die "IPv4 mask not representing valid CIDR mask: $ip";
$ip_len = $len;
} else {
die "Invalid IPv4 network mask or CIDR prefix length: $ip";
}
$ip_len<=32 or die "IPv4 network prefix length greater than 32: $ip";
$ip_len += 128-32; }
$ip_len = 128 if !defined($ip_len);
$ip_len<=128 or die "IPv6 network prefix length greater than 128: $ip";
$ipa =~ s/^IPv6://i;
if ($ipa !~ /^(.*?)::(.*)\z/s) { @ip_fields = split(/:/,$ipa,-1); } else { my(@a) = split(/:/,$1,-1); my(@b) = split(/:/,$2,-1);
my($missing_cnt) = 8-(@a+@b); $missing_cnt = 1 if $missing_cnt<1;
@ip_fields = (@a, (0) x $missing_cnt, @b);
}
!grep { !/^[0-9a-zA-Z]{1,4}\z/ } @ip_fields
or die "Invalid syntax of IPv6 address: $ip";
@ip_fields<8 and die "IPv6 address $ip contains fewer than 8 fields";
@ip_fields>8 and die "IPv6 address $ip contains more than 8 fields";
my($vec) = pack("n8", map {hex} @ip_fields);
$ip_len=~/^\d{1,3}\z/
or die "Invalid prefix length syntax in IP address: $ip";
$ip_len<=128 or die "Invalid prefix length in IPv6 address: $ip";
my($mask) = pack('B128', '1' x $ip_len);
($vec,$mask,$ip_len);
}
sub lookup_ip_acl($@) {
my($ip, @nets_ref) = @_;
my($ip_vec,$ip_mask) = ip_to_vec($ip,0);
my($label,$found,$fullkey,$result);
for my $tb (@nets_ref) {
my($t) = ref($tb) eq 'REF' ? $$tb : $tb; if (!ref($t) || ref($t) eq 'SCALAR') { my($r) = ref($t) ? $$t : $t; $result = $r; $fullkey = "(constant:$r)";
$found++ if defined $result;
} elsif (ref($t) eq 'HASH') {
my($ip_c); my($ip_dq); $ip_c = join(':', map {sprintf('%04x',$_)} unpack('n8',$ip_vec));
my($ipv4_vec,$ipv4_mask) = ip_to_vec('::FFFF:0:0/96',1);
if ( ($ip_vec & $ipv4_mask) eq ($ipv4_vec & $ipv4_mask) ) {
$ip_dq = join('.', unpack('C4',substr($ip_vec,12,4))); }
do_log(5, "lookup_ip_acl keys: \"$ip_dq\", \"$ip_c\"");
if (defined $ip_dq) { $fullkey = $ip_dq; $result = $t->{$fullkey};
$found++ if defined $result;
}
if (!$found) { $fullkey = $ip_c; $result = $t->{$fullkey};
$found++ if defined $result;
}
} elsif (ref($t) eq 'ARRAY') {
for my $net (@$t) {
$fullkey = $net; my($key) = $fullkey; $result = 1;
if ($key =~ /^(!+)(.*)\z/s) { $key = $2;
$result = 1 - $result if (length($1) & 1); }
my($acl_ip_vec, $acl_mask) = ip_to_vec($key,1);
$found++ if ($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask);
last if $found;
}
} elsif ($t->isa('Amavis::Lookup::Label')) { $label = $t->display; } else {
die "TROUBLE: lookup table is an unknown object: " . ref($t);
}
last if $found;
}
$fullkey = $result = undef if !$found;
if ($label ne '') { $label = " ($label)" }
ll(4) && do_log(4, "lookup_ip_acl$label: key=\"$ip\""
. (!$found ? ", no match" : " matches \"$fullkey\", result=$result"));
!wantarray ? $result : ($result, $fullkey);
}
1;
package Amavis::Expand;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = ();
@EXPORT_OK = qw(&expand);
}
use subs @EXPORT_OK;
BEGIN {
import Amavis::Util qw(ll do_log);
}
sub expand($$) {
my($str_ref) = shift; my($builtins_href) = shift; my($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) =
\('[', '[?', ']', '|', '#'); my(%lexmap); for (keys(%$builtins_href))
{ $lexmap{"%$_"} = \"%$_"; $lexmap{"% for ($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) { $lexmap{$$_} = $_ }
my(@tokens) = $$str_ref =~ /\G \ \\ [0-7]{1,3} | [^\[\]\\|%\n my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
e => "\e", a => "\a", t => "\t");
for (@tokens) {
if (exists $lexmap{$_}) { $_ = $lexmap{$_} } elsif ($_ eq "\\\n") { $_ = '' } elsif (/^%(%)\z/) { $_ = $1 } elsif (/^(% elsif (/^\\([0-7]{1,3})\z/) { $_ = chr(oct($1)) } elsif (/^\\(.)\z/s) { $_ = (exists($esc{$1}) ? $esc{$1} : $1) }
}
my($level) = 0; my($quote_level) = 0; my(@macro_type, @arg);
my($output_str) = ''; my($whereto) = \$output_str;
while (@tokens) {
my($t) = shift(@tokens);
if ($t eq '') { } elsif ($quote_level>0 && ref($t) && ($t == $lex_lbr || $t == $lex_lbrq)){
$quote_level++;
ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
} elsif (ref($t) && $t == $lex_lbr) { $quote_level++; $level++;
unshift(@arg, [[]]); unshift(@macro_type, ''); $whereto = $arg[0][0];
} elsif (ref($t) && $t == $lex_lbrq) { $level++; unshift(@arg, [[]]); unshift(@macro_type, '');
$whereto = $arg[0][0]; $macro_type[0] = 'select';
} elsif ($quote_level > 1 && ref($t) && $t == $lex_rbr) {
$quote_level--;
ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
} elsif ($level > 0 && ref($t) && $t == $lex_sep) { if ($quote_level == 0 && $macro_type[0] eq 'select' && @{$arg[0]} == 1) {
$quote_level++;
}
if ($quote_level == 1) {
unshift(@{$arg[0]}, []); $whereto = $arg[0][0]; } else {
ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
}
} elsif ($quote_level > 0 && ref($t) && $t == $lex_rbr) {
$quote_level--; $level-- if $level > 0;
my(@result);
if ($macro_type[0] eq 'select') {
my($sel, @alternatives) = reverse @{$arg[0]}; $sel = !ref($sel) ? '' : join('', @$sel); if ($sel =~ /^\s*\z/) { $sel = 0 }
elsif ($sel =~ /^\s*(\d+)\s*\z/) { $sel = 0+$1 } else { $sel = 1 }
push(@alternatives, []) if @alternatives < 2 && $sel > 0;
if ($sel < 0) { $sel = 0 }
elsif ($sel > $ @result = @{$alternatives[$sel]};
} else { my($cvar_r, $sep_r, $body_r, $cvar); if (@{$arg[0]} >= 3) { ($cvar_r,$body_r,$sep_r) = reverse @{$arg[0]} }
else { ($body_r, $sep_r) = reverse @{$arg[0]}; $cvar_r = $body_r }
for (@$cvar_r) {
if (ref && $$_ =~ /^%(.)\z/s) { $cvar = $1; last }
}
if (exists($builtins_href->{$cvar})) {
my($values_r) = $builtins_href->{$cvar};
while (ref($values_r) eq 'CODE') { $values_r = &$values_r }
$values_r = [$values_r] if !ref($values_r);
my($ind);
my($re) = qr/^%\Q$cvar\E\z/;
for my $val (@$values_r) {
push(@result, @$sep_r) if ++$ind > 1 && ref($sep_r);
push(@result, map { (ref && $$_ =~ /$re/) ? $val : $_ } @$body_r);
}
}
}
shift(@macro_type); shift(@arg);
$whereto = $level > 0 ? $arg[0][0] : \$output_str;
unshift(@tokens, @result); } else { my($s) = '';
if ($quote_level > 0 || !ref($t)) {
$s = $t; } elsif ($t == $lex_h) { while (@tokens) { last if shift(@tokens) eq "\n" }
} elsif ($$t =~ /^%\ if (!exists($builtins_href->{$1})) { $s = 0 } else {
$s = $builtins_href->{$1};
while (ref($s) eq 'CODE') { $s = &$s } $s = ref($s) ? @$s : ($s !~ /^\s*\z/);
}
} elsif ($$t =~ /^%(.)\z/s) { if (!exists($builtins_href->{$1})) { $s = ''} else {
$s = $builtins_href->{$1};
while (ref($s) eq 'CODE') { $s = &$s } $s = join(', ', @$s) if ref $s;
}
} else { $s = $$t } ref($whereto) eq 'ARRAY' ? push(@$whereto, $s) : ($$whereto .= $s);
}
}
\$output_str;
}
1;
package Amavis::In::Connection;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
sub new
{ my($class) = @_; bless {}, $class }
sub client_ip { my($self)=shift; !@_ ? $self->{client_ip} : ($self->{client_ip}=shift) }
sub socket_ip { my($self)=shift; !@_ ? $self->{socket_ip} : ($self->{socket_ip}=shift) }
sub socket_port { my($self)=shift; !@_ ? $self->{socket_port}:($self->{socket_port}=shift) }
sub proto { my($self)=shift; !@_ ? $self->{proto} : ($self->{proto}=shift) }
sub smtp_proto { my($self)=shift; !@_ ? $self->{smtp_proto}: ($self->{smtp_proto}=shift) }
sub smtp_helo { my($self)=shift; !@_ ? $self->{smtp_helo} : ($self->{smtp_helo}=shift) }
1;
package Amavis::In::Message::PerRecip;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
sub new { my($class) = @_; bless [(undef) x 11], $class }
sub recip_addr { my($self)=shift; !@_ ? $$self[0] : ($$self[0]=shift) }
sub recip_addr_modified
{ my($self)=shift; !@_ ? $$self[1] : ($$self[1]=shift) }
sub recip_destiny { my($self)=shift; !@_ ? $$self[2] : ($$self[2]=shift) }
sub recip_done { my($self)=shift; !@_ ? $$self[3] : ($$self[3]=shift) }
sub recip_smtp_response { my($self)=shift; !@_ ? $$self[4] : ($$self[4]=shift) }
sub recip_remote_mta_smtp_response { my($self)=shift; !@_ ? $$self[5] : ($$self[5]=shift) }
sub recip_remote_mta { my($self)=shift; !@_ ? $$self[6] : ($$self[6]=shift) }
sub recip_mbxname { my($self)=shift; !@_ ? $$self[7] : ($$self[7]=shift) }
sub recip_whitelisted_sender { my($self)=shift; !@_ ? $$self[8] : ($$self[8]=shift) }
sub recip_blacklisted_sender { my($self)=shift; !@_ ? $$self[9] : ($$self[9]=shift) }
sub recip_score_boost { my($self)=shift; !@_ ? $$self[10] : ($$self[10]=shift) }
sub recip_final_addr { my($self)=shift;
my($newaddr) = $self->recip_addr_modified;
defined $newaddr ? $newaddr : $self->recip_addr;
}
1;
package Amavis::In::Message;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
BEGIN {
import Amavis::Conf qw( :platform );
import Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp);
import Amavis::In::Message::PerRecip;
}
sub new
{ my($class) = @_; bless {}, $class }
sub rx_time { my($self)=shift; !@_ ? $self->{rx_time} : ($self->{rx_time}=shift) }
sub client_addr { my($self)=shift; !@_ ? $self->{cli_ip} : ($self->{cli_ip}=shift) }
sub client_name { my($self)=shift; !@_ ? $self->{cli_name} : ($self->{cli_name}=shift) }
sub client_proto { my($self)=shift; !@_ ? $self->{cli_proto} : ($self->{cli_proto}=shift) }
sub client_helo { my($self)=shift; !@_ ? $self->{cli_helo} : ($self->{cli_helo}=shift) }
sub queue_id { my($self)=shift; !@_ ? $self->{queue_id} : ($self->{queue_id}=shift) }
sub msg_size { my($self)=shift; !@_ ? $self->{msg_size} : ($self->{msg_size}=shift) }
sub auth_user { my($self)=shift; !@_ ? $self->{auth_user} : ($self->{auth_user}=shift) }
sub auth_pass { my($self)=shift; !@_ ? $self->{auth_pass} : ($self->{auth_pass}=shift) }
sub auth_submitter { my($self)=shift; !@_ ? $self->{auth_subm} : ($self->{auth_subm}=shift) }
sub body_type { my($self)=shift; !@_ ? $self->{body_type} : ($self->{body_type}=shift) }
sub sender { my($self)=shift; !@_ ? $self->{sender} : ($self->{sender}=shift) }
sub sender_contact { my($self)=shift; !@_ ? $self->{sender_c} : ($self->{sender_c}=shift) }
sub sender_source { my($self)=shift; !@_ ? $self->{sender_src} : ($self->{sender_src}=shift) }
sub mime_entity { my($self)=shift; !@_ ? $self->{mime_entity}: ($self->{mime_entity}=shift)}
sub parts_root { my($self)=shift; !@_ ? $self->{parts_root}: ($self->{parts_root}=shift)}
sub mail_text { my($self)=shift; !@_ ? $self->{mail_text} : ($self->{mail_text}=shift) }
sub mail_text_fn { my($self)=shift; !@_ ? $self->{mail_text_fn} : ($self->{mail_text_fn}=shift) }
sub mail_tempdir { my($self)=shift; !@_ ? $self->{mail_tempdir} : ($self->{mail_tempdir}=shift) }
sub header_edits { my($self)=shift; !@_ ? $self->{hdr_edits} : ($self->{hdr_edits}=shift) }
sub orig_header { my($self)=shift; !@_ ? $self->{orig_header}: ($self->{orig_header}=shift) }
sub orig_header_size { my($self)=shift; !@_ ? $self->{orig_hdr_s} : ($self->{orig_hdr_s}=shift) }
sub orig_body_size { my($self)=shift; !@_ ? $self->{orig_bdy_s} : ($self->{orig_bdy_s}=shift) }
sub body_digest { my($self)=shift; !@_ ? $self->{body_digest}: ($self->{body_digest}=shift) }
sub quarantined_to { my($self)=shift; !@_ ? $self->{quarantine} : ($self->{quarantine}=shift) }
sub dsn_sent { my($self)=shift; !@_ ? $self->{dsn_sent} : ($self->{dsn_sent}=shift) }
sub delivery_method { my($self)=shift; !@_ ? $self->{delivery_method} : ($self->{delivery_method}=shift) }
sub client_delete { my($self)=shift; !@_ ? $self->{client_delete} : ($self->{client_delete}=shift) }
sub per_recip_data { my($self) = shift;
if (@_) { @{$self->{recips}} = @{$_[0]} }
$self->{recips};
}
sub recips { my($self)=shift;
if (@_) { $self->per_recip_data([ map {
my($per_recip_obj) = Amavis::In::Message::PerRecip->new;
$per_recip_obj->recip_addr($_);
$per_recip_obj->recip_destiny(D_PASS); $per_recip_obj } @{$_[0]} ]);
}
return if !defined wantarray; [ map { $_->recip_addr } @{$self->per_recip_data} ];
}
1;
package Amavis::Out::EditHeader;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&hdr);
}
BEGIN {
import Amavis::Conf qw(:platform c cr ca);
import Amavis::Timing qw(section_time);
import Amavis::Util qw(ll do_log safe_encode q_encode);
}
use MIME::Words;
sub new { my($class) = @_; bless {}, $class }
sub prepend_header($$$;$) {
my($self, $field_name, $field_body, $structured) = @_;
unshift(@{$self->{prepend}}, hdr($field_name, $field_body, $structured));
}
sub append_header($$$;$) {
my($self, $field_name, $field_body, $structured) = @_;
push(@{$self->{append}}, hdr($field_name, $field_body, $structured));
}
sub delete_header($$) {
my($self, $field_name) = @_;
$self->{edit}{lc($field_name)} = undef;
}
sub edit_header($$$;$) {
my($self, $field_name, $field_edit_sub, $structured) = @_;
!defined($field_edit_sub) || ref($field_edit_sub) eq 'CODE'
or die "edit_header: arg#3 must be undef or a subroutine ref";
$self->{edit}{lc($field_name)} = $field_edit_sub;
}
sub inherit_header_edits($$) {
my($self, $other_edits) = @_;
if (defined $other_edits) {
unshift(@{$self->{prepend}},
@{$other_edits->{prepend}}) if $other_edits->{prepend};
unshift(@{$self->{append}},
@{$other_edits->{append}}) if $other_edits->{append};
if ($other_edits->{edit}) {
for (keys %{$other_edits->{edit}})
{ $self->{edit}{$_} = $other_edits->{edit}{$_} }
}
}
}
sub hdr($$;$) {
my($field_name, $field_body, $structured) = @_;
if ($field_name =~ /^(X-.*|Subject|Comments)\z/si &&
$field_body =~ /[^\011\012\040-\176]/ ) { $field_body =~ s/\n([ \t])/$1/g; chomp($field_body);
my($field_body_octets) = safe_encode(c('hdr_encoding'), $field_body);
my($qb) = c('hdr_encoding_qb');
if (uc($qb) eq 'Q') {
$field_body = q_encode($field_body_octets, $qb, c('hdr_encoding'));
} else {
$field_body = MIME::Words::encode_mimeword($field_body_octets,
$qb, c('hdr_encoding'));
}
} else { $field_body = safe_encode('ascii', $field_body);
}
$field_name = safe_encode('ascii', $field_name);
my($str) = $field_name . ':';
$str .= ' ' if $field_body !~ /^[ \t]/;
$str .= $field_body;
$str =~ s/\n([^ \t\n])/\n $1/g; $str =~ s/\n([ \t]*\n)+/\n/g; chomp($str); if ($structured) {
my(@sublines) = split(/\n/, $str, -1);
$str = ''; my($s) = ''; my($s_l) = 0;
for (@sublines) { if ($s !~ /^\s*\z/ && $s_l + length($_) > 78) {
$str .= "\n" if $str ne '';
$str .= $s; $s = ''; $s_l = 0;
}
$s .= $_; $s_l += length($_);
}
if ($s !~ /^\s*\z/) {
$str .= "\n" if $str ne '';
$str .= $s;
}
} elsif (length($str) > 998) {
$str = substr($str,0,998);
}
$str .= "\n"; do_log(5, "header: $str");
$str;
}
sub write_header($$$) {
my($self, $msg, $out_fh) = @_;
$out_fh = IO::Wrap::wraphandle($out_fh); my($is_mime) = ref($msg) && $msg->isa('MIME::Entity');
my(@header);
if ($is_mime) {
@header = map { /^[ \t]*\n?\z/ ? () : (/\n\z/ ? $_ : $_ . "\n") } @{$msg->header};
}
my($received_cnt) = 0; my($str) = '';
for (@{$self->{prepend}}) { $str .= $_ }
if ($str ne '') { $out_fh->print($str) or die "sending mail header1: $!" }
if (!defined($msg)) {
} elsif (!exists($self->{edit}) || !scalar(%{$self->{edit}})) {
if ($is_mime) {
for my $h (@header)
{ $out_fh->print($h) or die "sending mail header2: $!" }
} else { while (<$msg>) { last if $_ eq $eol; $out_fh->print($_) or die "sending mail header3: $!";
}
}
} else { my($curr_head, $next_head);
push(@header, $eol) if $is_mime; while (defined($next_head = $is_mime ? shift @header : <$msg>)) {
if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head } else { if (!defined($curr_head)) { } elsif ($curr_head !~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s) {
$out_fh->print($curr_head) or die "sending mail header4: $!";
} else { my($field_name, $field_body) = ($1, $2);
my($field_name_lc) = lc($field_name);
$received_cnt++ if $field_name_lc eq 'received';
if (!exists($self->{edit}{$field_name_lc})) { $out_fh->print($curr_head) or die "sending mail header5: $!";
} else {
my($edit) = $self->{edit}{$field_name_lc};
if (defined($edit)) { chomp($field_body);
$out_fh->print(hdr($field_name, &$edit($field_name,$field_body)))
or die "sending mail header6: $!";
}
}
}
last if $next_head eq $eol; $curr_head = $next_head;
}
}
}
$str = '';
for (@{$self->{append}}) { $str .= $_ }
$str .= $eol; $out_fh->print($str) or die "sending mail header7: $!";
section_time('write-header');
$received_cnt;
}
1;
package Amavis::Out::Local;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&mail_to_local_mailbox);
}
use Errno qw(ENOENT);
use POSIX qw(strftime);
use IO::File ();
use IO::Wrap;
BEGIN {
import Amavis::Conf qw(:platform $gzip $bzip2 c cr ca);
import Amavis::Lock;
import Amavis::Util qw(ll do_log am_id exit_status_str run_command_consumer);
import Amavis::Timing qw(section_time);
import Amavis::rfc2821_2822_Tools;
import Amavis::Out::EditHeader;
}
use subs @EXPORT_OK;
sub mail_to_local_mailbox(@) {
my($via, $msginfo, $initial_submission, $filter) = @_;
$via =~ /^local:(.*)\z/si or die "Bad local method: $via";
my($via_arg) = $1;
my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
@{$msginfo->per_recip_data};
return 1 if !@per_recip_data;
my($msg) = $msginfo->mail_text; if (defined($msg) && !$msg->isa('MIME::Entity')) {
$msg = IO::Wrap::wraphandle($msg); }
my($sender) = $msginfo->sender;
for my $r (@per_recip_data) {
my($recip) = $r->recip_final_addr;
next if $recip eq '';
my($localpart,$domain) = split_address($recip);
my($smtp_response);
my($mbxname, $suggested_filename);
{ my($ldar) = cr('local_delivery_aliases'); my($alias) = $ldar->{$localpart};
if (ref($alias) eq 'ARRAY') {
($mbxname, $suggested_filename) = @$alias;
} elsif (ref($alias) eq 'CODE') { ($mbxname, $suggested_filename) = &$alias;
} elsif ($alias ne '') {
($mbxname, $suggested_filename) = ($alias, undef);
} elsif (!exists $ldar->{$localpart}) {
do_log(0, "no key '$localpart' in \%local_delivery_aliases, skip local delivery");
}
if ($mbxname eq '') {
my($why) = !exists $ldar->{$localpart} ? 1 : $alias eq '' ? 2 : 3;
do_log(2, "skip local delivery($why): <$sender> -> <$recip>");
$smtp_response = "250 2.6.0 Ok, skip local delivery($why)";
last; }
my($ux); if (!-d $mbxname) { $ux = 1; } else { $ux = 0; if ($suggested_filename eq '')
{ $suggested_filename = $via_arg ne '' ? $via_arg : 'msg-%i-%n' }
$suggested_filename =~ s{%(.)}
{ $1 eq 'b' ? $msginfo->body_digest
: $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time))
: $1 eq 'n' ? am_id()
: $1 eq '%' ? '%' : '%'.$1 }egs;
$mbxname = "$mbxname/$suggested_filename";
}
do_log(1, "local delivery: <$sender> -> <$recip>, mbx=$mbxname");
my($mp,$pos,$pipe,$pid);
my($errn) = stat($mbxname) ? 0 : 0+$!;
local $SIG{CHLD} = 'DEFAULT';
local $SIG{PIPE} = 'IGNORE'; eval { if (!$ux) { if ($errn == ENOENT) { } elsif (!$errn && -e _)
{ die "File $mbxname already exists, refuse to overwrite" }
if ($mbxname =~ /\.gz\z/) {
($mp,$pid) = run_command_consumer($mbxname,undef,$gzip);
$pipe = 1;
} else {
$mp = IO::File->new;
$mp->open($mbxname,'>',0640)
or die "Can't create file $mbxname: $!";
}
} else { if ($errn == ENOENT) {
$mp = IO::File->new;
$mp->open($mbxname,'>',0640)
or die "Can't create file $mbxname: $!";
} elsif (!$errn && !-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;
$mp->open($mbxname,'>>',0640) or die "Can't append to $mbxname: $!";
}
binmode($mp, ":bytes") or die "Can't cancel :utf8 mode: $!"
if $unicode_aware;
lock($mp);
$mp->seek(0,2) or die "Can't position mailbox file to its tail: $!";
$pos = $mp->tell;
}
if (defined($msg) && !$msg->isa('MIME::Entity'))
{ $msg->seek(0,0) or die "Can't rewind mail file: $!" }
};
if ($@ ne '') {
chomp($@);
$smtp_response = $@ eq "timed out" ? "450 4.4.2" : "451 4.5.0";
$smtp_response .= " Local delivery(1) to $mbxname failed: $@";
last; }
eval { if ($ux) {
$mp->printf("From %s %s$eol", quote_rfc2821_local($sender),
scalar(localtime($msginfo->rx_time)) )
or die "Can't write 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',
quote_rfc2821_local($recip));
$hdr_edits->prepend_header('Return-Path',
qquote_rfc2821_local($sender));
my($received_cnt) = $hdr_edits->write_header($msg,$mp);
if ($received_cnt > 110) {
die "Too many hops: $received_cnt 'Received:' header lines\n";
}
if (!$ux) { while ($msg->read($_,16384) > 0)
{ $mp->print($_) or die "Can't write to $mbxname: $!" }
} else { my($blank_line) = 1;
while (<$msg>) {
$mp->print('>') or die "Can't write to $mbxname: $!"
if $blank_line && /^From /;
$mp->print($_) or die "Can't write to $mbxname: $!";
$blank_line = $_ eq $eol;
}
}
$mp->print($eol) or die "Can't write to $mbxname: $!" if $ux;
};
my($failed) = 0;
if ($@ ne '') { chomp($@);
if ($ux && defined($pos) && $can_truncate) {
$mp->truncate($pos) or die "Can't truncate file $mbxname: $!";
}
$failed = 1;
}
unlock($mp) if $ux;
if (!$pipe) {
$mp->close or die "Can't close $mbxname: $!";
} else {
my($err); $mp->close or $err = $!;
$?==0 or die ("Closing pipe to $gzip: ".exit_status_str($?,$err));
}
if (!$failed) {
$smtp_response = "250 2.6.0 Ok, delivered to $mbxname";
} elsif ($@ eq "timed out") {
$smtp_response = "450 4.4.2 Local delivery to $mbxname timed out";
} elsif ($@ =~ /too many hops/i) {
$smtp_response = "550 5.4.6 Rejected delivery to mailbox $mbxname: $@";
} else {
$smtp_response = "451 4.5.0 Local delivery to mailbox $mbxname failed: $@";
}
} do_log(-1, $smtp_response) if $smtp_response !~ /^2/;
$smtp_response .= ", id=" . am_id();
$r->recip_smtp_response($smtp_response);
$r->recip_done(2);
$r->recip_mbxname($mbxname) if defined $mbxname;
}
section_time('save-to-local-mailbox');
}
1;
package Amavis::Out;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = qw(&mail_dispatch);
}
use IO::File ();
use IO::Wrap;
use Net::Cmd;
use Net::SMTP 2.24;
use POSIX qw(strftime
WIFEXITED WIFSIGNALED WIFSTOPPED
WEXITSTATUS WTERMSIG WSTOPSIG);
BEGIN {
import Amavis::Conf qw(:platform $DEBUG $QUARANTINEDIR
$relayhost_is_client c cr ca);
import Amavis::Util qw(untaint min max ll do_log debug_oneshot
am_id snmp_count retcode exit_status_str
prolong_timer run_command_consumer);
import Amavis::Timing qw(section_time);
import Amavis::rfc2821_2822_Tools;
import Amavis::Out::Local qw(mail_to_local_mailbox);
import Amavis::Out::EditHeader;
}
sub dynamic_destination($$) {
my($method,$conn) = @_;
my($client_ip) = !defined($conn) ? undef : $conn->client_ip;
if ($client_ip ne '' && $method =~ /^smtp\b/i) {
my($new_method); my($relayhost,$relayhost_port,$rest);
(undef,$relayhost,$relayhost_port,$rest) = split(/:/,$method,4);
if ($relayhost_is_client) { ($relayhost,$relayhost_port,$rest) = ('*','*','') }
$relayhost = "[$client_ip]" if $relayhost eq '*';
$relayhost_port = $conn->socket_port+1 if $relayhost_port eq '*';
$rest = ':'.$rest if $rest ne '';
$new_method = sprintf("smtp:%s:%s%s", $relayhost,$relayhost_port,$rest);
if ($new_method ne $method) {
do_log(3, "dynamic destination override: $method -> $new_method");
$method = $new_method;
}
}
$method;
}
sub mail_dispatch($$$;$) {
my($conn) = shift; my($msginfo,$initial_submission,$filter) = @_;
my($via) = $msginfo->delivery_method;
if ($via =~ /^smtp:/i) {
mail_via_smtp(dynamic_destination($via,$conn), @_);
} elsif ($via =~ /^pipe:/i) {
mail_via_pipe($via, @_);
} elsif ($via =~ /^bsmtp:/i) {
mail_via_bsmtp($via, @_);
} elsif ($via =~ /^local:/i) {
mail_to_local_mailbox($via, $msginfo, $initial_submission,
sub { shift->recip_final_addr !~ /\@/ ? 1 : 0 });
if (grep { !$_->recip_done } @{$msginfo->per_recip_data}) {
my($nm) = c('notify_method'); if ($nm =~ /^smtp:/i) { mail_via_smtp(dynamic_destination($nm,$conn),@_)}
elsif ($nm =~ /^pipe:/i) { mail_via_pipe($nm, @_) }
elsif ($nm =~ /^bsmtp:/i) { mail_via_bsmtp($nm, @_) }
}
}
}
sub new_smtp_data { my($class, $sh) = @_; bless \$sh, $class }
sub print {
my($self) = shift;
$$self->datasend(\@_) or die "datasend timed out while sending header\n";
}
sub mail_via_smtp(@) {
my($via, $msginfo, $initial_submission, $filter) = @_;
my($num_recips_undone) =
scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) }
@{$msginfo->per_recip_data});
while ($num_recips_undone > 0) {
mail_via_smtp_single(@_); my($num_recips_undone_after) =
scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) }
@{$msginfo->per_recip_data});
if ($num_recips_undone_after >= $num_recips_undone) {
do_log(-2, "TROUBLE: Number of recipients ($num_recips_undone_after) "
. "not reduced in SMTP transaction, abandon the effort");
last;
}
if ($num_recips_undone_after > 0) {
do_log(1, sprintf("Sent to %s recipients via SMTP, %s still to go",
$num_recips_undone - $num_recips_undone_after,
$num_recips_undone_after));
}
$num_recips_undone = $num_recips_undone_after;
}
1;
}
sub mail_via_smtp_single(@) {
my($via, $msginfo, $initial_submission, $filter) = @_;
my($which_section) = 'fwd_init';
snmp_count('OutMsgs');
local($1,$2,$3); $via =~ /^smtp: (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) /six
or die "Bad fwd method syntax: $via";
my($relayhost, $relayhost_port) = ($1.$2, $3);
my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
@{$msginfo->per_recip_data};
my($logmsg) = sprintf("%s via SMTP: [%s]:%s <%s>",
($initial_submission ? 'SEND' : 'FWD'),
$relayhost, $relayhost_port, $msginfo->sender);
if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 }
do_log(1, $logmsg . " -> " .
qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data));
my($msg) = $msginfo->mail_text; my($smtp_handle, $smtp_response); my($smtp_code, $smtp_msg, $received_cnt);
my($any_valid_recips) = 0; my($any_tempfail_recips) = 0;
my($any_valid_recips_and_data_sent) = 0; my($in_datasend_mode) = 0;
if (defined($msg) && !$msg->isa('MIME::Entity')) {
$msg = IO::Wrap::wraphandle($msg); $msg->seek(0,0) or die "Can't rewind mail file: $!";
}
my($remaining_time) = alarm(0); eval {
$which_section = 'fwd-connect';
my($localaddr) = c('local_client_bind_address'); my($heloname) = c('localhost_name'); $smtp_handle = Net::SMTP->new($relayhost, Port => $relayhost_port,
($localaddr eq '' ? () : (LocalAddr => $localaddr)),
($heloname eq '' ? () : (Hello => $heloname)),
ExactAddresses => 1,
Timeout => max(60, min(5 * 60, $remaining_time)), );
defined($smtp_handle)
or die "Can't connect to $relayhost port $relayhost_port, $!";
ll(5) && do_log(5,"Remote host presents itself as: ".$smtp_handle->domain);
section_time($which_section);
prolong_timer($which_section, $remaining_time); $remaining_time = undef;
$which_section = 'fwd-xforward';
if ($msginfo->client_addr ne '' && $smtp_handle->supports('XFORWARD')) {
my($cmd) = join(' ', 'XFORWARD', map
{ my($n,$v) = @$_;
$v =~ s/[^\041-\176]/?/g;
$v =~ s/[<>()\\";@]/?/g; $v = substr($v,0,255) if length($v) > 255; $v eq '' ? () : ("$n=$v") }
( ['ADDR', $msginfo->client_addr], ['NAME',$msginfo->client_name],
['PROTO',$msginfo->client_proto],['HELO',$msginfo->client_helo] ));
do_log(5, "sending $cmd");
$smtp_handle->command($cmd);
$smtp_handle->response()==2 or die "sending $cmd\n";
section_time($which_section); prolong_timer($which_section);
}
$which_section = 'fwd-auth';
my($auth_user) = $msginfo->auth_user;
my($mechanisms) = $smtp_handle->supports('AUTH');
if (!c('auth_required_out')) {
do_log(3,"AUTH not needed, user='$auth_user', MTA offers '$mechanisms'");
} elsif ($mechanisms eq '') {
do_log(3,"INFO: MTA does not offer AUTH capability, user='$auth_user'");
} elsif (!defined $auth_user) {
do_log(0,"INFO: AUTH needed for submission but AUTH data not available");
} else {
do_log(3,"INFO: authenticating $auth_user, server supports AUTH $mechanisms");
my($sasl) = Authen::SASL->new(
'callback' => { 'user' => $auth_user, 'authname' => $auth_user,
'pass' => $msginfo->auth_pass });
$smtp_handle->auth($sasl) or die "sending AUTH, user=$auth_user\n";
section_time($which_section); prolong_timer($which_section);
}
$which_section = 'fwd-mail-from';
$smtp_handle->mail(qquote_rfc2821_local($msginfo->sender))
or die "sending MAIL FROM\n";
section_time($which_section); prolong_timer($which_section);
$which_section = 'fwd-rcpt-to';
my($skipping_resp);
for my $r (@per_recip_data) { if (defined $skipping_resp) {
$r->recip_smtp_response($skipping_resp); $r->recip_done(2);
next;
}
$smtp_handle->recipient(qquote_rfc2821_local($r->recip_final_addr));
$smtp_code = $smtp_handle->code;
$smtp_msg = $smtp_handle->message;
chomp($smtp_msg);
my($rcpt_smtp_resp) = "$smtp_code $smtp_msg";
if ($smtp_code =~ /^2/) {
$any_valid_recips++;
} else { do_log(3, "response to RCPT TO: \"$rcpt_smtp_resp\"");
if ($rcpt_smtp_resp =~ /^0/) {
do_log(-1, "response to RCPT TO not yet available");
$rcpt_smtp_resp = "450 4.4.2 ($rcpt_smtp_resp - probably timed out)";
}
$r->recip_remote_mta($relayhost);
$r->recip_remote_mta_smtp_response($rcpt_smtp_resp);
if ($rcpt_smtp_resp =~ /^ (\d{3}) \s+ ([245] \. \d{1,3} \. \d{1,3})?
\s* (.*) \z/xs)
{
my($resp_code, $resp_enhcode, $resp_msg) = ($1, $2, $3);
if ($resp_enhcode eq '' && $resp_code =~ /^([245])/) {
my($c1) = $1;
$resp_enhcode = $resp_code eq '452' ?
"$c1.5.3" : "$c1.1.0"; $rcpt_smtp_resp = "$resp_code $resp_enhcode $smtp_msg";
}
}
if ($rcpt_smtp_resp =~ /^452/) { do_log(-1, sprintf('Only %d recips sent in one go: "%s"',
$any_valid_recips, $rcpt_smtp_resp));
$skipping_resp = $rcpt_smtp_resp;
} elsif ($rcpt_smtp_resp =~ /^4/) {
$any_tempfail_recips++;
$smtp_response = $rcpt_smtp_resp if !defined($smtp_response);
}
$r->recip_smtp_response($rcpt_smtp_resp); $r->recip_done(2);
$smtp_response = $rcpt_smtp_resp
if $rcpt_smtp_resp =~ /^5/ && $smtp_response !~ /^5/; }
}
section_time($which_section); prolong_timer($which_section);
$smtp_code = $smtp_msg = undef;
my($dsn_per_recip_capable) = 0;
if (!$any_valid_recips) {
do_log(-1,"mail_via_smtp: DATA skipped, no valid recips, $any_tempfail_recips");
} elsif ($any_tempfail_recips && !$dsn_per_recip_capable) {
do_log(-1,"mail_via_smtp: DATA skipped, tempfailed recips: $any_tempfail_recips");
} else { $which_section = 'fwd-data';
$smtp_handle->data or die "sending DATA command\n";
$in_datasend_mode = 1;
my($smtp_resp) = $smtp_handle->code . " " . $smtp_handle->message;
chomp($smtp_resp);
do_log(5, "response to DATA: \"$smtp_resp\"");
my($smtp_data_fh) = Amavis::Out->new_smtp_data($smtp_handle);
my($hdr_edits) = $msginfo->header_edits;
$hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
$received_cnt = $hdr_edits->write_header($msg, $smtp_data_fh);
if ($received_cnt > 100) {
die "Too many hops: $received_cnt 'Received:' header lines\n";
}
if (!defined($msg)) {
} elsif ($msg->isa('MIME::Entity')) {
$msg->print_body($smtp_data_fh);
} else {
while ($msg->read($_, 16384) > 0) {
$smtp_handle->datasend($_)
or die "datasend timed out while sending body\n";
}
}
section_time($which_section); prolong_timer($which_section);
$which_section = 'fwd-data-end';
$smtp_handle->dataend;
$in_datasend_mode = 0; $any_valid_recips_and_data_sent = 1;
section_time($which_section); prolong_timer($which_section);
$which_section = 'fwd-rundown-1';
$smtp_code = $smtp_handle->code;
my(@msgs) = $smtp_handle->message;
my($smtp_msg) = $msgs[$ $smtp_response = "$smtp_code $smtp_msg";
do_log(5, "response to data end: \"$smtp_response\"");
for my $r (@per_recip_data) {
next if $r->recip_done; $r->recip_remote_mta($relayhost);
$r->recip_remote_mta_smtp_response($smtp_response);
}
if ($smtp_code =~ /^[245]/) {
my($smtp_status) = substr($smtp_code, 0, 1);
$smtp_response = sprintf("%s %d.6.0 %s, id=%s, from MTA: %s",
$smtp_code, $smtp_status, ($smtp_status == 2 ? 'Ok' : 'Failed'),
am_id(), $smtp_response);
}
}
};
my($err) = $@;
my($saved_section_name) = $which_section;
if ($err ne '') { chomp($err); $err = ' ' if $err eq '' } prolong_timer($which_section, $remaining_time); $which_section = 'fwd-rundown';
if ($err ne '') { do_log(3, "mail_via_smtp: session failed: $err");
if (!defined($smtp_handle)) { $smtp_code = ''; $smtp_msg = '' }
else {
$smtp_code = $smtp_handle->code; $smtp_msg = $smtp_handle->message;
chomp($smtp_msg);
}
}
if (!defined $smtp_handle) {
} elsif ($in_datasend_mode) {
do_log(-1, "mail_via_smtp: NOTICE: aborting SMTP session, $err");
$smtp_handle->close; } else {
$smtp_handle->timeout(15); $smtp_handle->quit; if ($err eq '' && $smtp_handle->status != CMD_OK) {
do_log(-1,"WARN: sending SMTP QUIT command failed: "
. $smtp_handle->code . " " . $smtp_handle->message);
}
}
if ($err eq '') { if ($any_valid_recips_and_data_sent && $smtp_response !~ /^[245]/) {
$smtp_response =
sprintf("451 4.6.0 Bad SMTP code, id=%s, from MTA: \"%s\"",
am_id(), $smtp_response);
}
} elsif ($err eq "timed out" || $err =~ /: Timeout\z/) {
my($msg) = ($in_datasend_mode && $smtp_code =~ /^354/) ?
'' : ", $smtp_code $smtp_msg";
$smtp_response = sprintf("450 4.4.2 Timed out during %s%s, id=%s",
$saved_section_name, $msg, am_id());
} elsif ($err =~ /^Can't connect/) {
$smtp_response = sprintf("450 4.4.1 %s, id=%s", $err, am_id());
} elsif ($err =~ /^Too many hops/) {
$smtp_response = sprintf("550 5.4.6 Rejected: %s, id=%s", $err, am_id());
} elsif ($smtp_code =~ /^5/) { # 5xx
$smtp_response = sprintf("%s 5.5.0 Rejected by MTA: %s %s, id=%s",
($smtp_code !~ /^5\d\d\z/ ? "550" : $smtp_code),
$smtp_code, $smtp_msg, am_id());
} elsif ($smtp_code =~ /^0/) { # 000
$smtp_response = sprintf("450 4.4.2 No response during %s (%s): id=%s",
$saved_section_name, $err, am_id());
} else {
$smtp_response = sprintf("%s 4.5.0 from MTA during %s (%s): %s %s, id=%s",
($smtp_code !~ /^4\d\d\z/ ? "451" : $smtp_code),
$saved_section_name, $err, $smtp_code, $smtp_msg,
am_id());
}
do_log( ($smtp_response =~ /^2/ ? 3 : -1),
"mail_via_smtp: $smtp_response" ) if $smtp_response ne '';
if (defined $smtp_response) {
for my $r (@per_recip_data) {
if (!$r->recip_done) { # mark it as done
$r->recip_smtp_response($smtp_response); $r->recip_done(2);
} elsif ($any_valid_recips_and_data_sent
&& $r->recip_smtp_response =~ /^452/) {
# 'undo' the RCPT TO '452 Too many recipients' situation,
# needs to be handled in more than one transaction
$r->recip_smtp_response(undef); $r->recip_done(undef);
}
}
}
if ( $smtp_response =~ /^2/) { snmp_count('OutMsgsDelivers') }
elsif ($smtp_response =~ /^4/) { snmp_count('OutAttemptFails') }
elsif ($smtp_response =~ /^5/) { snmp_count('OutMsgsRejects') }
section_time($which_section);
1;
}
# Send mail using external mail submission program 'sendmail' (also available
# with Postfix and Exim) - used for forwarding original mail or sending notif.
# May throw exception (die) if temporary failure (4xx) or other problem
#
sub mail_via_pipe(@) {
my($via, $msginfo, $initial_submission, $filter) = @_;
snmp_count('OutMsgs');
$via =~ /^pipe:(.*)\z/si or die "Bad fwd method syntax: $via";
my($pipe_args) = $1;
$pipe_args =~ s/^flags=\S*\s*//i; # flags are currently ignored, q implied
$pipe_args =~ s/^argv=//i;
my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
@{$msginfo->per_recip_data};
my($logmsg) = sprintf("%s via PIPE: <%s>",
($initial_submission ? 'SEND' : 'FWD'), $msginfo->sender);
if (!@per_recip_data) {
do_log(5, "$logmsg, nothing to do");
return 1;
}
do_log(1, $logmsg . " -> " .
qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data));
my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object
if (defined($msg) && !$msg->isa('MIME::Entity')) {
# at this point, we have no idea what the user gave us...
# a globref? a FileHandle?
$msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
$msg->seek(0,0) or die "Can't rewind mail file: $!";
}
my(@pipe_args) = split(' ', $pipe_args); my(@command) = shift @pipe_args;
for (@pipe_args) {
# The sendmail command line expects addresses quoted as per RFC 822.
# "funny user"@some.domain
# For compatibility with Sendmail, the Postfix sendmail command line
# also accepts address formats that are legal in RFC 822 mail headers:
# Funny Dude <"funny user"@some.domain>
# Although addresses passed as args to sendmail initial submission
# should not be <...> bracketed, for some reason original sendmail
# issues a warning on null reverse-path, but gladly accepty <>.
# As this is not strictly wrong, we comply to make it happy.
if (/^\$\{sender\}\z/i) {
push(@command,
map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) }
$msginfo->sender);
} elsif (/^\$\{recipient\}\z/i) {
push(@command,
map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) }
map { $_->recip_final_addr } @per_recip_data);
} else {
push(@command, $_);
}
}
do_log(5, "mail_via_pipe running command: " . join(' ', @command));
local $SIG{CHLD} = 'DEFAULT';
local $SIG{PIPE} = 'IGNORE'; # write to broken pipe would throw a signal
my($mp,$pid) = run_command_consumer(undef,undef,@command);
binmode($mp) or die "Can't set pipe to binmode: $!"; # dflt since Perl 5.8.1
my($hdr_edits) = $msginfo->header_edits;
$hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
my($received_cnt) = $hdr_edits->write_header($msg, $mp);
if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
# deal with it later, for now just skip the body
} elsif (!defined($msg)) {
# empty mail body
} elsif ($msg->isa('MIME::Entity')) {
$msg->print_body($mp);
} else {
while ($msg->read($_, 16384) > 0)
{ $mp->print($_) or die "Submitting mail text failed: $!" }
}
my($smtp_response);
if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
do_log(-2, "Too many hops: $received_cnt 'Received:' header lines");
kill(15, $pid); # kill the process running mail submission program
$mp->close; # and ignore status
$smtp_response = "550 5.4.6 Rejected: " .
"Too many hops: $received_cnt 'Received:' header lines";
} else {
my($err); $mp->close or $err=$!; my($child_stat) = $?;
my($error_str) = exit_status_str($child_stat,$err);
my($status) = WEXITSTATUS($child_stat);
# sendmail program (Postfix variant) can return the following exit codes:
# EX_OK(0), EX_DATAERR, EX_SOFTWARE, EX_TEMPFAIL, EX_NOUSER, EX_UNAVAILABLE
if ($status == EX_OK) {
$smtp_response = "250 2.6.0 Ok"; # submitted to MTA
snmp_count('OutMsgsDelivers');
} elsif ($status == EX_TEMPFAIL) {
$smtp_response = "450 4.5.0 Temporary failure submitting message";
snmp_count('OutAttemptFails');
} elsif ($status == EX_NOUSER) {
$smtp_response = "550 5.1.1 Recipient unknown";
snmp_count('OutMsgsRejects');
} elsif ($status == EX_UNAVAILABLE) {
$smtp_response = "550 5.5.0 Mail submission service unavailable";
snmp_count('OutMsgsRejects');
} else {
$smtp_response = "451 4.5.0 Failed to submit a message: $error_str";
snmp_count('OutAttemptFails');
}
}
$smtp_response .= ", id=" . am_id();
for my $r (@per_recip_data) {
next if $r->recip_done;
$r->recip_smtp_response($smtp_response); $r->recip_done(2);
}
section_time('fwd-pipe');
1;
}
sub mail_via_bsmtp(@) {
my($via, $msginfo, $initial_submission, $filter) = @_;
snmp_count('OutMsgs'); local($1);
$via =~ /^bsmtp:(.*)\z/si or die "Bad fwd method: $via";
my($bsmtp_file_final) = $1; my($mbxname);
$bsmtp_file_final =~ s{%(.)}
{ $1 eq 'b' ? $msginfo->body_digest
: $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time))
: $1 eq 'n' ? am_id()
: $1 eq '%' ? '%' : '%'.$1 }egs;
$bsmtp_file_final = $QUARANTINEDIR."/".$bsmtp_file_final
if $bsmtp_file_final !~ m{^/}; # prepend directory if not specified
my($bsmtp_file_tmp) = $bsmtp_file_final . ".tmp";
my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
@{$msginfo->per_recip_data};
my($logmsg) = sprintf("%s via BSMTP: %s",
($initial_submission ? 'SEND' : 'FWD'),
qquote_rfc2821_local($msginfo->sender));
if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 }
do_log(1, $logmsg . " -> " .
qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data) .
", file " . $bsmtp_file_final);
my($msg) = $msginfo->mail_text; # a scalar reference, or a file handle
if (defined($msg) && !$msg->isa('MIME::Entity')) {
# at this point, we have no idea what the user gave us...
# a globref? a FileHandle?
$msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
$msg->seek(0,0) or die "Can't rewind mail file: $!";
}
my($mp) = IO::File->new;
eval {
$mp->open($bsmtp_file_tmp,'>',0640)
or die "Can't create BSMTP file $bsmtp_file_tmp: $!";
binmode($mp, ":bytes") or die "Can't set :bytes, $!" if $unicode_aware;
$mp->print("EHLO ", c('localhost_name'), $eol)
or die "print failed (EHLO): $!";
$mp->printf("MAIL FROM:%s BODY=8BITMIME%s", # avoid conversion to 7bit
qquote_rfc2821_local($msginfo->sender), $eol)
or die "print failed (MAIL FROM): $!";
for my $r (@per_recip_data) {
$mp->print("RCPT TO:", qquote_rfc2821_local($r->recip_final_addr), $eol)
or die "print failed (RCPT TO): $!";
}
$mp->print("DATA", $eol) or die "print failed (DATA): $!";
my($hdr_edits) = $msginfo->header_edits;
$hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits;
my($received_cnt) = $hdr_edits->write_header($msg,$mp);
if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
die "Too many hops: $received_cnt 'Received:' header lines";
} elsif (!defined($msg)) { # empty mail body
} elsif ($msg->isa('MIME::Entity')) {
$msg->print_body($mp);
} else {
while (<$msg>) {
$mp->print(/^\./ ? (".",$_) : $_) or die "print failed-data: $!";
}
}
$mp->print(".", $eol) or die "print failed (final dot): $!";
# $mp->print("QUIT",$eol) or die "print failed (QUIT): $!";
$mp->close or die "Can't close BSMTP file $bsmtp_file_tmp: $!";
$mp = undef;
rename($bsmtp_file_tmp, $bsmtp_file_final)
or die "Can't rename BSMTP file to $bsmtp_file_final: $!";
$mbxname = $bsmtp_file_final;
};
my($err) = $@; my($smtp_response);
if ($err eq '') {
$smtp_response = "250 2.6.0 Ok, queued as BSMTP $bsmtp_file_final";
snmp_count('OutMsgsDelivers');
} else {
chomp($err);
unlink($bsmtp_file_tmp)
or do_log(-2,"Can't delete half-finished BSMTP file $bsmtp_file_tmp: $!");
$mp->close if defined $mp; # ignore status
if ($err =~ /too many hops/i) {
$smtp_response = "550 5.4.6 Rejected: $err";
snmp_count('OutMsgsRejects');
} else {
$smtp_response = "451 4.5.0 Writing $bsmtp_file_tmp failed: $err";
snmp_count('OutAttemptFails');
}
}
$smtp_response .= ", id=" . am_id();
for my $r (@per_recip_data) {
next if $r->recip_done;
$r->recip_smtp_response($smtp_response);
$r->recip_done(2);
$r->recip_mbxname($mbxname) if defined $mbxname;
}
section_time('fwd-bsmtp');
1;
}
1;
#
package Amavis::UnmangleSender;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = ();
@EXPORT_OK = qw(&best_try_originator_ip &best_try_originator
&first_received_from);
}
use subs @EXPORT_OK;
BEGIN {
import Amavis::Conf qw(:platform @viruses_that_fake_sender_maps);
import Amavis::Util qw(ll do_log);
import Amavis::rfc2821_2822_Tools qw(
split_address parse_received fish_out_ip_from_received);
import Amavis::Lookup qw(lookup lookup_ip_acl);
}
use Mail::Address;
# Returns the envelope sender address, or reconstructs it if there is
# a good reason to believe the envelope address has been changed or forged,
# as is common for some varieties of viruses. Returns best guess of the
# sender address, or undef if it can not be determined.
#
sub unmangle_sender($$$) {
my $sender = shift; # rfc2821 envelope sender address
my $from = shift; # rfc2822 'From:' header, may include comment
my $virusname_list = shift; # list ref containing names of detected viruses
# based on ideas from Furio Ercolessi, Mike Atkinson, Mark Martinec
my($best_try_originator) = $sender;
my($localpart,$domain) = split_address($sender);
# extract the RFC2822 'from' address, ignoring phrase and comment
chomp($from);
{
local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted !
$from = (Mail::Address->parse($from))[0];
}
$from = $from->address if $from ne '';
# NOTE: rfc2822 allows multiple addresses in the From field!
if (grep { /magistr/i } @$virusname_list) {
for my $j (0..2) { # assemble possible `shifted' candidates
next if $j >= length($localpart);
my($try) = $sender;
substr($try, $j, 1) = chr(ord(substr($try, $j, 1)) - 1);
if (lc($from) eq lc($try)) { $best_try_originator = $try; last }
}
}
if (grep { /badtrans/i } @$virusname_list) {
if ($from =~ /^ (joanna\@mail\.utexas\.edu | powerpuff\@videotron\.ca |
(mary\@c-com | support\@cyberramp | admin\@gte |
administrator\@border) \.net |
(monika\@telia | jessica\@aol | spiderroll\@hotmail |
lgonzal\@hotmail | andy\@hweb-media | Gravity49\@aol |
tina0828\@yahoo | JUJUB271\@AOL | aizzo\@home) \.com
) \z/xi )
{ $best_try_originator = undef;
} else {
$best_try_originator = $1 if $from=~/^_(.+)\z/s && lc($sender) ne lc($1);
}
}
for my $vn (@$virusname_list) {
my($result,$matching_key) = lookup(0,$vn,@viruses_that_fake_sender_maps);
if ($result) {
do_log(2, "Virus $vn matches $matching_key, sender addr ignored");
$best_try_originator = undef;
last;
}
}
$best_try_originator;
}
sub ip_addr_to_name($) {
my($addr) = @_; local($1,$2,$3,$4); my($result);
if ($addr !~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
$result = $addr; } else {
my($binaddr) = pack('C4', $1,$2,$3,$4); do_log(5, "ip_addr_to_name: DNS reverse-resolving: $addr");
my(@addr) = gethostbyaddr($binaddr,2); $result = '['.$addr.']'; if (@addr) {
my($name,$aliases,$addrtype,$length,@addrs) = @addr;
if ($name =~ /[^.]\.[a-zA-Z]+\z/s) {
do_log(5, "ip_addr_to_name: DNS forward-resolving: $name");
my(@raddr) = gethostbyname($name); my($rname,$raliases,$raddrtype,$rlength,@raddrs) = @raddr;
for my $ra (@raddrs) {
if (lc($ra) eq lc($binaddr)) { $result = $name; last }
}
}
}
}
do_log(3, "ip_addr_to_name: returning: $result");
$result;
}
sub first_received_from($) {
my($entity) = shift;
my($first_received);
if (defined($entity)) {
my($fields) = parse_received($entity->head->get('received', -1));
if (exists $fields->{'from'}) {
my($item, $v1, $v2, $v3, $comment) = @{$fields->{'from'}};
$first_received = join(' ', $item, $comment);
$first_received =~ s/^[ \t\n\r]+//s; # discard leading whitespace
$first_received =~ s/[ \t\n\r]+\z//s; # discard trailing whitespace
}
do_log(5, "first_received_from: $first_received");
}
$first_received;
}
sub best_try_originator_ip($) {
my($entity) = @_;
my($first_received_from_ip);
if (defined($entity)) {
my(@publicnetworks) = qw(
!0.0.0.0/8 !127.0.0.0/8 !172.16.0.0/12 !192.168.0.0/16 !10.0.0.0/8
!169.254.0.0/16 !192.0.2.0/24 !192.88.99.0/24 !224.0.0.0/4
::FFFF:0:0/96
!:: !::1 !FF00::/8 !FE80::/10 !FEC0::/10
::/0 ); my(@received) = reverse $entity->head->get('received');
$ for my $r (@received) {
$first_received_from_ip = fish_out_ip_from_received($r);
last if $first_received_from_ip ne '' &&
eval { lookup_ip_acl($first_received_from_ip,\@publicnetworks) };
}
do_log(5, "best_try_originator_ip: $first_received_from_ip");
}
$first_received_from_ip;
}
sub best_try_originator($$$) {
my($sender, $entity, $virusname_list) = @_;
return ($sender,$sender) if !defined($entity); my($originator) =
unmangle_sender($sender, $entity->head->get('from',0), $virusname_list);
return ($originator, $originator) if defined $originator;
my($first_received_from_ip) = best_try_originator_ip($entity);
$originator = '?@' . ip_addr_to_name($first_received_from_ip)
if $first_received_from_ip ne '';
(undef, $originator);
}
1;
package Amavis::Unpackers::NewFilename;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&consumed_bytes);
}
BEGIN {
import Amavis::Conf qw(c cr ca
$MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR
$MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR);
import Amavis::Util qw(ll do_log min max);
}
use vars qw($avail_quota); use vars qw($rem_quota);
sub new($;$$) { my($class, $maxfiles,$mail_size) = @_;
$avail_quota = $rem_quota = max($MIN_EXPANSION_QUOTA, $mail_size * $MIN_EXPANSION_FACTOR,
min($MAX_EXPANSION_QUOTA, $mail_size * $MAX_EXPANSION_FACTOR));
do_log(4,"Original mail size: $mail_size; quota set to: $avail_quota bytes");
bless {
num_of_issued_names => 0, first_issued_ind => 1, last_issued_ind => 0,
maxfiles => $maxfiles, objlist => [],
}, $class;
}
sub parts_list_reset($) { my($self) = shift;
$self->{num_of_issued_names} = 0;
$self->{first_issued_ind} = $self->{last_issued_ind} + 1;
$self->{objlist} = [];
}
sub parts_list($) { my($self) = shift;
$self->{objlist};
}
sub parts_list_add($$) { my($self, $part) = @_;
push(@{$self->{objlist}}, $part);
}
sub generate_new_num($) { my($self) = @_;
if (defined($self->{maxfiles}) &&
$self->{num_of_issued_names} >= $self->{maxfiles}) {
die "Maximum number of files ($self->{maxfiles}) exceeded";
}
$self->{num_of_issued_names}++; $self->{last_issued_ind}++;
$self->{last_issued_ind};
}
sub consumed_bytes($$;$$) {
my($bytes, $bywhom, $tentatively, $exquota) = @_;
my($perc) = !$avail_quota ? '' : sprintf(", (%.0f%%)",
100 * ($avail_quota - ($rem_quota - $bytes)) / $avail_quota);
do_log(4,"Charging $bytes bytes to remaining quota $rem_quota"
. " (out of $avail_quota$perc) - by $bywhom");
if ($bytes > $rem_quota && $rem_quota >= 0) {
my($msg) = "Exceeded storage quota $avail_quota bytes by $bywhom; ".
"last chunk $bytes bytes";
do_log(-1, $msg);
die "$msg\n" if !$exquota;
}
$rem_quota -= $bytes unless $tentatively;
$rem_quota; }
1;
package Amavis::Unpackers::Part;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
BEGIN {
import Amavis::Util qw(ll do_log);
}
use vars qw($file_generator_object);
sub init($) { $file_generator_object = shift }
sub new($;$$) { my($class, $dir_name,$parent) = @_;
my($self) = bless {}, $class;
if (!defined($dir_name) && !defined($parent)) {
} else {
$self->number($file_generator_object->generate_new_num);
$self->dir_name($dir_name) if defined $dir_name;
if (defined $parent) {
$self->parent($parent);
my($ch_ref) = $parent->children;
push(@$ch_ref,$self); $parent->children($ch_ref);
}
$file_generator_object->parts_list_add($self); ll(4) && do_log(4, "Issued a new " .
(defined $dir_name ? "file name" : "pseudo part") . ": " .
$self->base_name);
}
$self;
}
sub number
{ my($self)=shift; !@_ ? $self->{number} : ($self->{number}=shift) };
sub dir_name
{ my($self)=shift; !@_ ? $self->{dir_name} : ($self->{dir_name}=shift) };
sub parent
{ my($self)=shift; !@_ ? $self->{parent} : ($self->{parent}=shift) };
sub children
{ my($self)=shift; !@_ ? $self->{children}||[] : ($self->{children}=shift) };
sub mime_placement { my($self)=shift; !@_ ? $self->{place} : ($self->{place}=shift) };
sub type_short { my($self)=shift; !@_ ? $self->{ty_short} : ($self->{ty_short}=shift) };
sub type_long
{ my($self)=shift; !@_ ? $self->{ty_long} : ($self->{ty_long}=shift) };
sub type_declared
{ my($self)=shift; !@_ ? $self->{ty_decl} : ($self->{ty_decl}=shift) };
sub name_declared { my($self)=shift; !@_ ? $self->{nm_decl} : ($self->{nm_decl}=shift) };
sub size
{ my($self)=shift; !@_ ? $self->{size} : ($self->{size}=shift) };
sub exists
{ my($self)=shift; !@_ ? $self->{exists} : ($self->{exists}=shift) };
sub attributes { my($self)=shift; !@_ ? $self->{attr} : ($self->{attr}=shift) };
sub attributes_add { my($self)=shift; my($a) = $self->{attr} || [];
for my $arg (@_) { push(@$a,$arg) if $arg ne '' && !grep {$_ eq $arg} @$a }
$self->{attr} = $a;
};
sub base_name { my($self)=shift; sprintf("p%03d",$self->number) }
sub full_name {
my($self)=shift; my($d) = $self->dir_name;
!defined($d) ? undef : $d.'/'.$self->base_name;
}
sub path {
my($self)=shift;
my(@path);
for (my($p)=$self; defined($p); $p=$p->parent) { unshift(@path,$p) }
\@path;
};
1;
package Amavis::Unpackers::OurFiler;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter MIME::Parser::Filer); %EXPORT_TAGS = ();
@EXPORT = ();
@EXPORT_OK = ();
}
sub new($$$) {
my($class, $dir, $parent_obj) = @_;
$dir =~ s{/+\z}{}; bless {parent => $parent_obj, directory => $dir}, $class;
}
sub output_path($@) {
my($self, $head) = @_;
my($newpart_obj) =
Amavis::Unpackers::Part->new($self->{directory}, $self->{parent});
get_amavisd_part($head, $newpart_obj); $newpart_obj->full_name;
}
sub get_amavisd_part($;$) {
my($head) = shift;
!@_ ? $head->{amavisd_parts_obj} : ($head->{amavisd_parts_obj} = shift);
}
1;
package Amavis::Unpackers::Validity;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = ();
@EXPORT_OK = qw(&check_header_validity &check_for_banned_names);
}
BEGIN {
import Amavis::Util qw(ll do_log sanitize_str);
import Amavis::Conf qw(:platform c cr ca);
import Amavis::Lookup qw(lookup);
}
use subs @EXPORT_OK;
sub check_header_validity($$) {
my($conn, $msginfo) = @_;
my(@bad);
my($curr_head);
for my $next_head (@{$msginfo->orig_header}, "\n") {
if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head } else { if (!defined($curr_head)) { } else {
my($field_name, $field_body) =
$curr_head =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s
? ($1, $2) : (undef, $curr_head);
my($msg1,$msg2);
if (!defined($field_name) && $curr_head=~/^()()(.*)\z/s) {
$msg1 = "Invalid header field head";
} elsif ($curr_head =~ /^(.*?)([\000\015])(.*)\z/s) {
$msg1 = "Improper use of control character";
} elsif ($curr_head =~ /^(.*?)([\200-\377])(.*)\z/s) {
$msg1 = "Non-encoded 8-bit data";
} elsif ($curr_head =~ /^(.*?)([^\000-\377])(.*)\z/s) {
$msg1 = "Non-encoded Unicode character";
} elsif ($curr_head =~ /^()()([ \t]+)$/m) {
$msg1 ="Improper folded header field made up entirely of whitespace";
}
if (defined $msg1) {
my($pre, $ch, $post) = ($1, $2, $3);
if (length($post) > 20) { $post = substr($post,0,15) . "..." }
if (length($pre)-length($field_name)-2 > 50-length($post)) {
$pre = "$field_name: ..."
. substr($pre, length($pre) - (45-length($post)));
}
$msg1 .= sprintf(" (char %02X hex)", ord($ch)) if length($ch)==1;
$msg1 .= " in message header '$field_name'" if $field_name ne '';
$msg2 = sanitize_str($pre); my($msg2_pre_l) = length($msg2);
$msg2 .= sanitize_str($ch . $post);
push(@bad, "$msg1: $msg2");
}
}
last if $next_head eq $eol; $curr_head = $next_head;
}
}
@bad;
}
sub check_for_banned_names($) {
my($parts_root) = @_;
do_log(3, "Checking for banned types and filenames");
my(@banned_part_descr,@banned_matching_keys,@banned_rhs); my($part);
my($bfnmr) = ca('banned_filename_maps'); for (my(@unvisited)=($parts_root);
@unvisited and $part=shift(@unvisited);
push(@unvisited,@{$part->children}))
{ my(@path) = @{$part->path};
next if @path <= 1;
shift(@path); next if @{$part->children}; my(@descr); my($found,$key_val,$key_what,$result,$matchingkey);
for my $p (@path) {
my(@k,$n);
$n = $p->base_name;
if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"P=$n") }
$n = $p->mime_placement;
if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"L=$n") }
$n = $p->type_declared;
$n = [$n] if !ref($n);
for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"M=$m")} }
$n = $p->type_short;
$n = [$n] if !ref($n);
for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"T=$m")} }
$n = $p->name_declared;
$n = [$n] if !ref($n);
for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"N=$m")} }
$n = $p->attributes;
$n = [$n] if !ref($n);
for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"A=$m")} }
push(@descr, join("\t",@k));
if (!$found && @$bfnmr) { for my $k (@k) {
$k =~ /^([a-zA-Z0-9])=(.*)\z/s;
($key_what,$key_val) = ($1,$2);
next unless $key_what =~ /^[TMNA]\z/;
if ($key_what eq 'T') {
$key_val = '.' . $key_val; } elsif ($key_what eq 'A') {
if ($key_val eq 'U') { $key_val = 'UNDECIPHERABLE' } else { next }
}
do_log(4, sprintf("check_for_banned (%s) %s=%s",
$p->base_name, $key_what, $key_val));
($result,$matchingkey) = lookup(0,$key_val,@$bfnmr);
$found++ if defined $result;
last if $found;
}
}
}
my($key_val_str) = join(' | ',@descr); $key_val_str =~ s/\t/,/g;
if (!$found) { ($result,$matchingkey) =
lookup(0,join("\n",@descr),
Amavis::Lookup::Label->new('banned_namepath_re'),
cr('banned_namepath_re'));
$found++ if defined $result;
}
my($ll) = $result ? 1 : 3;
if (ll($ll)) { my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
e => "\e", a => "\a", t => "\t");
my($mk) = $matchingkey; $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : '\\'.$1 }egsx;
do_log($ll, sprintf('p.path%s: "%s"%s',
!$result?'':" BANNED:$result", $key_val_str,
!defined $result ? '' : ", matching_key=\"$mk\""));
}
if ($result) {
push(@banned_part_descr, $key_val_str);
push(@banned_matching_keys, $matchingkey);
push(@banned_rhs, $result);
}
}
(\@banned_part_descr, \@banned_matching_keys, \@banned_rhs);
}
1;
package Amavis::Unpackers::MIME;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = ();
@EXPORT_OK = qw(&mime_decode);
}
use Errno qw(ENOENT);
use MIME::Parser;
use MIME::Words;
BEGIN {
import Amavis::Conf qw(:platform c cr ca);
import Amavis::Timing qw(section_time);
import Amavis::Util qw(snmp_count ll do_log);
import Amavis::Unpackers::NewFilename qw(consumed_bytes);
}
use subs @EXPORT_OK;
sub mime_decode_pre_epi($$$$$) {
my($pe_name, $pe_lines, $tempdir, $parent_obj, $placement) = @_;
if (defined $pe_lines && @$pe_lines) {
do_log(5, "mime_decode_$pe_name: " . scalar(@$pe_lines) . " lines");
if (@$pe_lines > 5 || "@$pe_lines" !~ m{^[a-zA-Z0-9/\@:;,. \t\n_-]*\z}s) {
my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",
$parent_obj);
$newpart_obj->mime_placement($placement);
$newpart_obj->name_declared($pe_name);
my($newpart) = $newpart_obj->full_name;
my($outpart) = IO::File->new;
$outpart->open($newpart,'>')
or die "Can't create $pe_name file $newpart: $!";
binmode($outpart, ":bytes") or die "Can't cancel :utf8 mode: $!"
if $unicode_aware;
my($len);
for (@$pe_lines) {
$outpart->print($_) or die "Can't write $pe_name to $newpart: $!";
$len += length($_);
}
$outpart->close or die "Can't close $pe_name $newpart: $!";
$newpart_obj->size($len);
consumed_bytes($len, "mime_decode_$pe_name", 0, 1);
}
}
}
sub mime_traverse($$$$$) {
my($entity, $tempdir, $parent_obj, $depth, $placement) = @_;
mime_decode_pre_epi('preamble', $entity->preamble,
$tempdir, $parent_obj, $placement);
my($mt, $et) = ($entity->mime_type, $entity->effective_type);
my($part); my($head) = $entity->head; my($body) = $entity->bodyhandle;
if (!defined($body)) { $part = Amavis::Unpackers::Part->new(undef,$parent_obj);
do_log(2, $part->base_name." $placement Content-Type: $mt");
} else { my($fn) = $body->path; my($size);
if (!defined($fn)) { $size = length($body->as_string) }
else {
my($msg); my($errn) = lstat($fn) ? 0 : 0+$!;
if ($errn == ENOENT) { $msg = "does not exist" }
elsif ($errn) { $msg = "is inaccessible: $!" }
elsif (!-r _) { $msg = "is not readable" }
elsif (!-f _) { $msg = "is not a regular file" }
else {
$size = -s _;
do_log(4,"mime_traverse: file $fn is empty") if !$size;
}
do_log(-1,"WARN: mime_traverse: file $fn $msg") if defined $msg;
}
consumed_bytes($size, 'mime_decode', 0, 1);
$part = Amavis::Unpackers::OurFiler::get_amavisd_part($head);
if (defined $part) {
$part->size($size);
if ($size==0) { $part->type_short('empty'); $part->type_long('empty') }
do_log(2, $part->base_name." $placement Content-Type: $mt" .
", size: $size B, name: ".$entity->head->recommended_filename);
my($old_parent_obj) = $part->parent;
if ($parent_obj ne $old_parent_obj) { ll(5) && do_log(5,sprintf("reparenting %s from %s to %s",
$part->base_name,
$old_parent_obj->base_name, $parent_obj->base_name));
my($ch_ref) = $old_parent_obj->children;
$old_parent_obj->children([grep {$_ ne $part} @$ch_ref]);
$ch_ref = $parent_obj->children;
push(@$ch_ref,$part); $parent_obj->children($ch_ref);
$part->parent($parent_obj);
}
}
}
if (defined $part) {
$part->mime_placement($placement);
$part->type_declared($mt eq $et ? $mt : [$mt, $et]);
my(@rn); my($val, $val_decoded);
$val = $head->mime_attr('content-disposition.filename');
if ($val ne '') {
push(@rn, $val);
$val_decoded = MIME::Words::decode_mimewords($val);
push(@rn, $val_decoded) if $val_decoded ne $val;
}
$val = $head->mime_attr('content-type.name');
if ($val ne '') {
$val_decoded = MIME::Words::decode_mimewords($val);
push(@rn, $val_decoded) if !grep { $_ eq $val_decoded } @rn;
push(@rn, $val) if !grep { $_ eq $val } @rn;
}
$part->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
}
mime_decode_pre_epi('epilogue', $entity->epilogue,
$tempdir, $parent_obj, $placement);
my($item_num) = 0;
for my $e ($entity->parts) { $item_num++;
mime_traverse($e,$tempdir,$part,$depth+1,"$placement/$item_num");
}
}
sub mime_decode($$$) {
my($fileh, $tempdir, $parent_obj) = @_;
my($parser) = MIME::Parser->new;
$parser->filer(Amavis::Unpackers::OurFiler->new("$tempdir/parts",
$parent_obj));
$parser->ignore_errors(1); $parser->extract_nested_messages("NEST"); $parser->extract_uuencode(1);
my($entity);
snmp_count('OpsDecByMimeParser');
if (ref($fileh)) { do_log(4, "Extracting mime components");
$fileh->seek(0,0) or die "Can't rewind mail file: $!";
local($1,$2,$3,$4); $entity = $parser->parse($fileh);
} else { do_log(4, "Extracting mime components from $fileh");
local($1,$2,$3,$4); $entity = $parser->parse_open("$tempdir/parts/$fileh");
}
my($mime_err) = $parser->results->errors;
$mime_err=~s/\s+\z//; $mime_err=~s/[ \t\r]*\n+/; /g; $mime_err=~s/\s+/ /g;
$mime_err = substr($mime_err,0,250) . '...' if length($mime_err) > 250;
do_log(1, "WARN: MIME::Parser $mime_err") if $mime_err ne '';
mime_traverse($entity, $tempdir, $parent_obj, 0, '1');
section_time('mime_decode');
($entity, $mime_err);
}
1;
package Amavis::Notify;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = ();
@EXPORT_OK = qw(&delivery_status_notification &delivery_short_report
&string_to_mime_entity &defanged_mime_entity);
}
BEGIN {
import Amavis::Util qw(ll do_log am_id safe_encode q_encode);
import Amavis::Timing qw(section_time);
import Amavis::Conf qw(:platform $myhostname c cr ca);
import Amavis::Lookup qw(lookup);
import Amavis::Expand qw(expand);
import Amavis::rfc2821_2822_Tools;
}
use MIME::Entity;
use subs @EXPORT_OK;
sub string_to_mime_entity($) {
my($mail_as_string_ref) = @_;
local($1,$2,$3); my($entity); my($m_hdr,$m_body);
($m_hdr, $m_body) = ($1, $3)
if $$mail_as_string_ref =~ /^(.*?\r?\n)(\r?\n|\z)(.*)\z/s;
$m_body = safe_encode(c('bdy_encoding'), $m_body);
my($nxmh) = c('notify_xmailer_header');
eval {$entity = MIME::Entity->build(
Type => 'text/plain', Encoding => '-SUGGEST', Charset=> c('bdy_encoding'),
(defined $nxmh && $nxmh eq '' ? () : ('X-Mailer' => $nxmh) ), Data => $m_body); 1} or do {chomp($@); die $@};
my($head) = $entity->head;
$m_hdr =~ s/\r?\n([ \t])/$1/g; for my $hdr_line (split(/\r?\n/, $m_hdr)) {
if ($hdr_line =~ /^([^:]*):\s*(.*)\z/s) {
my($fhead, $fbody) = ($1, $2);
if ($fhead =~ /^(X-.*|Subject|Comments)\z/si &&
$fbody =~ /[^\011\012\040-\176]/) { my($fbody_octets) = $fbody; if ($unicode_aware && Encode::is_utf8($fbody)) {
$fbody_octets = safe_encode(c('hdr_encoding'), $fbody);
do_log(5, "string_to_mime_entity UTF-8 body: $fbody");
do_log(5, "string_to_mime_entity body octets: $fbody_octets");
}
my($qb) = c('hdr_encoding_qb');
if (uc($qb) eq 'Q') {
$fbody = q_encode($fbody_octets, $qb, c('hdr_encoding'));
} else {
$fbody = MIME::Words::encode_mimeword($fbody_octets,
$qb, c('hdr_encoding'));
}
} else { $fbody = safe_encode('ascii', $fbody);
}
$fhead = safe_encode('ascii', $fhead);
do_log(5, sprintf("string_to_mime_entity %s: %s", $fhead, $fbody));
if (!eval { $head->replace($fhead, $fbody); 1 }) {
chomp($@);
die sprintf("%s header field '%s: %s'",
($@ eq '' ? "invalid" : "$@, "), $fhead, $fbody);
}
}
}
$entity; }
sub delivery_status_notification($$$$$) {
my($conn,$msginfo,$report_success_dsn_also,$builtins_ref,$template_ref) = @_;
my($dsn_time) = time; my($notification);
if ($msginfo->sender eq '') { do_log(4, "Not sending DSN to empty return path");
} else {
my($from_mta, $client_ip) = ($conn->smtp_helo, $conn->client_ip);
my($msg) = ''; $msg .= "Reporting-MTA: dns; $myhostname\n";
$msg .= "Received-From-MTA: smtp; $from_mta ([$client_ip])\n"
if $from_mta ne '';
$msg .= "Arrival-Date: " . rfc2822_timestamp($msginfo->rx_time) . "\n";
my($any); for my $r (@{$msginfo->per_recip_data}) {
my($remote_mta) = $r->recip_remote_mta;
my($smtp_resp) = $r->recip_smtp_response;
if (!$r->recip_done) {
if ($msginfo->delivery_method eq '') { $smtp_resp = "250 2.5.0 Ok, continue delivery";
} else {
do_log(-2,"TROUBLE: recipient not done: <"
. $r->recip_addr . "> " . $smtp_resp);
}
}
my($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg);
if ($smtp_resp =~ /^ (\d{3}) \s+ ([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;
}
my($smtp_resp_class) = $smtp_resp_code =~ /^(\d)/ ? $1 : '0';
if ($smtp_resp_enhcode eq '' && $smtp_resp_class =~ /^([245])\z/) {
$smtp_resp_enhcode = "$1.0.0";
}
next unless $smtp_resp_class ne '2' || $report_success_dsn_also;
$any++;
$msg .= "\n"; if ($remote_mta ne '' && $r->recip_final_addr ne $r->recip_addr) {
$msg .= "X-NextToLast-Final-Recipient: rfc822; "
. quote_rfc2821_local($r->recip_addr) . "\n";
$msg .= "Final-Recipient: rfc822; "
. quote_rfc2821_local($r->recip_final_addr) . "\n";
} else {
$msg .= "Final-Recipient: rfc822; "
. quote_rfc2821_local($r->recip_addr) . "\n";
}
$msg .= "Action: ".($smtp_resp_class eq '2' ? 'delivered':'failed')."\n";
$msg .= "Status: $smtp_resp_enhcode\n";
my($rem_smtp_resp) = $r->recip_remote_mta_smtp_response;
if ($remote_mta eq '' || $rem_smtp_resp eq '') {
$msg .= "Diagnostic-Code: smtp; $smtp_resp\n";
} else {
$msg .= "Remote-MTA: dns; $remote_mta\n";
$msg .= "Diagnostic-Code: smtp; $rem_smtp_resp\n";
}
$msg .= "Last-Attempt-Date: " . rfc2822_timestamp($dsn_time) . "\n";
}
return $notification if !$any;
my($to_hdr) = qquote_rfc2821_local($msginfo->sender_contact);
my(%mybuiltins) = %$builtins_ref; $mybuiltins{'f'} = c('hdrfrom_notify_sender');
$mybuiltins{'T'} = $to_hdr;
$mybuiltins{'d'} = rfc2822_timestamp($dsn_time);
my($dsn) = expand($template_ref, \%mybuiltins);
my($dsn_entity) = string_to_mime_entity($dsn);
$dsn_entity->make_multipart;
my($head) = $dsn_entity->head;
eval { $head->replace('From', c('hdrfrom_notify_sender')); 1 }
or do { chomp($@); die $@ };
eval { $head->replace('To', $to_hdr); 1 } or do { chomp($@); die $@ };
eval { $head->replace('Date', rfc2822_timestamp($dsn_time)); 1 }
or do { chomp($@); die $@ };
my($field) = Mail::Field->new('Content_type'); $field->type("multipart/report; report-type=delivery-status");
$field->boundary(MIME::Entity::make_boundary());
$head->replace('Content-type', $field->stringify);
$head = undef;
eval {$dsn_entity->attach(
Type => 'message/delivery-status', Encoding => '7bit',
Description => 'Delivery error report',
Data => $msg); 1} or do {chomp($@); die $@};
eval {$dsn_entity->attach(
Type => 'text/rfc822-headers', Encoding => '-SUGGEST',
Description => 'Undelivered-message headers',
Data => $msginfo->orig_header); 1} or do {chomp($@); die $@};
$notification = Amavis::In::Message->new;
$notification->rx_time($dsn_time);
$notification->delivery_method(c('notify_method'));
$notification->sender(c('mailfrom_notify_sender')); $notification->auth_submitter('<>');
$notification->auth_user(c('amavis_auth_user'));
$notification->auth_pass(c('amavis_auth_pass'));
$notification->recips([$msginfo->sender_contact]);
$notification->mail_text($dsn_entity);
}
$notification;
}
sub delivery_short_report($) {
my($msginfo) = @_;
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,
($remote_mta eq ''?'':" $remote_mta said:"), $smtp_resp));
}
}
(\@succ_recips, \@failed_recips, \@failed_recips_full);
}
sub defanged_mime_entity($$$) {
my($conn,$msginfo,$first_part) = @_;
my($new_entity);
my($resent_time) = $msginfo->rx_time;
$first_part = safe_encode(c('bdy_encoding'), $first_part);
my($nxmh) = c('notify_xmailer_header');
eval {$new_entity = MIME::Entity->build(
Type => 'multipart/mixed',
(defined $nxmh && $nxmh eq '' ? () : ('X-Mailer' => $nxmh) ), ); 1} or do {chomp($@); die $@};
my($head) = $new_entity->head;
my($orig_head) = $msginfo->mime_entity->head;
for my $field_head ( 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 X-Mailer) ) {
for my $value ($orig_head->get_all($field_head)) {
do_log(4, "copying-over the header field: $field_head");
eval { $head->add($field_head, $value); 1 } or do {chomp($@); die $@};
}
}
$head = undef; eval {$new_entity->attach(
Type => 'text/plain', Encoding => '-SUGGEST', Charset => c('bdy_encoding'),
Data => $first_part); 1} or do {chomp($@); die $@};
eval {$new_entity->attach( Type => 'message/rfc822; x-spam-type=original',
Encoding => '8bit', Path => $msginfo->mail_text_fn,
Description => 'Original message',
Filename => 'message.txt', Disposition => 'attachment'); 1}
or do {chomp($@); die $@};
$new_entity;
}
1;
package Amavis::Cache;
use strict;
use re 'taint';
BEGIN {
import Amavis::Util qw(ll do_log);
}
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.0331';
@ISA = qw(Exporter);
}
sub new { my($class) = @_;
do_log(5,"BerkeleyDB not available, using memory-based local cache");
bless {}, $class;
}
sub get { my($self,$key) = @_; thaw($self->{$key}) }
sub set { my($self,$key,$obj) = @_; $self->{$key} = freeze($obj) }
sub encode($) {
my($str) = @_; $str =~ s/[%~\000\200]/sprintf("%%%02X",ord($&))/egs; $str;
}
sub freeze($); sub freeze($) {
my($obj) = @_; my($ty) = ref($obj);
if (!defined($obj)) { 'U' }
elsif (!$ty) { join('~', '', encode($obj)) } elsif ($ty eq 'SCALAR') { join('~', 'S', encode(freeze($$obj))) }
elsif ($ty eq 'REF') { join('~', 'R', encode(freeze($$obj))) }
elsif ($ty eq 'ARRAY') { join('~', 'A', map {encode(freeze($_))} @$obj) }
elsif ($ty eq 'HASH') {
join('~','H',map {(encode($_),encode(freeze($obj->{$_})))} sort keys %$obj)
} else { die "Can't freeze object type $ty" }
}
sub thaw($); sub thaw($) {
my($str) = @_;
return undef if !defined $str;
my($ty,@val) = split(/~/,$str,-1);
for (@val) { s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg }
if ($ty eq 'U') { undef }
elsif ($ty eq '') { $val[0] }
elsif ($ty eq 'S') { my($obj)=thaw($val[0]); \$obj }
elsif ($ty eq 'R') { my($obj)=thaw($val[0]); \$obj }
elsif ($ty eq 'A') { [map {thaw($_)} @val] }
elsif ($ty eq 'H') {
my($hr) = {};
while (@val) { my($k) = shift @val; $hr->{$k} = thaw(shift @val) }
$hr;
} else { die "Can't thaw object type $ty" }
}
1;
package Amavis;
require 5.005; use strict;
use re 'taint';
use POSIX qw(strftime);
use Errno qw(ENOENT);
use IO::File ();
use Digest::MD5;
use Net::Server 0.83;
use Net::Server::PreForkSimple;
BEGIN {
import Amavis::Conf qw(:platform :sa :confvars c cr ca);
import Amavis::Util qw(untaint min max ll do_log sanitize_str debug_oneshot
am_id snmp_counters_init snmp_count prolong_timer);
import Amavis::Log;
import Amavis::Timing qw(section_time get_time_so_far);
import Amavis::rfc2821_2822_Tools;
import Amavis::Lookup qw(lookup lookup_ip_acl);
import Amavis::Out;
import Amavis::Out::EditHeader;
import Amavis::UnmangleSender qw(best_try_originator_ip best_try_originator
first_received_from);
import Amavis::Unpackers::Validity qw(
check_header_validity check_for_banned_names);
import Amavis::Unpackers::MIME qw(mime_decode);
import Amavis::Expand qw(expand);
import Amavis::Notify qw(delivery_status_notification delivery_short_report
string_to_mime_entity defanged_mime_entity);
import Amavis::In::Connection;
import Amavis::In::Message;
}
use vars qw(@ISA);
@ISA = qw(Net::Server::PreForkSimple);
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
use vars qw(
$extra_code_db $extra_code_cache
$extra_code_sql $extra_code_ldap
$extra_code_in_amcl $extra_code_in_smtp
$extra_code_antivirus $extra_code_antispam $extra_code_unpackers);
use vars qw($spam_level $spam_status $spam_report);
use vars qw($user_id_sql $wb_listed_sql $implicit_maps_inserted);
use vars qw($db_env $snmp_db);
use vars qw($body_digest $body_digest_cache);
use vars qw(%builtins); use vars qw($child_invocation_count $child_task_count);
use vars qw($VIRUSFILE $CONN $MSGINFO);
use vars qw($av_output @virusname @detecting_scanners
@banned_filename @bad_headers);
use vars qw($amcl_in_obj $smtp_in_obj); use vars qw($sql_policy $sql_wblist); use vars qw($ldap_policy);
sub after_chroot_init() {
my($euid) = $>; $> = 0; POSIX::setuid(0) if $> != 0; if ($> == 0) { my(@msg) = ("It is possible to change EUID from $euid to root, ABORTING!",
"Perhaps you forgot to patch the Net::Server - see:",
" http://www.ijs.si/software/amavisd/#net-server-sec",
"or start as non-root, e.g. by su(1) or using option -u user");
do_log(-3,"FATAL: $_") for @msg;
print STDERR (map {"$_\n"} @msg); die "EUID problem, ABORTING";
exit 1; }
for my $m ('Amavis::Conf',
sort map { s/\.pm\z//; s[/][::]g; $_ } grep { /\.pm\z/ } keys %INC){
next if !grep { $_ eq $m } qw(Amavis::Conf
Archive::Tar Archive::Zip Compress::Zlib Convert::TNEF Convert::UUlib
MIME::Entity MIME::Parser MIME::Tools Mail::Header Mail::Internet
Mail::ClamAV Mail::SpamAssassin Mail::SpamAssassin::SpamCopURI URI
Razor2::Client::Version Mail::SPF::Query Authen::SASL
Net::DNS Net::SMTP Net::Cmd Net::Server Net::LDAP
DBI BerkeleyDB DB_File SAVI Unix::Syslog Time::HiRes);
do_log(0, sprintf("Module %-19s %s", $m, $m->VERSION || '?'));
}
if (c('forward_method') eq '' && $extra_code_in_smtp) {
do_log(1,"forward_method in default policy bank is null (milter setup?), ".
"DISABLING SMTP-in AS A PRECAUTION");
$extra_code_in_smtp = undef;
}
do_log(0,"Amavis::DB code ".($extra_code_db ?'':" NOT")." loaded");
do_log(0,"Amavis::Cache code ".($extra_code_cache ?'':" NOT")." loaded");
do_log(0,"Lookup::SQL code ".($extra_code_sql ?'':" NOT")." loaded");
do_log(0,"Lookup::LDAP code ".($extra_code_ldap ?'':" NOT")." loaded");
do_log(0,"AMCL-in protocol code ".($extra_code_in_amcl?'':" NOT")." loaded");
do_log(0,"SMTP-in protocol code ".($extra_code_in_smtp?'':" NOT")." loaded");
do_log(0,"ANTI-VIRUS code ".($extra_code_antivirus?'':" NOT")." loaded");
do_log(0,"ANTI-SPAM code ".($extra_code_antispam ?'':" NOT")." loaded");
do_log(0,"Unpackers code ".($extra_code_unpackers?'':" NOT")." loaded");
%builtins = (
'.' => undef,
p => sub {c('policy_bank_name')},
d => sub {rfc2822_timestamp($MSGINFO->rx_time)}, U => sub {iso8601_utc_timestamp($MSGINFO->rx_time)}, y => sub {sprintf("%.0f", 1000*get_time_so_far())}, u => sub {sprintf("%010d",$MSGINFO->rx_time)},
h => $myhostname, l => sub {my($ip) = $MSGINFO->client_addr; my($val);
$val = $ip ne '' ? lookup_ip_acl($ip,@{ca('mynetworks_maps')})
: lookup(0,$MSGINFO->sender_source,
@{ca('local_domains_maps')});
$val ? 1 : undef}, s => sub {qquote_rfc2821_local($MSGINFO->sender)}, S => sub { sanitize_str($MSGINFO->sender_contact) }, o => sub { sanitize_str($MSGINFO->sender_source) }, R => sub {$MSGINFO->recips}, D => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $y}, O => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $n}, N => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $f}, Q => sub {$MSGINFO->queue_id}, m => sub { local($_) = $MSGINFO->mime_entity; if (defined) { $_ = $_->head->get('Message-ID',0); chomp;
s/^[ \t]+//; s/[ \t\n]+\z//; # trim whitespace
s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }},
r => sub { local($_) = $MSGINFO->mime_entity; if (defined) { $_ = $_->head->get('Resent-Message-ID',0); chomp;
s/^[ \t]+//; s/[ \t\n]+\z//; # trim whitespace
s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }},
j => sub { local($_) = $MSGINFO->mime_entity; if (defined) { $_ = $_->head->get('Subject',0); chomp;
s/\n([ \t])/$1/g; s{([=\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }},
b => sub {$MSGINFO->body_digest}, n => \&am_id, i => sub {$VIRUSFILE}, q => sub {my($q) = $MSGINFO->quarantined_to;
!defined($q) ? undef :
[map { my($m)=$_; $m=~s{^\Q$QUARANTINEDIR\E/}{}; $m } @$q];
}, v => sub {[split(/[ \t]*\r?\n/,$av_output)]}, V => sub {my(%seen); [grep {!$seen{$_}++} @virusname]}, F => sub {@banned_filename<=1 ? \@banned_filename
: [$banned_filename[0], '...'] }, X => sub {\@bad_headers}, W => sub {\@detecting_scanners}, H => sub {[map {my $h=$_; chomp($h); $h} @{$MSGINFO->orig_header}]}, A => sub {[split(/\r?\n/, $spam_report)]}, c => sub {!defined $spam_level ? '-' : 0+sprintf("%.3f",$spam_level+min(map {$_->recip_score_boost}
@{$MSGINFO->per_recip_data}))},
z => sub {$MSGINFO->orig_body_size+1+$MSGINFO->orig_header_size}, t => sub { sanitize_str(first_received_from($MSGINFO->mime_entity)) },
e => sub { sanitize_str(best_try_originator_ip($MSGINFO->mime_entity)) },
a => sub {$MSGINFO->client_addr}, g => sub { sanitize_str($MSGINFO->client_name) },
k => sub { my($kill_level);
scalar(grep { !$_->recip_whitelisted_sender &&
($_->recip_blacklisted_sender ||
($kill_level=lookup(0,$_->recip_addr,
@{ca('spam_kill_level_maps')}),
defined $spam_level && defined $kill_level &&
$spam_level + $_->recip_score_boost >= $kill_level) )
} @{$MSGINFO->per_recip_data}) },
'1'=> sub { my($tag_level);
scalar(grep { !$_->recip_whitelisted_sender &&
($_->recip_blacklisted_sender ||
($tag_level=lookup(0,$_->recip_addr,
@{ca('spam_tag_level_maps')}),
defined $spam_level && defined $tag_level &&
$spam_level + $_->recip_score_boost >= $tag_level) )
} @{$MSGINFO->per_recip_data}) },
'2'=> sub { my($tag2_level);
scalar(grep { !$_->recip_whitelisted_sender &&
($_->recip_blacklisted_sender ||
($tag2_level=lookup(0,$_->recip_addr,
@{ca('spam_tag2_level_maps')}),
defined $spam_level && defined $tag2_level &&
$spam_level + $_->recip_score_boost >= $tag2_level) )
} @{$MSGINFO->per_recip_data}) },
);
%Amavis::Conf::local_delivery_aliases = (
'virus-quarantine' => sub { ($QUARANTINEDIR, $VIRUSFILE) },
'banned-quarantine' => sub { ($QUARANTINEDIR, $VIRUSFILE) },
'bad-header-quarantine'=>sub { ($QUARANTINEDIR, $VIRUSFILE) },
'spam-quarantine' => sub { ($QUARANTINEDIR, "$VIRUSFILE.gz") },
'sender-quarantine' =>
sub { my($s) = $MSGINFO->sender; local($1);
$s =~ s/[^a-zA-Z0-9._@]/=/g; $s =~ s/\@/%/g;
$s = untaint($s) if $s =~ /^([a-zA-Z0-9._=%]+)\z/; $s =~ s/%/%%/g; ( $QUARANTINEDIR, "sender-$s-%i-%n.gz" ); },
'recip-quarantine' =>
sub { ("$QUARANTINEDIR/recip-archive.mbox", undef) },
'ham-quarantine' =>
sub { ("$QUARANTINEDIR/ham.mbox", undef) },
'outgoing-quarantine' =>
sub { ("$QUARANTINEDIR/outgoing.mbox", undef) },
'incoming-quarantine' =>
sub { ("$QUARANTINEDIR/incoming.mbox", undef) },
);
for my $name (keys %policy_bank) {
if (ref($policy_bank{$name}) eq 'HASH' &&
!exists($policy_bank{$name}{'policy_bank_name'}))
{ $policy_bank{$name}{'policy_bank_name'} = $name }
}
};
sub load_policy_bank($) {
my($policy_bank_name) = @_;
if (!exists $policy_bank{$policy_bank_name}) {
do_log(-1,"policy bank \"$policy_bank_name\" does not exist, ignored");
} elsif ($policy_bank_name eq '') {
%current_policy_bank = %{$policy_bank{$policy_bank_name}};
do_log(4,'loaded base policy bank');
} else {
my($cpbn) = c('policy_bank_name'); for my $k (keys %{$policy_bank{$policy_bank_name}}) {
do_log(-1,"loading policy bank \"$policy_bank_name\": ".
"unknown field \"$k\"") if !exists $current_policy_bank{$k};
$current_policy_bank{$k} = $policy_bank{$policy_bank_name}{$k};
}
do_log(2,sprintf('loaded policy bank "%s"%s', $policy_bank_name,
$cpbn eq '' ? '' : " over \"$cpbn\""));
}
}
sub pre_loop_hook {
my($self) = @_;
local $SIG{CHLD} = 'DEFAULT';
eval {
after_chroot_init();
find_external_programs([split(/:/, $path, -1)]);
my($name) = $TEMPBASE;
$name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
my($errn) = stat($TEMPBASE) ? 0 : 0+$!;
if ($errn==ENOENT) { die "No TEMPBASE directory: $name" }
elsif ($errn) { die "TEMPBASE directory inaccessible, $!: $name" }
elsif (!-d _) { die "TEMPBASE is not a directory: $name" }
elsif (!-w _) { die "TEMPBASE directory is not writable: $name" }
if ($enable_global_cache && $extra_code_db) {
my($name) = $db_home;
$name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
$errn = stat($db_home) ? 0 : 0+$!;
if ($errn == ENOENT) {
die "Please create an empty directory $name to hold a database".
" (config variable \$db_home)\n" }
elsif ($errn) { die "db_home inaccessible, $!: $name" }
elsif (!-d _) { die "db_home is not a directory : $name" }
elsif (!-w _) { die "db_home directory is not writable: $name" }
Amavis::DB::init(1);
}
if ($QUARANTINEDIR ne '') {
my($name) = $QUARANTINEDIR;
$name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
$errn = stat($QUARANTINEDIR) ? 0 : 0+$!;
if ($errn == ENOENT) { } elsif ($errn) { die "QUARANTINEDIR inaccessible, $!: $name" }
elsif (-d _ && !-w _) { die "QUARANTINEDIR directory not writable: $name" }
}
Amavis::SpamControl::init() if $extra_code_antispam;
};
if ($@ ne '') {
chomp($@); my($msg) = "TROUBLE in pre_loop_hook: $@"; do_log(-2,$msg);
die ("Suicide (" . am_id() . ") " . $msg . "\n"); }
1;
}
sub write_to_log_hook {
my($self,$level,$msg) = @_;
my($prop) = $self->{server};
local $SIG{CHLD} = 'DEFAULT';
chomp($msg);
do_log(1, "Net::Server: " . $msg); 1;
}
sub child_init_hook {
my($self) = @_;
local $SIG{CHLD} = 'DEFAULT';
$0 = 'amavisd (virgin child)';
eval {
$db_env = $snmp_db = $body_digest_cache = undef; Amavis::Timing::init(); snmp_counters_init();
if ($extra_code_db) {
$db_env = Amavis::DB->new; $snmp_db = Amavis::DB::SNMP->new($db_env);
$snmp_db->register_proc('') if defined $snmp_db; }
$body_digest_cache = Amavis::Cache->new($db_env);
if ($extra_code_db) { section_time('bdb-open');
do_log(2, Amavis::Timing::report()); }
};
if ($@ ne '') {
chomp($@); do_log(-2, "TROUBLE in child_init_hook: $@");
die "Suicide in child_init_hook: $@\n";
}
Amavis::Timing::go_idle('vir');
}
sub post_accept_hook {
my($self) = @_;
local $SIG{CHLD} = 'DEFAULT';
$child_invocation_count++;
$0 = sprintf("amavisd (ch%d-accept)", $child_invocation_count);
Amavis::Timing::go_busy('hi ');
Amavis::Timing::init(); snmp_counters_init();
$snmp_db->register_proc('A') if defined $snmp_db; load_policy_bank(''); }
sub allow_deny_hook {
my($self) = @_;
local($1,$2,$3,$4); local $SIG{CHLD} = 'DEFAULT';
my($prop) = $self->{server}; my($sock) = $prop->{client}; my($bank_name);
my($is_ux) = UNIVERSAL::can($sock,'NS_proto') && $sock->NS_proto eq 'UNIX';
if ($is_ux) {
$bank_name = $interface_policy{"SOCK"}; } else {
my($myif,$myport) = ($prop->{sockaddr}, $prop->{sockport});
if (defined $interface_policy{"$myif:$myport"}) {
$bank_name = $interface_policy{"$myif:$myport"};
} elsif (defined $interface_policy{$myport}) {
$bank_name = $interface_policy{$myport};
}
}
load_policy_bank($bank_name) if defined $bank_name &&
$bank_name ne c('policy_bank_name');
if ($is_ux) {
} else {
my($permit,$fullkey) = lookup_ip_acl($prop->{peeraddr},ca('inet_acl'));
if (!$permit) {
my($msg) = sprintf("DENIED ACCESS from IP %s, policy bank '%s'",
$prop->{peeraddr}, c('policy_bank_name') );
$msg .= ", blocked by rule $fullkey" if defined $fullkey;
do_log(-1,$msg);
return 0;
}
}
1;
}
sub process_request {
my($self) = shift;
my($prop) = $self->{server}; my($sock) = $prop->{client};
local $SIG{CHLD} = 'DEFAULT';
local($1,$2,$3,$4); binmode(STDIN) or die "Can't set STDIN to binmode: $!";
binmode(STDOUT) or die "Can't set STDOUT to binmode: $!";
binmode($sock) or die "Can't set socket to binmode: $!";
$| = 1;
local $SIG{ALRM} = sub { die "timed out\n" }; eval {
prolong_timer('new request - timer reset', $child_timeout); if ($extra_code_ldap && !defined $ldap_policy) {
$ldap_policy = Amavis::Lookup::LDAP->new($default_ldap);
}
if (defined $ldap_policy && !$implicit_maps_inserted) {
my $lf = sub{Amavis::Lookup::LDAPattr->new($ldap_policy,@_)};
unshift(@Amavis::Conf::virus_lovers_maps, $lf->('amavisVirusLover', 'B-'));
unshift(@Amavis::Conf::spam_lovers_maps, $lf->('amavisSpamLover', 'B-'));
unshift(@Amavis::Conf::banned_files_lovers_maps, $lf->('amavisBannedFilesLover', 'B-'));
unshift(@Amavis::Conf::bad_header_lovers_maps, $lf->('amavisBadHeaderLover', 'B-'));
unshift(@Amavis::Conf::bypass_virus_checks_maps, $lf->('amavisBypassVirusChecks', 'B-'));
unshift(@Amavis::Conf::bypass_spam_checks_maps, $lf->('amavisBypassSpamChecks', 'B-'));
unshift(@Amavis::Conf::bypass_banned_checks_maps,$lf->('amavisBypassBannedChecks', 'B-'));
unshift(@Amavis::Conf::bypass_header_checks_maps,$lf->('amavisBypassHeaderChecks', 'B-'));
unshift(@Amavis::Conf::spam_tag_level_maps, $lf->('amavisSpamTagLevel', 'N'));
unshift(@Amavis::Conf::spam_tag2_level_maps, $lf->('amavisSpamTag2Level', 'N'));
unshift(@Amavis::Conf::spam_kill_level_maps, $lf->('amavisSpamKillLevel', 'N'));
unshift(@Amavis::Conf::spam_modifies_subj_maps, $lf->('amavisSpamModifiesSubject','B-'));
unshift(@Amavis::Conf::message_size_limit_maps, $lf->('amavisMessageSizeLimit', 'N-'));
unshift(@Amavis::Conf::virus_quarantine_to_maps, $lf->('amavisVirusQuarantineTo', 'S-'));
unshift(@Amavis::Conf::spam_quarantine_to_maps, $lf->('amavisSpamQuarantineTo', 'S-'));
unshift(@Amavis::Conf::banned_quarantine_to_maps, $lf->('amavisBannedQuarantineTo','S-'));
unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $lf->('amavisBadHeaderQuarantineTo', 'S-'));
unshift(@Amavis::Conf::local_domains_maps, $lf->('amavisLocal',
'B1'));
section_time('ldap-prepare');
}
if ($extra_code_sql && @lookup_sql_dsn) {
$sql_wblist = Amavis::Lookup::SQL->new
if !defined $sql_wblist && defined $sql_select_white_black_list;
$sql_policy = Amavis::Lookup::SQL->new
if !defined $sql_policy && defined $sql_select_policy;
}
if (defined $sql_policy && !$implicit_maps_inserted) {
my $nf = sub{Amavis::Lookup::SQLfield->new($sql_policy,@_)}; $user_id_sql = $nf->('id', 'S');
unshift(@Amavis::Conf::local_domains_maps, $nf->('local', 'B1'));
unshift(@Amavis::Conf::virus_lovers_maps, $nf->('virus_lover', 'B0'));
unshift(@Amavis::Conf::spam_lovers_maps, $nf->('spam_lover', 'B-'));
unshift(@Amavis::Conf::banned_files_lovers_maps, $nf->('banned_files_lover', 'B-'));
unshift(@Amavis::Conf::bad_header_lovers_maps, $nf->('bad_header_lover', 'B-'));
unshift(@Amavis::Conf::bypass_virus_checks_maps, $nf->('bypass_virus_checks', 'B0'));
unshift(@Amavis::Conf::bypass_spam_checks_maps, $nf->('bypass_spam_checks', 'B0'));
unshift(@Amavis::Conf::bypass_banned_checks_maps, $nf->('bypass_banned_checks', 'B-'));
unshift(@Amavis::Conf::bypass_header_checks_maps, $nf->('bypass_header_checks', 'B-'));
unshift(@Amavis::Conf::spam_tag_level_maps, $nf->('spam_tag_level', 'N'));
unshift(@Amavis::Conf::spam_tag2_level_maps, $nf->('spam_tag2_level', 'N'));
unshift(@Amavis::Conf::spam_kill_level_maps, $nf->('spam_kill_level', 'N'));
unshift(@Amavis::Conf::spam_dsn_cutoff_level_maps,$nf->('spam_dsn_cutoff_level','N-'));
unshift(@Amavis::Conf::spam_modifies_subj_maps, $nf->('spam_modifies_subj', 'B-'));
unshift(@Amavis::Conf::spam_subject_tag_maps, $nf->('spam_subject_tag', 'S-'));
unshift(@Amavis::Conf::spam_subject_tag2_maps, $nf->('spam_subject_tag2', 'S-'));
unshift(@Amavis::Conf::virus_quarantine_to_maps, $nf->('virus_quarantine_to', 'S-'));
unshift(@Amavis::Conf::banned_quarantine_to_maps, $nf->('banned_quarantine_to', 'S-'));
unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $nf->('bad_header_quarantine_to','S-'));
unshift(@Amavis::Conf::spam_quarantine_to_maps, $nf->('spam_quarantine_to', 'S-'));
unshift(@Amavis::Conf::message_size_limit_maps, $nf->('message_size_limit', 'N-'));
unshift(@Amavis::Conf::addr_extension_virus_maps, $nf->('addr_extension_virus', 'S-'));
unshift(@Amavis::Conf::addr_extension_spam_maps, $nf->('addr_extension_spam', 'S-'));
unshift(@Amavis::Conf::addr_extension_banned_maps,$nf->('addr_extension_banned','S-'));
unshift(@Amavis::Conf::addr_extension_bad_header_maps,$nf->('addr_extension_bad_header','S-'));
unshift(@Amavis::Conf::warnvirusrecip_maps, $nf->('warnvirusrecip', 'B-'));
unshift(@Amavis::Conf::warnbannedrecip_maps, $nf->('warnbannedrecip', 'B-'));
unshift(@Amavis::Conf::warnbadhrecip_maps, $nf->('warnbadhrecip', 'B-'));
unshift(@Amavis::Conf::virus_admin_maps, $nf->('virus_admin', 'S-'));
unshift(@Amavis::Conf::spam_admin_maps, $nf->('spam_admin', 'S-'));
section_time('sql-prepare');
}
Amavis::Conf::label_default_maps() if !$implicit_maps_inserted;
$implicit_maps_inserted = 1;
my($conn) = Amavis::In::Connection->new;
$CONN = $conn; $conn->proto($sock->NS_proto);
my($suggested_protocol) = c('protocol'); do_log(5,"process_request: suggested_protocol=\"$suggested_protocol\" on ".
$sock->NS_proto);
if ($sock->NS_proto eq 'UNIX') { if ($suggested_protocol eq 'COURIER') {
die "unavailable support for protocol: $suggested_protocol";
} elsif ($suggested_protocol eq 'AM.PDP') {
$amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj;
$amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0);
} else { $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj;
$amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 1);
}
} elsif ($sock->NS_proto eq 'TCP') {
$conn->socket_ip($prop->{sockaddr});
$conn->socket_port($prop->{sockport});
$conn->client_ip($prop->{peeraddr});
if ($suggested_protocol eq 'TCP-LOOKUP') { process_tcp_lookup_request($sock, $conn);
do_log(2, Amavis::Timing::report()); } elsif ($suggested_protocol eq 'AM.PDP') {
$amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj;
$amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0);
} else { if (!$extra_code_in_smtp) {
die "incoming TCP connection, but dynamic SMTP/LMTP code not loaded";
}
$smtp_in_obj = Amavis::In::SMTP->new if !$smtp_in_obj;
$smtp_in_obj->process_smtp_request(
$sock, ($suggested_protocol eq 'LMTP'?1:0), $conn, \&check_mail);
}
} else {
die ("unsupported protocol: $suggested_protocol, " . $sock->NS_proto);
}
}; alarm(0); if ($@ ne '') {
chomp($@); my($timed_out) = $@ eq "timed out";
my($msg) = $timed_out ? "Child task exceeded $child_timeout seconds, abort"
: "TROUBLE in process_request: $@";
do_log(-2, $msg);
$smtp_in_obj->preserve_evidence(1) if $smtp_in_obj && !$timed_out;
do_log(-1, "Requesting process rundown after fatal error");
$self->done(1);
}
if ($child_task_count > $max_requests) {
do_log(1, "Requesting process rundown after $child_task_count tasks ".
"(and $child_invocation_count sessions)");
$self->done(1);
}
}
sub done(@) {
my($self) = shift;
if (@_) { $self->{server}->{done} = shift }
elsif (!$self->{server}->{done})
{ $self->{server}->{done} = $self->SUPER::done }
$self->{server}->{done};
}
sub post_process_request_hook {
my($self) = @_;
local $SIG{CHLD} = 'DEFAULT';
debug_oneshot(0);
$0 = sprintf("amavisd (ch%d-avail)", $child_invocation_count);
alarm(0); do_log(5,"post_process_request_hook: timer stopped");
$snmp_db->register_proc('') if defined $snmp_db; Amavis::Timing::go_idle('bye'); Amavis::Timing::report_load();
}
sub child_finish_hook {
my($self) = @_;
local $SIG{CHLD} = 'DEFAULT';
$0 = sprintf("amavisd (ch%d-finish)", $child_invocation_count);
do_log(5,"child_finish_hook: invoking DESTROY methods");
$smtp_in_obj = undef; $amcl_in_obj = undef; $sql_wblist = undef; $sql_policy = undef; $ldap_policy = undef; $body_digest_cache = undef; eval { $snmp_db->register_proc(undef) } if defined $snmp_db; $snmp_db = undef; $db_env = undef;
}
sub END { do_log(5,"at the END handler: invoking DESTROY methods");
$smtp_in_obj = undef; $amcl_in_obj = undef; $sql_wblist = undef; $sql_policy = undef; $ldap_policy = undef; $body_digest_cache = undef; eval { $snmp_db->register_proc(undef) } if defined $snmp_db; $snmp_db = undef; $db_env = undef;
}
sub process_tcp_lookup_request($$) {
my($sock, $conn) = @_;
local($/) = "\012"; my($req_cnt);
while (<$sock>) {
$req_cnt++; my($level) = 0;
my($resp_code, $resp_msg) = (400, 'INTERNAL ERROR');
if (/^get (.*?)\015?\012\z/si) {
my($key) = tcp_lookup_decode($1);
my($sl); $sl = lookup(0,$key, @{ca('spam_lovers_maps')});
$resp_code = 200; $level = 2;
$resp_msg = $sl ? "OK Recipient <$key> IS spam lover"
: "DUNNO Recipient <$key> is NOT spam lover";
} elsif (/^put ([^ ]*) (.*?)\015?\012\z/si) {
$resp_code = 500; $resp_msg = 'request not implemented: ' . $_;
} else {
$resp_code = 500; $resp_msg = 'illegal request: ' . $_;
}
do_log($level, "tcp_lookup($req_cnt): $resp_code $resp_msg");
$sock->printf("%03d %s\012", $resp_code, tcp_lookup_encode($resp_msg))
or die "Can't write to tcp_lookup socket: $!";
}
do_log(0, "tcp_lookup: RUNDOWN after $req_cnt requests");
}
sub tcp_lookup_encode($) {
my($str) = @_;
$str =~ s/[^\041-\044\046-\176]/sprintf("%%%02x",ord($&))/eg;
$str;
}
sub tcp_lookup_decode($) {
my($str) = @_;
$str =~ s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg;
$str;
}
sub check_mail($$$$) {
my($conn, $msginfo, $dsn_per_recip_capable, $tempdir) = @_;
my($am_id) = am_id();
$snmp_db->register_proc($am_id) if defined $snmp_db;
my($fh) = $msginfo->mail_text; my(@recips) = @{$msginfo->recips};
$MSGINFO = $msginfo; $child_task_count++;
$VIRUSFILE = undef; $av_output = undef; @detecting_scanners = ();
@virusname = (); @banned_filename = (); @bad_headers = ();
$spam_level = undef; $spam_status = undef; $spam_report = undef;
$sql_policy->clear_cache if defined $sql_policy;
$sql_wblist->clear_cache if defined $sql_wblist;
$ldap_policy->clear_cache if defined $ldap_policy;
$body_digest = get_body_digest($fh, $msginfo);
my($mail_size) = $msginfo->msg_size; $mail_size = $msginfo->orig_header_size + 1 + $msginfo->orig_body_size
if $mail_size <= 0;
my($file_generator_object) = Amavis::Unpackers::NewFilename->new($MAXFILES?$MAXFILES:undef, $mail_size);
Amavis::Unpackers::Part::init($file_generator_object); my($parts_root) = Amavis::Unpackers::Part->new;
$msginfo->parts_root($parts_root);
my($smtp_resp, $exit_code, $preserve_evidence);
my($banned_filename_checked,$virus_presence_checked,$spam_presence_checked);
my($banned_dsn_suppress) = 0;
my($any_undecipherable) = 0;
my($cl_ip) = $msginfo->client_addr; my($pbn) = c('policy_bank_name');
do_log(1,sprintf("Checking: %s%s%s -> %s",
$pbn eq '' ? '' : "$pbn ",
$cl_ip eq '' ? '' : "[$cl_ip] ",
qquote_rfc2821_local($msginfo->sender),
join(',', qquote_rfc2821_local(@recips)) ));
my($mime_err); my($hold); my($which_section);
eval {
snmp_count('InMsgs');
snmp_count('InMsgsNullRPath') if $msginfo->sender eq '';
if (@recips == 1) { snmp_count( 'InMsgsRecips' ) }
elsif (@recips > 1) { snmp_count( ['InMsgsRecips',scalar(@recips)] ) }
$which_section = "creating_partsdir";
if (-d "$tempdir/parts") {
} else {
mkdir("$tempdir/parts", 0750)
or die "Can't create directory $tempdir/parts: $!";
section_time('mkdir parts');
}
chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
$which_section = "cached";
snmp_count('CacheAttempts');
my($cache_entry); my($now) = time;
my($cache_entry_ttl) =
max($virus_check_negative_ttl, $virus_check_positive_ttl,
$spam_check_negative_ttl, $spam_check_positive_ttl);
my($now_utc_iso8601) = iso8601_utc_timestamp($now,1);
my($expires_utc_iso8601) = iso8601_utc_timestamp($now+$cache_entry_ttl,1);
$cache_entry = $body_digest_cache->get($body_digest)
if $body_digest_cache && defined $body_digest;
if (!defined $cache_entry) {
snmp_count('CacheMisses');
$cache_entry->{'ctime'} = $now_utc_iso8601; } else {
snmp_count('CacheHits');
$banned_filename_checked = defined $cache_entry->{'FB'} ? 1 : 0;
$virus_presence_checked = defined $cache_entry->{'VN'} ? 1 : 0;
$spam_presence_checked = defined $cache_entry->{'SL'} ? 1 : 0;
if ($msginfo->orig_body_size < 200) { $spam_presence_checked = 0 }
if ($virus_presence_checked && defined $cache_entry->{'Vt'}) {
my($ttl) = !@{$cache_entry->{'VN'}} ? $virus_check_negative_ttl
: $virus_check_positive_ttl;
if ($now > $cache_entry->{'Vt'} + $ttl) {
do_log(2,"Cached virus check expired, TTL = $ttl s");
$virus_presence_checked = 0;
}
}
if ($spam_presence_checked && defined $cache_entry->{'St'}) {
my($ttl) = $cache_entry->{'SL'} < 6 ? $spam_check_negative_ttl
: $spam_check_positive_ttl;
if ($now > $cache_entry->{'St'} + $ttl) {
do_log(2,"Cached spam check expired, TTL = $ttl s");
$spam_presence_checked = 0;
}
}
if ($virus_presence_checked) {
$av_output = $cache_entry->{'VO'};
@virusname = @{$cache_entry->{'VN'}};
@detecting_scanners = @{$cache_entry->{'VD'}};
}
if ($banned_filename_checked) {
@banned_filename = @{$cache_entry->{'FB'}};
$banned_dsn_suppress = $cache_entry->{'FS'};
}
($spam_level, $spam_status, $spam_report) = @$cache_entry{'SL','SS','SR'}
if $spam_presence_checked;
do_log(1,sprintf("cached %s from <%s> (%s,%s,%s)",
$body_digest, $msginfo->sender,
$banned_filename_checked, $virus_presence_checked,
$spam_presence_checked));
snmp_count('CacheHitsVirusCheck') if $virus_presence_checked;
snmp_count('CacheHitsVirusMsgs') if @virusname;
snmp_count('CacheHitsSpamCheck') if $spam_presence_checked;
snmp_count('CacheHitsSpamMsgs') if $spam_level >= 6; do_log(5,sprintf("cache entry age: %s c=%s a=%s",
(@virusname ? 'V' : $spam_level > 5 ? 'S' : '.'),
$cache_entry->{'ctime'}, $cache_entry->{'atime'} ));
}
my($will_do_virus_scanning) = !$virus_presence_checked && $extra_code_antivirus &&
grep {!lookup(0,$_, @{ca('bypass_virus_checks_maps')})} @recips;
my($will_do_banned_checking) = !$banned_filename_checked &&
(@{ca('banned_filename_maps')} || cr('banned_namepath_re')) &&
grep {!lookup(0,$_, @{ca('bypass_banned_checks_maps')})} @recips;
my($will_do_parts_decoding) =
!c('bypass_decode_parts') &&
($will_do_virus_scanning || $will_do_banned_checking);
$which_section = "mime_decode-1";
my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
$msginfo->mime_entity($ent);
push(@bad_headers, "MIME error: ".$mime_err) if $mime_err ne '';
prolong_timer($which_section);
if ($will_do_parts_decoding) {
snmp_count('OpsDec');
($hold,$any_undecipherable) =
Amavis::Unpackers::decompose_mail($tempdir,$file_generator_object);
}
if (grep {!lookup(0,$_,@{ca('bypass_header_checks_maps')})} @recips) {
push(@bad_headers, check_header_validity($conn,$msginfo));
}
if ($will_do_banned_checking) { $which_section = "check-banned";
my($banned_part_descr_ref, $banned_matching_keys_ref, $banned_rhs_ref) =
check_for_banned_names($parts_root);
for my $j (0..$ if ($banned_rhs_ref->[$j] =~ /^DISCARD/) {
$banned_dsn_suppress = 1;
do_log(4,sprintf('BANNED:%s: %s', $banned_rhs_ref->[$j],
$banned_part_descr_ref->[$j]));
}
}
push(@banned_filename, @$banned_part_descr_ref);
}
$cache_entry->{'FB'} = \@banned_filename;
$cache_entry->{'FS'} = $banned_dsn_suppress;
if ($virus_presence_checked) {
do_log(5, "virus_presence cached, skipping virus_scan");
} elsif (!$extra_code_antivirus) {
do_log(5, "no anti-virus code loaded, skipping virus_scan");
} elsif (!grep {!lookup(0,$_,@{ca('bypass_virus_checks_maps')})} @recips) {
do_log(5, "bypassing of virus checks requested");
} elsif ($hold ne '') { do_log(0, "NOTICE: Virus scanning skipped: $hold");
$will_do_virus_scanning = 0;
} else {
if (!$will_do_virus_scanning)
{ do_log(-1, "NOTICE: will_do_virus_scanning is false???") }
if (!defined($msginfo->mime_entity)) {
$which_section = "mime_decode-3";
my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
$msginfo->mime_entity($ent);
prolong_timer($which_section);
}
if ($mime_err ne '' ||
lookup(0,'MAIL',@keep_decoded_original_maps) ||
$any_undecipherable && lookup(0,'MAIL-UNDECIPHERABLE',
@keep_decoded_original_maps)) {
$which_section = "linking-to-MAIL";
my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",
$parts_root);
my($newpart) = $newpart_obj->full_name;
do_log(2, "providing full original message to scanners as $newpart".
(!$any_undecipherable ?'' :", $any_undecipherable undecipherable").
($mime_err eq '' ? '' : ", MIME error: $mime_err") );
link($msginfo->mail_text_fn, $newpart)
or die sprintf("Can't create hard link %s to %s: %s",
$newpart, $msginfo->mail_text_fn, $!);
$newpart_obj->type_short('MAIL');
$newpart_obj->type_declared('message/rfc822');
}
$which_section = "virus_scan";
my($remaining_time) = alarm(0); my($av_ret);
eval {
my($vn, $ds);
($av_ret, $av_output, $vn, $ds) =
Amavis::AV::virus_scan($tempdir, $child_task_count==1, $parts_root);
@virusname = @$vn; @detecting_scanners = @$ds; };
prolong_timer($which_section, $remaining_time); if ($@ ne '') {
chomp($@);
if ($@ eq "timed out") {
@virusname = (); $av_ret = 0; do_log(-1, "virus_scan TIMED OUT, ASSUME NOT A VIRUS !!!");
} else {
$hold = "virus_scan: $@"; $av_ret = 0; die "$hold\n"; }
}
snmp_count('OpsVirusCheck');
defined($av_ret) or die "All virus scanners failed!";
@$cache_entry{'Vt','VO','VN','VD'} =
($now, $av_output, \@virusname, \@detecting_scanners);
$virus_presence_checked = 1;
}
my($sender_contact,$sender_source);
if (!@virusname) { $sender_contact = $sender_source = $msginfo->sender }
else {
($sender_contact,$sender_source) = best_try_originator(
$msginfo->sender, $msginfo->mime_entity, \@virusname);
section_time('best_try_originator');
}
$msginfo->sender_contact($sender_contact); $msginfo->sender_source($sender_source);
if (!$extra_code_antispam) {
do_log(5, "no anti-spam code loaded, skipping spam_scan");
} elsif (@virusname || @banned_filename) {
do_log(5, "infected or banned contents, skipping spam_scan");
} elsif (!grep {!lookup(0,$_,@{ca('bypass_spam_checks_maps')})} @recips) {
do_log(5, "bypassing of spam checks requested");
} else {
$which_section = "spam-wb-list";
my($any_wbl, $all_wbl) = Amavis::SpamControl::white_black_list(
$conn, $msginfo, $sql_wblist, $user_id_sql, $ldap_policy);
section_time($which_section);
if ($all_wbl) {
do_log(5, "sender white/blacklisted, skipping spam_scan");
} elsif ($spam_presence_checked) {
do_log(5, "spam_presence cached, skipping spam_scan");
} else {
$which_section = "spam_scan";
($spam_level, $spam_status, $spam_report) =
Amavis::SpamControl::spam_scan($conn, $msginfo);
prolong_timer($which_section);
snmp_count('OpsSpamCheck');
@$cache_entry{'St','SL','SS','SR'} =
($now, $spam_level, $spam_status, $spam_report);
$spam_presence_checked = 1;
}
}
$cache_entry->{'atime'} = $now_utc_iso8601; $body_digest_cache->set($body_digest,$cache_entry,
$now_utc_iso8601,$expires_utc_iso8601)
if $body_digest_cache && defined $body_digest;
$cache_entry = undef; section_time('update_cache');
snmp_count("virus.byname.$_") for @virusname;
my($considered_spam_by_some_recips,$considered_oversize_by_some_recips);
if (@virusname || @banned_filename) { $which_section = "deal_with_virus_or_banned";
my($final_destiny) = @virusname ? c('final_virus_destiny')
: @banned_filename ? c('final_banned_destiny')
: @bad_headers ? c('final_bad_header_destiny')
: D_PASS;
for my $r (@{$msginfo->per_recip_data}) {
next if $r->recip_done; if ($final_destiny == D_PASS) {
} elsif ((!@virusname || lookup(0,$r->recip_addr, @{ca('virus_lovers_maps')})) &&
(!@banned_filename || lookup(0,$r->recip_addr, @{ca('banned_files_lovers_maps')})) &&
(!@bad_headers || lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')})) )
{
} else { $r->recip_destiny($final_destiny);
my($reason);
if (@virusname)
{ $reason = "VIRUS: " . join(", ", @virusname) }
elsif (@banned_filename)
{ $reason = "BANNED: " . join(", ", @banned_filename) }
elsif (@bad_headers)
{ $reason = "BAD_HEADER: " . join(", ", @bad_headers) }
$reason = substr($reason,0,100)."..." if length($reason) > 100+3;
$r->recip_smtp_response( ($final_destiny == D_DISCARD
? "250 2.7.1 Ok, discarded"
: "550 5.7.1 Message content rejected") .
", id=$am_id - $reason");
$r->recip_done(1);
}
}
$which_section = "virus_or_banned quar+notif";
ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
do_virus($conn, $msginfo);
} else { $which_section = "deal_with_spam";
my($final_destiny) = c('final_spam_destiny');
for my $r (@{$msginfo->per_recip_data}) {
next if $r->recip_done; my($kill_level);
$kill_level = lookup(0,$r->recip_addr, @{ca('spam_kill_level_maps')});
my($boost) = $r->recip_score_boost;
my($should_be_killed) =
!$r->recip_whitelisted_sender &&
($r->recip_blacklisted_sender ||
(defined $spam_level && defined $kill_level ?
$spam_level+$boost >= $kill_level : 0) );
next unless $should_be_killed;
$considered_spam_by_some_recips = 1;
if ($final_destiny == D_PASS ||
lookup(0,$r->recip_addr, @{ca('spam_lovers_maps')})) {
} else { ll(3) && do_log(3,sprintf(
"SPAM-KILL, %s -> %s, hits=%s, kill=%s%s",
qquote_rfc2821_local($msginfo->sender, $r->recip_addr),
(!defined $spam_level ? 'x'
: !defined $boost ? $spam_level
: $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost),
!defined $kill_level ? 'x' : 0+sprintf("%.3f",$kill_level),
$r->recip_blacklisted_sender ? ', BLACKLISTED' : ''));
$r->recip_destiny($final_destiny);
my($reason) =
$r->recip_blacklisted_sender ? 'sender blacklisted' : 'UBE';
$r->recip_smtp_response(($final_destiny == D_DISCARD
? "250 2.7.1 Ok, discarded, $reason"
: "550 5.7.1 Message content rejected, $reason"
) . ", id=$am_id");
$r->recip_done(1);
}
}
if ($considered_spam_by_some_recips) {
$which_section = "spam quar+notif";
ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
do_spam($conn, $msginfo);
section_time('post-do_spam');
}
}
if (@bad_headers) { $which_section = "deal_with_bad_headers";
ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
my($is_bulk) = $msginfo->mime_entity->head->get('precedence', 0);
chomp($is_bulk);
do_log(1,sprintf("BAD HEADER from %s<%s>: %s",
$is_bulk eq '' ? '' : "($is_bulk) ", $msginfo->sender,
$bad_headers[0]));
$is_bulk = $is_bulk=~/^(bulk|list|junk)/i ? $1 : undef;
if (defined $is_bulk || $msginfo->sender eq '') {
} else {
my($any_badh); my($final_destiny) = c('final_bad_header_destiny');
for my $r (@{$msginfo->per_recip_data}) {
next if $r->recip_done; if ($final_destiny == D_PASS ||
lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')}))
{
} else { $r->recip_destiny($final_destiny);
my($reason) = (split(/\n/, $bad_headers[0]))[0];
$r->recip_smtp_response(($final_destiny == D_DISCARD
? "250 2.6.0 Ok, message with invalid header discarded"
: "554 5.6.0 Message with invalid header rejected"
) . ", id=$am_id - $reason");
$r->recip_done(1);
$any_badh++;
}
}
if ($any_badh) { do_virus($conn, $msginfo); }
}
section_time($which_section);
}
my($mslm) = ca('message_size_limit_maps');
if (@$mslm) {
$which_section = "deal_with_mail_size";
my($mail_size) = $msginfo->msg_size;
for my $r (@{$msginfo->per_recip_data}) {
next if $r->recip_done; my($size_limit) = lookup(0,$r->recip_addr, @$mslm);
$size_limit = 65536
if $size_limit && $size_limit < 65536; if ($size_limit && $mail_size > $size_limit) {
do_log(1,sprintf("OVERSIZE from <%s> to <%s>: size %s B, limit %s B",
$msginfo->sender, $r->recip_addr, $mail_size, $size_limit))
if !$considered_oversize_by_some_recips;
$considered_oversize_by_some_recips = 1;
$r->recip_destiny(D_BOUNCE);
$r->recip_smtp_response("552 5.3.4 Message size ($mail_size B) ".
"exceeds fixed maximium message size of $size_limit B, id=$am_id");
$r->recip_done(1);
}
}
section_time($which_section);
}
$which_section = "snooping_quarantine";
$which_section = "checking_sender_ip";
my(@recips) = @{$msginfo->recips};
if ($considered_spam_by_some_recips && @recips==1 &&
$recips[0] eq $msginfo->sender &&
lookup(0,$msginfo->sender, @{ca('local_domains_maps')}))
{
my($cl_ip) = $msginfo->client_addr;
if ($cl_ip eq '') {
ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
$cl_ip = fish_out_ip_from_received(
$msginfo->mime_entity->head->get('received',0));
}
if ($cl_ip ne '') {
my($is_our_ip) =
eval { lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) };
if ($@ ne '' && !$is_our_ip) {
do_log(0, "FAKE SENDER, SPAM: $cl_ip, " . $msginfo->sender);
$msginfo->sender_contact(undef); }
}
}
if ($hold ne '') { do_log(-1, "NOTICE: HOLD reason: $hold") }
my($which_content_counter) =
@virusname ? 'ContentVirusMsgs'
: @banned_filename ? 'ContentBannedMsgs'
: $considered_spam_by_some_recips ? 'ContentSpamMsgs'
: @bad_headers ? 'ContentBadHdrMsgs'
: $considered_oversize_by_some_recips ? 'ContentOversizeMsgs'
: 'ContentCleanMsgs';
snmp_count($which_content_counter);
my($hdr_edits) = $msginfo->header_edits;
if (!$hdr_edits) {
$hdr_edits = Amavis::Out::EditHeader->new;
$msginfo->header_edits($hdr_edits);
}
if ($msginfo->delivery_method eq '') { $which_section = "AM.PDP headers";
ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
$hdr_edits = add_forwarding_header_edits_common(
$conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
$virus_presence_checked, $spam_presence_checked, undef);
my($done_all);
my($recip_cl); ($hdr_edits, $recip_cl, $done_all) =
add_forwarding_header_edits_per_recip(
$conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
$virus_presence_checked, $spam_presence_checked, undef, undef);
$msginfo->header_edits($hdr_edits); if (@$recip_cl && !$done_all) {
do_log(-1, "AM.PDP: CLIENTS REQUIRE DIFFERENT HEADERS");
};
} elsif (grep { !$_->recip_done } @{$msginfo->per_recip_data}) { $which_section = "forwarding";
ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
my($mail_defanged); my($explanation);
if ($hold ne '') { $explanation =
"WARNING: possible mail bomb, NOT CHECKED FOR VIRUSES:\n $hold";
} elsif (@virusname) { $explanation =
'WARNING: contains virus '.join(' ',@virusname) if c('defang_virus');
} elsif (@banned_filename) { $explanation =
"WARNING: contains banned part" if c('defang_banned');
} elsif ($any_undecipherable) { $explanation =
"WARNING: contains undecipherable part" if c('defang_undecipherable');
} elsif ($considered_spam_by_some_recips) { $explanation =
$spam_report if c('defang_spam');
} elsif (@bad_headers) { $explanation =
'WARNING: bad headers '.join(' ',@bad_headers) if c('defang_bad_header');
} else { $explanation = '(clean)' if c('defang_all') }
if (defined $explanation) { $explanation .= "\n" if $explanation !~ /\n\z/;
my($s) = $explanation; $s=~s/[ \t\n]+\z//;
if (length($s) > 100) { $s = substr($s,0,100-3) . "..." }
do_log(1, "DEFANGING MAIL: $s");
my($d) = defanged_mime_entity($conn,$msginfo,$explanation);
$msginfo->mail_text($d); $msginfo->mail_text_fn(undef); $mail_defanged = 'Original mail wrapped as attachment (defanged)';
section_time('defang');
}
$hdr_edits = add_forwarding_header_edits_common(
$conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
$virus_presence_checked, $spam_presence_checked, $mail_defanged);
for (;;) { my($r_hdr_edits) = Amavis::Out::EditHeader->new; $r_hdr_edits->inherit_header_edits($hdr_edits);
my($done_all);
my($recip_cl); ($r_hdr_edits, $recip_cl, $done_all) =
add_forwarding_header_edits_per_recip(
$conn, $msginfo, $r_hdr_edits, $hold, $any_undecipherable,
$virus_presence_checked, $spam_presence_checked,
$mail_defanged, undef);
last if !@$recip_cl;
$msginfo->header_edits($r_hdr_edits); mail_dispatch($conn, $msginfo, 0,
sub { my($r) = @_; grep { $_ eq $r } @$recip_cl });
snmp_count('OutForwMsgs');
snmp_count('OutForwHoldMsgs') if $hold ne '';
last if $done_all;
}
}
prolong_timer($which_section);
$which_section = "delivery-notification";
my($dsn_needed);
($smtp_resp, $exit_code, $dsn_needed) =
one_response_for_all($msginfo, $dsn_per_recip_capable, $am_id);
my($warnsender_with_pass) =
$smtp_resp =~ /^2/ && !$dsn_needed &&
(@virusname && c('warnvirussender') ||
@banned_filename && c('warnbannedsender') ||
$considered_spam_by_some_recips && c('warnspamsender') ||
@bad_headers && c('warnbadhsender') );
ll(4) && do_log(4,sprintf(
"warnsender_with_pass=%s (%s,%s,%s,%s), dsn_needed=%s, exit=%s, %s",
$warnsender_with_pass,
c('warnvirussender'),c('warnbannedsender'),
c('warnbadhsender'),c('warnspamsender'),
$dsn_needed,$exit_code,$smtp_resp));
if ($dsn_needed || $warnsender_with_pass) {
ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
my($what_bad_content) = join(' & ',
!@virusname ? () : 'VIRUS',
!@banned_filename ? () : 'BANNED',
!$considered_spam_by_some_recips ? () : 'SPAM',
!@bad_headers ? () : 'BAD HEADER',
!$considered_oversize_by_some_recips ? () : 'OVERSIZE');
my($notification); my($dsn_cutoff_level);
if ($msginfo->sender eq '') { my($msg) = "DSN contains $what_bad_content; bounce is not bouncible";
if (!$dsn_needed) { do_log(4, $msg) }
else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
$msginfo->dsn_sent(2); } elsif ($msginfo->sender_contact eq '') {
my($msg) = sprintf("Not sending DSN to believed-to-be-faked "
. "sender <%s>, mail containing %s",
$msginfo->sender, $what_bad_content);
if (!$dsn_needed) { do_log(4, $msg) }
else { do_log(2, "NOTICE: $msg intentionally dropped") }
$msginfo->dsn_sent(2); } elsif ($banned_dsn_suppress) {
my($msg) = "Not sending DSN, as suggested by banned rule";
if (!$dsn_needed) { do_log(4, $msg) }
else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
$msginfo->dsn_sent(2); } elsif (defined $spam_level &&
!grep { $spam_level + $_->recip_score_boost <
lookup(0,$_->recip_addr,
@{ca('spam_dsn_cutoff_level_maps')}) }
@{$msginfo->per_recip_data} ) {
my($msg) = "Not sending DSN, spam level exceeds DSN cutoff level";
if (!$dsn_needed) { do_log(4, $msg) }
else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
$msginfo->dsn_sent(2); } elsif ((@virusname || @banned_filename ||
$considered_spam_by_some_recips || @bad_headers ||
$considered_oversize_by_some_recips) &&
$msginfo->mime_entity->head->get('precedence',0)
=~ /^(bulk|list|junk)/i )
{
my($msg) = sprintf("Not sending DSN in response to bulk mail "
. "from <%s> containing %s",
$msginfo->sender, $what_bad_content);
if (!$dsn_needed) { do_log(4, $msg) }
else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
$msginfo->dsn_sent(2); } else { my($which_dsn_counter,$dsnmsgref);
for my $r (@{$msginfo->per_recip_data}) {
next if !$r->recip_done;
local($_) = $r->recip_smtp_response;
($which_dsn_counter,$dsnmsgref) =
/^5.*\bVIRUS\b/ ?
('OutDsnVirusMsgs', cr('notify_virus_sender_templ'))
: /^5.*\bBANNED\b/ ?
('OutDsnBannedMsgs',cr('notify_virus_sender_templ'))
: /^5.*\b(?:UBE|blacklisted)\b/ ?
('OutDsnSpamMsgs', cr('notify_spam_sender_templ'))
: /^5.*\bheader\b/ ?
('OutDsnBadHdrMsgs',cr('notify_sender_templ'))
: ('OutDsnOtherMsgs', cr('notify_sender_templ'));
}
$notification = delivery_status_notification($conn, $msginfo,
$warnsender_with_pass, \%builtins, $dsnmsgref) if $dsnmsgref;
snmp_count($which_dsn_counter) if defined $notification;
}
if (defined $notification) { mail_dispatch($conn, $notification, 1);
snmp_count('OutDsnMsgs');
my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
one_response_for_all($notification, 0, $am_id); if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { $msginfo->dsn_sent(1); } elsif ($n_smtp_resp =~ /^4/) {
snmp_count('OutDsnTempFails');
die sprintf("temporarily unable to send DSN to <%s>: %s",
$msginfo->sender_contact, $n_smtp_resp);
} else {
snmp_count('OutDsnRejects');
do_log(-1,sprintf("NOTICE: UNABLE TO SEND DSN to <%s>: %s",
$msginfo->sender, $n_smtp_resp));
}
}
}
prolong_timer($which_section);
$which_section = 'main_log_entry';
my(%mybuiltins) = %builtins; { my($s) = $spam_status; $s =~ s/^tests=//; my(@s) = split(/,/,$s);
if (@s > 10) { $ $mybuiltins{'T'} = \@s; my($strr) = expand(cr('log_templ'), \%mybuiltins);
for my $logline (split(/[ \t]*\n/, $$strr)) {
do_log(0, $logline) if $logline ne '';
}
}
if (c('log_recip_templ') ne '') { for my $r (@{$msginfo->per_recip_data}) {
$mybuiltins{'.'}++;
my($recip) = $r->recip_addr;
my($smtp_resp) = $r->recip_smtp_response;
my($qrecip_addr) = scalar(qquote_rfc2821_local($recip));
$mybuiltins{'D'} = $mybuiltins{'O'} = $mybuiltins{'N'} = undef;
if ($r->recip_destiny==D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)){
$mybuiltins{'D'} = $qrecip_addr;
} else {
$mybuiltins{'O'} = $qrecip_addr;
my($remote_mta) = $r->recip_remote_mta;
$mybuiltins{'N'} = sprintf("%s:%s\n %s", $qrecip_addr,
($remote_mta eq '' ? '' : " $remote_mta said:"), $smtp_resp);
}
my($blacklisted) = $r->recip_blacklisted_sender;
my($whitelisted) = $r->recip_whitelisted_sender;
my($boost) = $r->recip_score_boost;
my($is_local,$tag_level,$tag2_level,$kill_level);
$is_local = lookup(0,$recip, @{ca('local_domains_maps')});
$tag_level = lookup(0,$recip, @{ca('spam_tag_level_maps')});
$tag2_level = lookup(0,$recip, @{ca('spam_tag2_level_maps')});
$kill_level = lookup(0,$recip, @{ca('spam_kill_level_maps')});
my($do_tag) =
$blacklisted || !defined $tag_level ||
(defined $spam_level ? $spam_level+$boost >= $tag_level
: $whitelisted ? (-10 >= $tag_level) : 0);
my($do_tag2) = !$whitelisted &&
( $blacklisted ||
(defined $spam_level && defined $tag2_level ?
$spam_level+$boost >= $tag2_level : 0) );
my($do_kill) = !$whitelisted &&
( $blacklisted ||
(defined $spam_level && defined $kill_level ?
$spam_level+$boost >= $kill_level : 0) );
for ($do_tag,$do_tag2,$do_kill) { $_ = $_ ? 'Y' : '0' } for ($is_local) { $_ = $_ ? 'L' : '0' } for ($tag_level,$tag2_level,$kill_level) { $_ = 'x' if !defined($_) }
$mybuiltins{'R'} = $recip;
$mybuiltins{'c'} = !defined $spam_level ? '-'
: 0+sprintf("%.3f",$spam_level+$boost);
@mybuiltins{('0','1','2','k')} = ($is_local,$do_tag,$do_tag2,$do_kill);
@mybuiltins{('3','4','5')} = ($tag_level,$tag2_level,$kill_level);
my($strr) = expand(cr('log_recip_templ'), \%mybuiltins);
for my $logline (split(/[ \t]*\n/, $$strr)) {
do_log(0, $logline) if $logline ne '';
}
}
}
section_time($which_section);
$which_section = 'finishing';
$snmp_db->update_counters if defined $snmp_db;
section_time('update_snmp');
}; if ($@ ne '') {
chomp($@);
$preserve_evidence = 1;
my($msg) = "$which_section FAILED: $@";
do_log(-2, "TROUBLE in check_mail: $msg");
$smtp_resp = "451 4.5.0 Error in processing, id=$am_id, $msg";
$exit_code = EX_TEMPFAIL;
for my $r (@{$msginfo->per_recip_data}) {
next if $r->recip_done;
$r->recip_smtp_response($smtp_resp); $r->recip_done(1);
}
}
if (!$preserve_evidence && debug_oneshot()) {
do_log(0, "DEBUG_ONESHOT CAUSES EVIDENCE TO BE PRESERVED");
$preserve_evidence = 1;
}
my($which_counter) = 'InUnknown';
if ($smtp_resp =~ /^4/) { $which_counter = 'InTempFails' }
elsif ($smtp_resp =~ /^5/) { $which_counter = 'InRejects' }
elsif ($smtp_resp =~ /^2/) {
my($dsn_sent) = $msginfo->dsn_sent;
if (!$dsn_sent) { $which_counter = $msginfo->delivery_method ne ''
? 'InAccepts' : 'InContinues' }
elsif ($dsn_sent==1) { $which_counter = 'InBounces' }
elsif ($dsn_sent==2) { $which_counter = 'InDiscards' }
}
snmp_count($which_counter);
$snmp_db->register_proc('.') if defined $snmp_db;
$MSGINFO = undef; ($smtp_resp, $exit_code, $preserve_evidence);
}
sub ensure_mime_entity($$$$$) {
my($msginfo, $fh, $tempdir, $virusname_list, $parts_root) = @_;
if (!defined($msginfo->mime_entity)) {
my($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
$msginfo->mime_entity($ent);
prolong_timer("ensure_mime_entity");
}
}
sub add_forwarding_header_edits_common($$$$$$) {
my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
$virus_presence_checked, $spam_presence_checked, $mail_defanged) = @_;
$hdr_edits->prepend_header('Received',
received_line($conn,$msginfo,am_id(),1), 1)
if $insert_received_line && $msginfo->delivery_method ne '';
$hdr_edits->delete_header('X-Amavis-Hold');
if ($hold ne '') {
$hdr_edits->append_header('X-Amavis-Hold', $hold);
do_log(-1, "Inserting header field: X-Amavis-Hold: $hold");
}
if ($mail_defanged ne '') {
$hdr_edits->prepend_header('Resent-Message-ID',
sprintf('<RE%s@%s>',am_id(),$myhostname) );
$hdr_edits->prepend_header('Resent-Date',
rfc2822_timestamp($msginfo->rx_time));
$hdr_edits->prepend_header('Resent-From', c('hdrfrom_notify_recip'));
my($msg) = "$mail_defanged by $myhostname";
$hdr_edits->append_header('X-Amavis-Modified', $msg);
do_log(1, "Inserting header field: X-Amavis-Modified: $msg");
}
if ($extra_code_antivirus) {
$hdr_edits->delete_header('X-Amavis-Alert');
$hdr_edits->delete_header(c('X_HEADER_TAG'))
if c('remove_existing_x_scanned_headers') &&
(c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/);
}
if ($extra_code_antispam) {
if (c('remove_existing_spam_headers')) {
my(@which_headers) = qw(
X-Spam-Status X-Spam-Level X-Spam-Flag X-Spam-Score
X-Spam-Report X-Spam-Checker-Version X-Spam-Tests);
push(@which_headers, qw(X-DSPAM-Result X-DSPAM-Signature X-DSPAM-User
X-DSPAM-Probability) ) if defined $dspam;
for my $h (@which_headers) { $hdr_edits->delete_header($h) }
}
}
$hdr_edits;
}
sub add_forwarding_header_edits_per_recip($$$$$$$$$) {
my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
$virus_presence_checked, $spam_presence_checked,
$mail_defanged, $filter) = @_;
my(@recip_cluster);
my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
@{$msginfo->per_recip_data};
my($per_recip_data_len) = scalar(@per_recip_data);
my($first) = 1; my($cluster_key); my($cluster_full_spam_status);
for my $r (@per_recip_data) {
my($recip) = $r->recip_addr;
my($is_local,$blacklisted,$whitelisted,$boost,$tag_level,$tag2_level,
$do_tag_virus_checked,$do_tag_virus,$do_tag_banned,$do_tag_badh,
$do_tag,$do_tag2,$do_subj,$do_subj_u,$subject_tag,$subject_tag2);
$is_local = lookup(0,$recip, @{ca('local_domains_maps')});
$do_tag_badh = @bad_headers &&
!lookup(0,$recip,@{ca('bypass_header_checks_maps')});
$do_tag_banned= @banned_filename &&
!lookup(0,$recip,@{ca('bypass_banned_checks_maps')});
$do_tag_virus = @virusname &&
!lookup(0,$recip,@{ca('bypass_virus_checks_maps')});
$do_tag_virus_checked =
$virus_presence_checked &&
(c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/) &&
!lookup(0,$recip,@{ca('bypass_virus_checks_maps')});
if ($extra_code_antispam) {
my($bypassed);
$blacklisted = $r->recip_blacklisted_sender;
$whitelisted = $r->recip_whitelisted_sender;
$boost = $r->recip_score_boost;
$bypassed = lookup(0,$recip, @{ca('bypass_spam_checks_maps')});
$tag_level = lookup(0,$recip, @{ca('spam_tag_level_maps')});
$tag2_level = lookup(0,$recip, @{ca('spam_tag2_level_maps')});
$do_tag = $is_local && !$bypassed &&
( $blacklisted || !defined $tag_level ||
(defined $spam_level ? $spam_level+$boost >= $tag_level
: $whitelisted ? (-10 >= $tag_level) : 0) );
$do_tag2 = $is_local && !$bypassed && !$whitelisted &&
( $blacklisted ||
(defined $spam_level && defined $tag2_level ?
$spam_level+$boost >= $tag2_level : 0) );
$subject_tag2 = !$do_tag2 ? undef
: lookup(0,$recip, @{ca('spam_subject_tag2_maps')});
$subject_tag = !($do_tag||$do_tag2) ? undef
: lookup(0,$recip, @{ca('spam_subject_tag_maps')});
$do_subj = ($subject_tag2 ne '' || $subject_tag ne '') &&
lookup(0,$recip, @{ca('spam_modifies_subj_maps')});
}
if ($hold ne '' || $any_undecipherable) { $do_subj_u = $is_local && c('undecipherable_subject_tag') ne '' &&
!(@virusname && !lookup(0,$recip,
@{ca('bypass_virus_checks_maps')}) );
}
for ($do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh,
$do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local) { $_ = $_?1:0 }
my($spam_level_bar, $full_spam_status);
if ($do_tag || $do_tag2) {
my($slc) = c('sa_spam_level_char');
$spam_level_bar =
$slc x min($blacklisted ? 64 : $spam_level+$boost, 64) if $slc ne '';
my($s) = $spam_status; $s =~ s/,/,\n /g; $full_spam_status = sprintf("%s,\n hits=%s\n%s%s %s%s",
$do_tag2 ? 'Yes' : 'No',
!defined $spam_level ? 'x' : 0+sprintf("%.3f",$spam_level+$boost),
!defined $tag_level ? '' : sprintf(" tagged_above=%s\n",$tag_level),
!defined $tag2_level ? '' : sprintf(" required=%s\n", $tag2_level),
join('', $blacklisted ? "BLACKLISTED\n " : (),
$whitelisted ? "WHITELISTED\n " : ()),
$s);
}
my($key) = join("\000",
$do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh,
$do_tag, $do_tag2, $do_subj, $do_subj_u, $spam_level_bar,
$full_spam_status);
if ($first) {
ll(4) && do_log(4,sprintf(
"headers CLUSTERING: NEW CLUSTER <%s>: ".
"hits=%s, tag=%s, tag2=%s, subj=%s, subj_u=%s, local=%s, bl=%s",
$recip,
(!defined $spam_level ? 'x'
: !defined $boost ? $spam_level
: $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost),
$do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local, $blacklisted));
$cluster_key = $key; $cluster_full_spam_status = $full_spam_status;
} elsif ($key eq $cluster_key) {
do_log(5,"headers CLUSTERING: <$recip> joining cluster");
} else {
do_log(5,"headers CLUSTERING: skipping <$recip> (tag=$do_tag, tag2=$do_tag2)");
next; }
if ($first) { if ($do_tag_virus_checked) {
$hdr_edits->append_header(c('X_HEADER_TAG'), c('X_HEADER_LINE'));
}
if ($do_tag_virus) {
$hdr_edits->append_header('X-Amavis-Alert',
"INFECTED, message contains virus:\n " . join(",\n ",@virusname), 1);
}
if ($do_tag_banned) {
my(@b) = @banned_filename>3 ? @banned_filename[0..2] :@banned_filename;
my($msg) = "BANNED, message contains "
. (@banned_filename==1 ? 'part' : 'parts') . ":\n "
. join(",\n ", @b) . (@banned_filename > @b ? ", ..." : "");
$hdr_edits->append_header('X-Amavis-Alert', $msg, 1);
}
if ($do_tag_badh) {
$hdr_edits->append_header('X-Amavis-Alert',
'BAD HEADER '.$bad_headers[0], 1);
}
if ($do_tag) {
$hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
$hdr_edits->append_header('X-Spam-Level',
$spam_level_bar) if defined $spam_level_bar;
}
if ($do_tag2) {
$hdr_edits->append_header('X-Spam-Flag', 'YES');
$hdr_edits->append_header('X-Spam-Report', $spam_report,1)
if $spam_report ne '' && c('sa_spam_report_header');
}
if ($do_subj || $do_subj_u) {
my($s) = '';
if ($do_subj_u) {
$s = c('undecipherable_subject_tag');
do_log(3,"adding $s, $any_undecipherable, $hold");
}
if ($do_subj) {
$s .= $do_tag2 && $subject_tag2 ne '' ? $subject_tag2 : $subject_tag;
}
my($entity) = $msginfo->mime_entity;
if (defined $entity && defined $entity->head->get('Subject',0)) {
$hdr_edits->edit_header('Subject',
sub { $_[1]=~/^([ \t]?)(.*)\z/s; ' '.$s.$2 });
} else { $s =~ s/[ \t]+\z//; # trim
$hdr_edits->append_header('Subject', $s);
if (!defined $entity) {
do_log(-1,"WARN: no MIME entity!? Inserting 'Subject'");
} else {
do_log(0,"INFO: no existing header field 'Subject', inserting it");
}
}
}
}
push(@recip_cluster,$r); $first = 0;
my($delim) = c('recipient_delimiter');
if ($delim ne '' && $is_local) {
my($ext_map) = $do_tag_virus ? ca('addr_extension_virus_maps')
: $do_tag_banned ? ca('addr_extension_banned_maps')
: $do_tag2 ? ca('addr_extension_spam_maps')
: $do_tag_badh ? ca('addr_extension_bad_header_maps')
: undef;
my($ext) = !ref($ext_map) ? undef : lookup(0,$recip, @$ext_map);
if ($ext ne '') {
my($localpart,$domain) = split_address($recip);
if (c('replace_existing_extension')) { $localpart =~ s/^(.*?)\Q$delim\E.*\z/$1/s }
do_log(5,"adding address extension $delim$ext to $localpart$domain");
$r->recip_addr_modified($localpart.$delim.$ext.$domain);
}
}
}
my($done_all);
if (@recip_cluster == $per_recip_data_len) {
do_log(5,"headers CLUSTERING: " .
"done all $per_recip_data_len recips in one go");
$done_all = 1;
} else {
ll(4) && do_log(4,sprintf(
"headers CLUSTERING: got %d recips out of %d: %s",
scalar(@recip_cluster), $per_recip_data_len,
join(", ", map { "<" . $_->recip_addr . ">" } @recip_cluster) ));
}
if (defined($cluster_full_spam_status) && @recip_cluster) {
my($s) = $cluster_full_spam_status; $s =~ s/\n[ \t]/ /g;
ll(2) && do_log(2,sprintf("SPAM-TAG, %s -> %s, %s",
qquote_rfc2821_local($msginfo->sender),
join(',', qquote_rfc2821_local(
map { $_->recip_addr } @recip_cluster)), $s));
}
($hdr_edits, \@recip_cluster, $done_all);
}
sub do_quarantine($$$$$;$) {
my($conn,$msginfo,$hdr_edits,$recips_ref,$quarantine_method,$snmp_id) = @_;
if ($quarantine_method eq '') { do_log(5, "quarantine disabled") }
else {
$quarantine_method =~ s/%b/$msginfo->body_digest/eg;
my($sender) = $msginfo->sender;
my($quar_msg) = Amavis::In::Message->new;
$quar_msg->rx_time($msginfo->rx_time); $quar_msg->delivery_method($quarantine_method);
if ($quarantine_method =~ /^bsmtp:/i) {
$quar_msg->sender($sender); $quar_msg->recips($msginfo->recips);
} else {
my($mftq) = c('mailfrom_to_quarantine');
$quar_msg->sender(defined $mftq ? $mftq : $sender);
$quar_msg->recips($recips_ref); $hdr_edits->prepend_header('X-Envelope-To',
join(",\n ", qquote_rfc2821_local(@{$msginfo->recips})), 1);
$hdr_edits->prepend_header('X-Envelope-From',
qquote_rfc2821_local($sender));
}
do_log(5, "DO_QUARANTINE, sender: " . $quar_msg->sender);
$quar_msg->auth_submitter(qquote_rfc2821_local($quar_msg->sender));
$quar_msg->auth_user(c('amavis_auth_user'));
$quar_msg->auth_pass(c('amavis_auth_pass'));
$quar_msg->header_edits($hdr_edits);
$quar_msg->mail_text($msginfo->mail_text);
snmp_count('QuarMsgs');
mail_dispatch($conn, $quar_msg, 1);
my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
one_response_for_all($quar_msg, 0, am_id()); if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { snmp_count($snmp_id eq '' ? 'QuarOther' : $snmp_id);
} elsif ($n_smtp_resp =~ /^4/) {
snmp_count('QuarAttemptTempFails');
die "temporarily unable to quarantine: $n_smtp_resp";
} else { snmp_count('QuarAttemptFails');
die "Can not quarantine: $n_smtp_resp";
}
my(@qa); my(%seen); for my $r (@{$quar_msg->per_recip_data}) {
my($mbxname) = $r->recip_mbxname;
$mbxname = $r->recip_final_addr if !defined($mbxname);
push(@qa,$mbxname) if $mbxname ne '' && !$seen{$mbxname}++;
}
$msginfo->quarantined_to(\@qa); do_log(5, "DO_QUARANTINE done");
}
}
sub do_virus($$) {
my($conn, $msginfo) = @_;
my($q_method, $quarantine_to_maps_ref, $bypass_checks_maps_ref) =
@virusname ?
(c('virus_quarantine_method'),
ca('virus_quarantine_to_maps'),
ca('bypass_virus_checks_maps'))
: @banned_filename ?
(c('banned_files_quarantine_method'),
ca('banned_quarantine_to_maps'),
ca('bypass_banned_checks_maps'))
: @bad_headers ?
(c('bad_header_quarantine_method'),
ca('bad_header_quarantine_to_maps'),
ca('bypass_header_checks_maps'))
: (undef, undef, undef);
$VIRUSFILE = $q_method =~ /^(?:local|bsmtp):(.*)\z/si ? $1 : "virus-%i-%n";
$VIRUSFILE =~ s{%(.)}
{ $1 eq 'b' ? $msginfo->body_digest
: $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time))
: $1 eq 'n' ? am_id()
: $1 eq '%' ? '%' : '%'.$1 }egs;
do_log(5, "do_virus: looking for per-recipient quarantine and admins");
my(@q_addr,@a_addr); for my $r (@{$msginfo->per_recip_data}) {
my($rec) = $r->recip_addr;
my($q); ($q) = lookup(0,$rec, @$quarantine_to_maps_ref);
$q = $rec if $q ne '' && $q_method =~ /^bsmtp:/i; my($a) = lookup(0,$rec, @{ca('virus_admin_maps')});
push(@q_addr, $q) if $q ne '' && !grep {$_ eq $q} @q_addr;
push(@a_addr, $a) if $a ne '' && !grep {$_ eq $a} @a_addr;
}
if (@q_addr) { my($hdr_edits) = Amavis::Out::EditHeader->new;
$hdr_edits->prepend_header('X-Quarantine-Id', "<$VIRUSFILE>");
if (@virusname) {
$hdr_edits->append_header('X-Amavis-Alert',
"INFECTED, message contains virus:\n " . join(",\n ", @virusname), 1);
}
if (@banned_filename) {
my(@b) = @banned_filename>3 ? @banned_filename[0..2] : @banned_filename;
my($msg) = "BANNED, message contains "
. (@banned_filename==1 ? 'part' : 'parts') . ":\n "
. join(",\n ", @b) . (@banned_filename > @b ? ", ..." : "");
$hdr_edits->append_header('X-Amavis-Alert', $msg, 1);
}
if (@bad_headers) {
$hdr_edits->append_header('X-Amavis-Alert',
'BAD HEADER '.$bad_headers[0], 1);
}
do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method,
@virusname ? 'QuarVirusMsgs' :
@banned_filename ? 'QuarBannedMsgs' :
@bad_headers ? 'QuarBadHMsgs' : 'QuarOther');
}
my($hdr_edits) = Amavis::Out::EditHeader->new;
do_log(4, "Skip virus_admin notification, no admin specified") if !@a_addr;
for my $admin (@a_addr) { do_log(5, "DO_VIRUS - NOTIFICATIONS to $admin; sender: ".$msginfo->sender);
my($notification) = Amavis::In::Message->new;
$notification->rx_time($msginfo->rx_time); $notification->delivery_method(c('notify_method'));
$notification->sender(c('mailfrom_notify_admin'));
$notification->auth_submitter(
qquote_rfc2821_local(c('mailfrom_notify_admin')));
$notification->auth_user(c('amavis_auth_user'));
$notification->auth_pass(c('amavis_auth_pass'));
$notification->recips([$admin]);
my(%mybuiltins) = %builtins; $mybuiltins{'T'} = [quote_rfc2821_local($admin)]; $mybuiltins{'f'} = c('hdrfrom_notify_admin'); $notification->mail_text(
string_to_mime_entity(expand(cr('notify_virus_admin_templ'),
\%mybuiltins)));
$notification->header_edits($hdr_edits);
mail_dispatch($conn, $notification, 1);
my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
one_response_for_all($notification, 0, am_id()); if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { } elsif ($n_smtp_resp =~ /^4/) {
die "temporarily unable to notify virus admin: $n_smtp_resp";
} else {
do_log(-1, "FAILED to notify virus admin: $n_smtp_resp");
}
}
my(@recips_to_notify) =
grep {
@virusname && !lookup(0,$_,@{ca('bypass_virus_checks_maps')}) ?
scalar(lookup(0,$_,@{ca('warnvirusrecip_maps')}))
: @banned_filename && !lookup(0,$_,@{ca('bypass_banned_checks_maps')}) ?
scalar(lookup(0,$_,@{ca('warnbannedrecip_maps')}))
: @bad_headers && !lookup(0,$_,@{ca('bypass_header_checks_maps')}) ?
scalar(lookup(0,$_,@{ca('warnbadhrecip_maps')}))
: 0 }
grep { c('warn_offsite') || lookup(0,$_,@{ca('local_domains_maps')}) }
@{$msginfo->recips};
if (!@recips_to_notify) {
do_log(5,"do_virus: recipient notifications not required");
} else {
my($notification) = Amavis::In::Message->new;
$notification->rx_time($msginfo->rx_time); $notification->delivery_method(c('notify_method'));
$notification->sender(c('mailfrom_notify_recip'));
$notification->auth_submitter(
qquote_rfc2821_local(c('mailfrom_notify_recip')));
$notification->auth_user(c('amavis_auth_user'));
$notification->auth_pass(c('amavis_auth_pass'));
$notification->recips(\@recips_to_notify);
my(%mybuiltins) = %builtins; $mybuiltins{'f'} = c('hdrfrom_notify_recip'); $mybuiltins{'T'} = (@recips_to_notify==1 && $recips_to_notify[0] ne '') ? [quote_rfc2821_local($recips_to_notify[0])] : undef;
$notification->mail_text(
string_to_mime_entity(expand(cr('notify_virus_recips_templ'),
\%mybuiltins)) );
$notification->header_edits($hdr_edits);
mail_dispatch($conn, $notification, 1);
my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
one_response_for_all($notification, 0, am_id()); if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { } elsif ($n_smtp_resp =~ /^4/) {
die "temporarily unable to notify recipients: $n_smtp_resp";
} else {
do_log(-1, "FAILED to notify recipients: $n_smtp_resp");
}
}
do_log(5, "DO_VIRUS - DONE");
}
sub do_spam($$) {
my($conn, $msginfo) = @_;
my($q_method) = c('spam_quarantine_method');
$VIRUSFILE = $q_method =~ /^(?:local|bsmtp):(.*)\z/si ? $1 : "spam-%b-%i-%n";
$VIRUSFILE =~ s{%(.)}
{ $1 eq 'b' ? $msginfo->body_digest
: $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time))
: $1 eq 'n' ? am_id()
: $1 eq '%' ? '%' : '%'.$1 }egs;
my($tag_level) =
min(map { scalar(lookup(0,$_,@{ca('spam_tag_level_maps')})) } @{$msginfo->recips});
my($tag2_level) =
min(map { scalar(lookup(0,$_,@{ca('spam_tag2_level_maps')})) } @{$msginfo->recips});
my($kill_level) =
min(map { scalar(lookup(0,$_,@{ca('spam_kill_level_maps')})) } @{$msginfo->recips});
my($blacklisted) =
scalar(grep { $_->recip_blacklisted_sender } @{$msginfo->per_recip_data});
my($whitelisted) =
scalar(grep { $_->recip_whitelisted_sender } @{$msginfo->per_recip_data});
my($s) = $spam_status; $s =~ s/,/,\n /g; my($full_spam_status) = sprintf(
"%s,\n hits=%s\n tag=%s\n tag2=%s\n kill=%s\n %s%s",
(defined $spam_level && defined $tag2_level && $spam_level>=$tag2_level ?
'Yes' : 'No'),
(map { !defined $_ ? 'x' : 0+sprintf("%.3f",$_) }
($spam_level, $tag_level, $tag2_level, $kill_level)),
join('', $blacklisted ? "BLACKLISTED\n " : (),
$whitelisted ? "WHITELISTED\n " : ()),
$s);
do_log(5, "do_spam: looking for a quarantine address");
my(@q_addr,@a_addr); my($sqbsm) = ca('spam_quarantine_bysender_to_maps');
if (@$sqbsm) { my($a); $a = lookup(0,$msginfo->sender, @$sqbsm);
push(@q_addr, $a) if $a ne '';
}
for my $r (@{$msginfo->per_recip_data}) {
my($rec) = $r->recip_addr;
my($q); ($q) = lookup(0,$rec, @{ca('spam_quarantine_to_maps')});
$q = $rec if $q ne '' && $q_method =~ /^bsmtp:/i; my($a) = lookup(0,$rec, @{ca('spam_admin_maps')});
push(@q_addr, $q) if $q ne '' && !grep {$_ eq $q} @q_addr;
push(@a_addr, $a) if $a ne '' && !grep {$_ eq $a} @a_addr;
}
if (@q_addr) { my($hdr_edits) = Amavis::Out::EditHeader->new;
$hdr_edits->prepend_header('X-Quarantine-Id', "<$VIRUSFILE>");
$hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
my($slc) = c('sa_spam_level_char');
$hdr_edits->append_header('X-Spam-Level',
$slc x min(0+$spam_level,64)) if $slc ne '';
$hdr_edits->append_header('X-Spam-Report', $spam_report,1)
if c('sa_spam_report_header') && $spam_report ne '';
do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method,'QuarSpamMsgs');
}
$s = $full_spam_status; $s =~ s/\n[ \t]/ /g;
do_log(2,sprintf("SPAM, <%s> -> %s, %s%s", $msginfo->sender_source,
join(',', qquote_rfc2821_local(@{$msginfo->recips})), $s,
!@q_addr ? '' : sprintf(", quarantine %s (%s)",
$VIRUSFILE, join(',', @q_addr)) ));
do_log(4, "Skip spam_admin notification, no admin specified") if !@a_addr;
for my $admin (@a_addr) { do_log(5, "DO_SPAM - NOTIFICATIONS to $admin; sender: ".$msginfo->sender);
my($notification) = Amavis::In::Message->new;
$notification->rx_time($msginfo->rx_time); $notification->delivery_method(c('notify_method'));
$notification->sender(c('mailfrom_notify_spamadmin'));
$notification->auth_submitter(
qquote_rfc2821_local(c('mailfrom_notify_spamadmin')));
$notification->auth_user(c('amavis_auth_user'));
$notification->auth_pass(c('amavis_auth_pass'));
$notification->recips([$admin]);
my(%mybuiltins) = %builtins; $mybuiltins{'T'} = [quote_rfc2821_local($admin)]; $mybuiltins{'f'} = c('hdrfrom_notify_spamadmin');
$notification->mail_text(
string_to_mime_entity(expand(cr('notify_spam_admin_templ'),
\%mybuiltins)));
my($hdr_edits) = Amavis::Out::EditHeader->new;
$notification->header_edits($hdr_edits);
mail_dispatch($conn, $notification, 1);
my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
one_response_for_all($notification, 0, am_id()); if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { } elsif ($n_smtp_resp =~ /^4/) {
die "temporarily unable to notify spam admin: $n_smtp_resp";
} else {
do_log(-1, "FAILED to notify spam admin: $n_smtp_resp");
}
}
do_log(5, "DO_SPAM DONE");
}
sub get_body_digest($$) {
my($fh, $msginfo) = @_;
$fh->seek(0,0) or die "Can't rewind mail file: $!";
local($_);
my($ctx) = Digest::MD5->new;
my(@orig_header);
my($header_size) = 0;
my($body_size) = 0;
while (<$fh>) { last if $_ eq $eol;
$header_size += length($_);
push(@orig_header, $_); }
my($len);
while (($len = read($fh,$_,16384)) > 0)
{ $ctx->add($_); $body_size += $len }
my($signature) = $ctx->hexdigest;
$signature = untaint($signature) if $signature =~ /^ [0-9a-fA-F]{32} (?: [0-9a-fA-F]{8} )? \z/x;
$msginfo->orig_header(\@orig_header);
$msginfo->orig_header_size($header_size);
$msginfo->orig_body_size($body_size);
$msginfo->body_digest($signature);
section_time('body_hash');
do_log(3, "body hash: $signature");
$signature;
}
sub find_program_path($$$) {
my($fv_list, $path_list_ref, $may_log) = @_;
$fv_list = [$fv_list] if !ref $fv_list;
my($found);
for my $fv (@$fv_list) {
my(@fv_cmd) = split(' ',$fv);
if (!@fv_cmd) { } elsif ($fv_cmd[0] =~ /^\//) { # absolute path
my($errn) = stat($fv_cmd[0]) ? 0 : 0+$!;
if ($errn == ENOENT) { }
elsif ($errn) {
do_log(-1, "find_program_path: " . "$fv_cmd[0] inaccessible: $!")
if $may_log;
} elsif (-x _ && !-d _) { $found = join(' ', @fv_cmd) }
} elsif ($fv_cmd[0] =~ /\//) { # relative path
die "find_program_path: relative paths not implemented: @fv_cmd\n";
} else { for my $p (@$path_list_ref) {
my($errn) = stat("$p/$fv_cmd[0]") ? 0 : 0+$!;
if ($errn == ENOENT) { }
elsif ($errn) {
do_log(-1, "find_program_path: " . "$p/$fv_cmd[0] inaccessible: $!")
if $may_log;
} elsif (-x _ && !-d _) {
$found = $p . '/' . join(' ', @fv_cmd);
last;
}
}
}
last if defined $found;
}
$found;
}
sub find_external_programs($) {
my($path_list_ref) = @_;
for my $f (qw($file $arc $gzip $bzip2 $lzop $lha $unarj $uncompress
$unfreeze $unrar $zoo $cpio $ar $rpm2cpio $cabextract $dspam))
{
my($g) = $f;
$g =~ s/\$/Amavis::Conf::/;
my($fv_list) = eval('$' . $g);
my($found) = find_program_path($fv_list, $path_list_ref, 1);
{ no strict 'refs'; $$g = $found } if (!defined $found) {
do_log(-1, sprintf("No %-14s not using it", "$f,"));
} else {
do_log(0,sprintf("Found %-11s at %s%s", $f,
$daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
$found));
}
}
my($tier) = 'primary'; for my $f (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) {
if ($f eq "\000") { $tier = 'secondary';
} elsif (!defined $f || !ref $f) { } elsif (ref($f->[1]) eq 'CODE') {
do_log(0, "Using internal av scanner code for ($tier) " . $f->[0]);
} else {
my($found) = $f->[1] = find_program_path($f->[1], $path_list_ref, 1);
if (!defined $found) {
do_log(3, "No $tier av scanner: " . $f->[0]);
$f = undef; } else {
do_log(0,sprintf("Found $tier av scanner %-11s at %s%s", $f->[0],
$daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
$found));
}
}
}
}
sub fetch_modules_extra() {
my(@modules);
if ($extra_code_sql) {
push(@modules, 'DBI');
for (@lookup_sql_dsn) {
my(@dsn) = split(/:/,$_->[0],-1);
push(@modules, 'DBD::'.$dsn[1]) if uc($dsn[0]) eq 'DBI';
}
}
push(@modules, qw(Net::LDAP Net::LDAP::Util)) if $extra_code_ldap;
if (c('bypass_decode_parts') &&
!grep {exists $policy_bank{$_}{'bypass_decode_parts'} &&
!$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) {
} else {
push(@modules, qw(Compress::Zlib Convert::TNEF Convert::UUlib
Archive::Zip Archive::Tar));
}
push(@modules, 'Mail::SpamAssassin') if $extra_code_antispam;
push(@modules, 'Authen::SASL') if c('auth_required_out');
Amavis::Boot::fetch_modules('REQUIRED ADDITIONAL MODULES', 1, @modules);
my($sa_version);
$sa_version = Mail::SpamAssassin::Version() if $extra_code_antispam;
@modules = (); if ($extra_code_antispam) { push(@modules, qw(
Mail::SpamAssassin::Locker::UnixNFSSafe
Mail::SpamAssassin::DBBasedAddrList
Mail::SpamAssassin::BayesStore::DBM
Mail::SpamAssassin::BayesStore::SQL
Mail::SpamAssassin::Plugin::SPF
Mail::SpamAssassin::Plugin::URIDNSBL
Mail::SpamAssassin::Plugin::Hashcash
Mail::SpamAssassin::Plugin::Razor2
Mail::SpamAssassin::UnixLocker
Mail::SpamAssassin::PerMsgLearner
Mail::SpamAssassin::BayesStoreDBM
Mail::SPF::Query Net::CIDR::Lite
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::TXT Net::Ping bytes
));
}
if ($extra_code_antispam && defined $sa_version && $sa_version < 3) {
push(@modules, qw(
Mail::SpamAssassin::SpamCopURI
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::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::isbn URI::urn::oid
URI::file URI::file::Base URI::file::Unix URI::file::Win32
));
}
Amavis::Boot::fetch_modules('PRE-COMPILE OPTIONAL MODULES', 0,
@modules) if @modules;
if ($extra_code_antivirus) {
my($savi_obj, $savi_module_ok, $clamav_module_ok);
for my $entry (@{ca('av_scanners')}, @{ca('av_scanners_backup')}) {
if (ref($entry) ne 'ARRAY') { } elsif ($entry->[1] eq \&ask_sophos_savi ||
$entry->[1] eq \&sophos_savi ||
$entry->[0] eq 'Sophos SAVI') {
if (!defined($savi_module_ok)) {
$savi_module_ok = eval { require SAVI };
$savi_module_ok = 0 if !defined $savi_module_ok;
}
if (!$savi_module_ok) { $entry->[1] = undef } else { $entry->[2] = $savi_obj if defined $savi_obj } } elsif ($entry->[1] eq \&ask_clamav ||
$entry->[0] =~ /^Mail::ClamAV/) {
if (!defined($clamav_module_ok)) {
$clamav_module_ok = eval { require Mail::ClamAV };
$clamav_module_ok = 0 if !defined $clamav_module_ok;
}
$entry->[1] = undef if !$clamav_module_ok; }
}
}
}
$Amavis::Conf::notify_spam_admin_templ = ''; $Amavis::Conf::notify_spam_recips_templ = ''; do { local($/) = "__DATA__\n"; chomp($_ = <Amavis::DATA>) for (
$extra_code_db, $extra_code_cache,
$extra_code_sql, $extra_code_ldap,
$extra_code_in_amcl, $extra_code_in_smtp,
$extra_code_antivirus, $extra_code_antispam, $extra_code_unpackers,
$Amavis::Conf::log_templ, $Amavis::Conf::log_recip_templ);
if ($unicode_aware) {
}
chomp($_ = <Amavis::DATA>) for (
$Amavis::Conf::notify_sender_templ,
$Amavis::Conf::notify_virus_sender_templ,
$Amavis::Conf::notify_virus_admin_templ,
$Amavis::Conf::notify_virus_recips_templ,
$Amavis::Conf::notify_spam_sender_templ,
$Amavis::Conf::notify_spam_admin_templ );
}; close(\*Amavis::DATA) or die "Can't close *Amavis::DATA: $!";
$Amavis::Conf::log_templ = $1
if $Amavis::Conf::log_templ=~/^(.*?)[\r\n]+\z/s;
$Amavis::Conf::log_recip_templ = $1
if $Amavis::Conf::log_recip_templ=~/^(.*?)[\r\n]+\z/s;
my($config_file) = '/etc/amavisd.conf';
my($desired_group); my($desired_user); if ($> != 0) { $desired_user = $> }
while (@ARGV >= 2 && $ARGV[0] =~ /^-[ugc]\z/) {
my($opt) = shift @ARGV;
if ($opt eq '-u') { my($val) = shift @ARGV;
if ($> == 0) { $desired_user = $val }
else { print STDERR "Ignoring option -u when not running as root\n" }
} elsif ($opt eq '-g') { my($val) = shift @ARGV;
if ($> == 0) { $desired_group = $val }
else { print STDERR "Ignoring option -g when not running as root\n" }
} elsif ($opt eq '-c') { $config_file = shift @ARGV;
$config_file = untaint($config_file)
if $config_file =~ m{^[A-Za-z0-9/._=+-]+\z};
}
}
if (defined $desired_user && ($> == 0 || $< == 0)) { my($username,$passwd,$uid,$gid) =
$desired_user=~/^(\d+)$/ ? (undef,undef,$1,undef) :getpwnam($desired_user);
defined $uid or die "No such username: $desired_user\n";
if ($desired_group eq '') { $desired_group = $gid } else { $gid = $desired_group=~/^(\d+)$/ ? $1 : getgrnam($desired_group) }
defined $gid or die "No such group: $desired_group\n";
$( = $gid; $) = "$gid $gid"; POSIX::setuid($uid) or die "Can't setuid to $uid: $!";
$> = $uid; $< = $uid; $> != 0 or die "Still running as root, aborting\n";
$< != 0 or die "Effective UID changed, but Real UID is 0\n";
}
umask(0027);
Amavis::Conf::build_default_maps();
Amavis::Conf::read_config($config_file);
if (defined $desired_user && $daemon_user ne '') {
my($username,$passwd,$uid,$gid) =
$daemon_user=~/^(\d+)$/ ? (undef,undef,$1,undef) : getpwnam($daemon_user);
$uid == $> or warn sprintf(
"WARN: running under user '%s' (UID=%s), the config file".
" specifies \$daemon_user='%s' (UID=%s)\n",
$desired_user, $>, $daemon_user, defined $uid ? $uid : '?');
}
if (!$enable_db) { $extra_code_db = undef }
else {
eval $extra_code_db or die "Problem in Amavis::DB or Amavis::DB::SNMP code: $@";
$extra_code_db = 1; }
if (!$enable_global_cache || !$extra_code_db) { $extra_code_cache = undef }
else {
eval $extra_code_cache or die "Problem in the Amavis::Cache code: $@";
$extra_code_cache = 1; }
if (!@lookup_sql_dsn) { $extra_code_sql = undef }
else {
eval $extra_code_sql or die "Problem in the Lookup::SQL code: $@";
$extra_code_sql = 1; }
if (!$enable_ldap) { $extra_code_ldap = undef }
else {
eval $extra_code_ldap or die "Problem in the Lookup::LDAP code: $@";
$extra_code_ldap = 1; }
if (c('protocol') eq 'COURIER') {
die "In::Courier code not available";
} elsif (c('protocol') eq 'AM.PDP' || $unix_socketname ne '') {
eval $extra_code_in_amcl or die "Problem in the In::AMCL code: $@";
$extra_code_in_amcl = 1; } else {
$extra_code_in_amcl = undef;
}
if (c('protocol') eq 'QMQPqq') { die "In::QMQPqq code not available";
} elsif (c('protocol') =~ /^(SMTP|LMTP)\z/ ||
$inet_socket_port ne '' &&
(!ref $inet_socket_port || @$inet_socket_port)) { eval $extra_code_in_smtp or die "Problem in the In::SMTP code: $@";
$extra_code_in_smtp = 1; } else {
$extra_code_in_smtp = undef;
}
my($bpvcm) = ca('bypass_virus_checks_maps');
if (!@{ca('av_scanners')} && !@{ca('av_scanners_backup')}) {
$extra_code_antivirus = undef;
} elsif (@$bpvcm && !ref($bpvcm->[0]) && $bpvcm->[0]) {
$extra_code_antivirus = undef;
} else {
eval $extra_code_antivirus or die "Problem in the antivirus code: $@";
$extra_code_antivirus = 1; }
if (!$extra_code_antivirus) { @Amavis::Conf::av_scanners = @Amavis::Conf::av_scanners_backup = () }
my($bpscm) = ca('bypass_spam_checks_maps');
if (@$bpscm && !ref($bpscm->[0]) && $bpscm->[0]) {
$extra_code_antispam = undef;
} else {
eval $extra_code_antispam or die "Problem in the antispam code: $@";
$extra_code_antispam = 1; }
if (c('bypass_decode_parts') &&
!grep {exists $policy_bank{$_}{'bypass_decode_parts'} &&
!$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) {
$extra_code_unpackers = undef;
} else {
eval $extra_code_unpackers or die "Problem in the Amavis::Unpackers code: $@";
$extra_code_unpackers = 1; }
my($cmd) = lc($ARGV[0]);
if ($cmd =~ /^(start|debug|debug-sa|foreground)?\z/) {
$DEBUG=1 if $cmd eq 'debug';
$daemonize=0 if $cmd eq 'foreground';
$daemonize=0, $sa_debug=1 if $cmd eq 'debug-sa';
} elsif ($cmd !~ /^(reload|stop)\z/) {
die "$myversion: Unknown argument. Usage:\n $0 [-u user] [-g group] [-c config-file] ( [start] | stop | reload | debug | debug-sa | foreground )\n";
} else { eval { $pid_file ne '' or die "Config parameter \$pid_file not defined";
my($errn) = stat($pid_file) ? 0 : 0+$!;
$errn != ENOENT or die "No PID file $pid_file\n";
$errn == 0 or die "PID file $pid_file inaccessible: $!";
my($amavisd_pid);
open(PID_FILE, "< $pid_file\0") or die "Can't read file $pid_file: $!";
while (<PID_FILE>) { chomp; $amavisd_pid = $_ if /^\d+\z/ }
close(PID_FILE) or die "Can't close file $pid_file: $!";
defined($amavisd_pid) or die "Invalid PID in the $pid_file";
$amavisd_pid = untaint($amavisd_pid);
kill('TERM',$amavisd_pid) or die "Can't SIGTERM amavisd[$amavisd_pid]: $!";
my($delay) = 1; for (;;) {
sleep($delay); $delay = 5;
last if !kill(0,$amavisd_pid); print STDERR "Waiting for the process $amavisd_pid to terminate\n";
}
};
if ($@ ne '') { chomp($@); die "$@, can't $cmd the process\n" }
exit 0 if $cmd eq 'stop';
print STDERR "daemon terminated, waiting for the dust to settle...\n";
sleep 5; print STDERR "becoming a new daemon...\n";
}
$daemonize = 0 if $DEBUG;
$ENV{PATH} = $path if $path ne '';
$ENV{HOME} = $helpers_home if $helpers_home ne '';
$ENV{TERM} = 'dumb'; $ENV{COLUMNS} = '80'; $ENV{LINES} = '100';
Amavis::Log::init("amavis", !1, $DO_SYSLOG, $SYSLOG_LEVEL, $LOGFILE);
do_log(1, "user=$desired_user, EUID: $> ($<); group=$desired_group, EGID: $) ($()");
do_log(0, "Perl version $]");
fetch_modules_extra();
my $server = bless {
server => {
commandline => [],
port => [ ($unix_socketname eq '' ? () : "$unix_socketname|unix"), map { "$_/tcp" } (ref $inet_socket_port ? @$inet_socket_port
: $inet_socket_port ne '' ? $inet_socket_port : () ),
],
host => ($inet_socket_bind eq '' ? '*' : $inet_socket_bind),
max_servers => $max_servers, max_requests => $max_requests, user => (($> == 0 || $< == 0) ? $daemon_user : undef),
group => (($> == 0 || $< == 0) ? $daemon_group : undef),
pid_file => $pid_file,
lock_file => $lock_file, background => $daemonize ? 1 : undef,
setsid => $daemonize ? 1 : undef,
chroot => $daemon_chroot_dir ne '' ? $daemon_chroot_dir : undef,
no_close_by_child => 1,
log_level => ($DEBUG ? 4 : 2),
log_file => undef, },
}, 'Amavis';
$0 = 'amavisd (master)';
$server->run;
exit 1;
__DATA__
package Amavis::DB::SNMP;
use strict;
use re 'taint';
BEGIN {
import Amavis::Conf qw($myversion $myhostname);
import Amavis::Util qw(ll do_log snmp_counters_get);
}
use BerkeleyDB;
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
sub new {
my($class,$db_env) = @_; my($env) = $db_env->get_db_env;
defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!.";
my($dbs) = BerkeleyDB::Hash->new(-Filename=>'snmp.db', -Env=>$env);
defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!.";
my($dbn) = BerkeleyDB::Hash->new(-Filename=>'nanny.db', -Env=>$env);
defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!.";
bless { 'db_snmp'=>$dbs, 'db_nanny'=>$dbn }, $class;
}
sub DESTROY {
my($self) = shift;
eval { do_log(5,"Amavis::DB::SNMP called") };
for my $db ($self->{'db_snmp'}, $self->{'db_nanny'}) {
if (defined $db) {
eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." };
if ($@ ne '') { warn "BDB S+N DESTROY $@" }
$db = undef;
}
}
}
sub put_initial_snmp_data($) {
my($db) = @_;
my($cursor) = $db->db_cursor(DB_WRITECURSOR);
defined $cursor or die "BDB S db_cursor: $BerkeleyDB::Error, $!.";
for my $obj (['sysDescr', 'STR', $myversion],
['sysObjectID', 'OID', '1.3.6.1.4.1.15312.2.1'],
['sysUpTime', 'INT', int(time)],
['sysContact', 'STR', ''],
['sysName', 'STR', $myhostname],
['sysLocation', 'STR', ''],
['sysServices', 'INT', 64], ) {
my($key,$type,$val) = @$obj;
$cursor->c_put($key, sprintf("%s %s",$type,$val), DB_KEYLAST) == 0
or die "BDB S c_put: $BerkeleyDB::Error, $!.";
};
$cursor->c_close==0 or die "BDB S c_close: $BerkeleyDB::Error, $!.";
}
sub update_counters {
my($self) = @_;
my($counter_names_ref) = snmp_counters_get();
my($eval_stat,$interrupt); $interrupt = '';
if (defined $counter_names_ref && @$counter_names_ref) {
my($db) = $self->{'db_snmp'}; my($cursor);
my($h1) = sub { $interrupt = $_[0] };
local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
eval { $cursor = $db->db_cursor(DB_WRITECURSOR); defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
for my $key (@$counter_names_ref) {
my($counter_name,$counter_incr) = ref($key) ? @$key : ($key,1);
my($val,$flags); my($type) = 'C32';
my($stat) = $cursor->c_get($counter_name,$val,DB_SET);
if ($stat==0) { if ($val =~ /^\Q$type\E (\d+)\z/o) { $val = $1 }
else { do_log(-2,"WARN: counter syntax? $val, clearing"); $val = 0 }
$flags = DB_CURRENT; $val = $val+$counter_incr;
} else { $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
$flags = DB_KEYLAST; $val = $counter_incr;
}
$cursor->c_put($counter_name, sprintf("%s %010d",$type,$val),$flags)==0
or die "c_put: $BerkeleyDB::Error, $!.";
}
$cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
$cursor = undef;
};
$eval_stat = $@;
if (defined $db) {
$cursor->c_close if defined $cursor; $cursor = undef;
if ($eval_stat eq '') {
}
}
}
delete $self->{'cnt'};
if ($interrupt ne '') { kill($interrupt,$$) } elsif ($eval_stat ne '')
{ chomp($eval_stat); die "update_counters: BDB S $eval_stat\n" }
}
sub register_proc {
my($self,$task_id) = @_;
my($db) = $self->{'db_nanny'}; my($cursor);
my($val,$new_val); my($key) = sprintf("%05d",$$);
$new_val = sprintf("%010d %-12s", time, $task_id) if defined $task_id;
my($eval_stat,$interrupt); $interrupt = '';
my($h1) = sub { $interrupt = $_[0] };
local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
eval { $cursor = $db->db_cursor(DB_WRITECURSOR); defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
my($stat) = $cursor->c_get($key,$val,DB_SET);
$stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
if ($stat==0 && !defined $task_id) { $cursor->c_del==0 or die "c_del: $BerkeleyDB::Error, $!.";
} elsif (defined $task_id && !($stat==0 && $new_val eq $val)) {
$cursor->c_put($key, $new_val,
$stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0
or die "c_put: $BerkeleyDB::Error, $!.";
}
$cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
$cursor = undef;
};
$eval_stat = $@;
if (defined $db) {
$cursor->c_close if defined $cursor; $cursor = undef;
if ($eval_stat eq '') {
}
}
if ($interrupt ne '') { kill($interrupt,$$) } elsif ($eval_stat ne '')
{ chomp($eval_stat); die "register_proc: BDB N $eval_stat\n" }
}
1;
package Amavis::DB;
use strict;
use re 'taint';
BEGIN {
import Amavis::Conf qw($db_home $daemon_chroot_dir);
import Amavis::Util qw(untaint ll do_log);
}
use BerkeleyDB;
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
sub init($) {
my($predelete) = @_; my($name) = $db_home;
$name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne '';
if ($predelete) { local(*DIR); my($f);
opendir(DIR,$db_home) or die "Can't open directory $name: $!";
while (defined($f = readdir(DIR))) {
next if ($f eq '.' || $f eq '..') && -d _;
if ($f =~ /^(__db\.\d+|(cache-expiry|cache|snmp|nanny)\.db)\z/s) {
$f = untaint($f);
unlink("$db_home/$f") or die "Can't delete file $name/$f: $!";
}
}
closedir(DIR) or die "Can't close directory $name: $!";
}
my($env) = BerkeleyDB::Env->new(-Home=>$db_home, -Mode=>0640,
-Flags=> DB_CREATE | DB_INIT_CDB | DB_INIT_MPOOL);
defined $env or die "BDB bad db env. at $db_home: $BerkeleyDB::Error, $!.";
do_log(0, sprintf("Creating db in %s/; BerkeleyDB %s, libdb %s",
$name, BerkeleyDB->VERSION, $BerkeleyDB::db_version));
my($dbc) = BerkeleyDB::Hash->new(
-Filename=>'cache.db', -Flags=>DB_CREATE, -Env=>$env );
defined $dbc or die "BDB no dbC: $BerkeleyDB::Error, $!.";
my($dbq) = BerkeleyDB::Queue->new(
-Filename=>'cache-expiry.db', -Flags=>DB_CREATE, -Env=>$env,
-Len=>15+1+32 ); defined $dbq or die "BDB no dbQ: $BerkeleyDB::Error, $!.";
my($dbs) = BerkeleyDB::Hash->new(
-Filename=>'snmp.db', -Flags=>DB_CREATE, -Env=>$env );
defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!.";
my($dbn) = BerkeleyDB::Hash->new(
-Filename=>'nanny.db', -Flags=>DB_CREATE, -Env=>$env );
defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!.";
Amavis::DB::SNMP::put_initial_snmp_data($dbs);
for my $db ($dbc, $dbq, $dbs, $dbn)
{ $db->db_close==0 or die "BDB db_close: $BerkeleyDB::Error, $!." }
}
sub new {
my($class) = @_; my($env);
if (defined $db_home) {
$env = BerkeleyDB::Env->new(
-Home=>$db_home, -Mode=>0640, -Flags=> DB_INIT_CDB | DB_INIT_MPOOL);
defined $env or die "BDB bad db env. at $db_home: $BerkeleyDB::Error, $!.";
}
bless \$env, $class;
}
sub get_db_env { my($self) = shift; $$self }
1;
__DATA__
package Amavis::Cache;
use strict;
use re 'taint';
BEGIN {
import Amavis::Util qw(ll do_log);
}
use BerkeleyDB;
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.0332';
@ISA = qw(Exporter);
}
sub new {
my($class,$db_env) = @_;
my($dbc,$dbq,$mem_cache);
if (!defined($db_env)) {
do_log(1,"BerkeleyDB not available, using memory-based local cache");
$mem_cache = {};
} else {
my($env) = $db_env->get_db_env;
defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!.";
$dbc = BerkeleyDB::Hash->new(-Filename=>'cache.db', -Env=>$env);
defined $dbc or die "BDB no dbC: $BerkeleyDB::Error, $!.";
$dbq = BerkeleyDB::Queue->new(-Filename=>'cache-expiry.db', -Env=>$env,
-Len=>15+1+32); defined $dbq or die "BDB no dbQ: $BerkeleyDB::Error, $!.";
}
bless {'db_cache'=>$dbc, 'db_queue'=>$dbq, 'mem_cache'=>$mem_cache}, $class;
}
sub DESTROY {
my($self) = shift;
eval { do_log(5,"Amavis::Cache called") };
for my $db ($self->{'db_cache'}, $self->{'db_queue'}) {
if (defined $db) {
eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." };
if ($@ ne '') { warn "BDB C+Q DESTROY $@" }
$db = undef;
}
}
}
sub enqueue {
my($self,$str,$now_utc_iso8601,$expires_utc_iso8601) = @_;
my($db) = $self->{'db_cache'}; my($dbq) = $self->{'db_queue'};
local($1,$2); my($stat,$key,$val); $key = '';
my($qcursor) = $dbq->db_cursor(DB_WRITECURSOR);
defined $qcursor or die "BDB Q db_cursor: $BerkeleyDB::Error, $!.";
while ( ($stat=$qcursor->c_get($key,$val,DB_NEXT)) == 0 ) {
if ($val !~ /^([^ ]+) (.*)\z/s) {
do_log(-2,"WARN: queue head invalid, deleting: $val");
} else {
my($t,$digest) = ($1,$2);
last if $t ge $now_utc_iso8601;
my($cursor) = $db->db_cursor(DB_WRITECURSOR);
defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!.";
my($v); my($st1) = $cursor->c_get($digest,$v,DB_SET);
$st1==0 || $st1==DB_NOTFOUND or die "BDB C c_get: $BerkeleyDB::Error, $!.";
if ($st1==0 && $v=~/^([^ ]+) /s) { if ($1 ne $t) {
do_log(5,"enqueue: not deleting: $digest, was refreshed since");
} else { do_log(5,"enqueue: deleting: $digest");
my($st2) = $cursor->c_del; $st2==0 || $st2==DB_KEYEMPTY
or die "BDB C c_del: $BerkeleyDB::Error, $!.";
}
}
$cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!.";
}
my($st3) = $qcursor->c_del;
$st3==0 || $st3==DB_KEYEMPTY or die "BDB Q c_del: $BerkeleyDB::Error, $!.";
}
$stat==0 || $stat==DB_NOTFOUND or die "BDB Q c_get: $BerkeleyDB::Error, $!.";
$qcursor->c_close==0 or die "BDB Q c_close: $BerkeleyDB::Error, $!.";
$dbq->db_put($key, "$expires_utc_iso8601 $str", DB_APPEND) == 0
or die "BDB Q db_put: $BerkeleyDB::Error, $!.";
}
sub get {
my($self,$key) = @_;
my($val); my($db) = $self->{'db_cache'};
if (!defined($db)) {
$val = $self->{'mem_cache'}{$key}; } else {
my($stat) = $db->db_get($key,$val);
$stat==0 || $stat==DB_NOTFOUND
or die "BDB C c_get: $BerkeleyDB::Error, $!.";
local($1,$2);
if ($stat==0 && $val=~/^([^ ]+) (.*)/s) { $val = $2 } else { $val = undef }
}
thaw($val);
}
sub set {
my($self,$key,$obj,$now_utc_iso8601,$expires_utc_iso8601) = @_;
my($db) = $self->{'db_cache'};
if (!defined($db)) {
$self->{'mem_cache'}{$key} = freeze($obj);
} else {
my($cursor) = $db->db_cursor(DB_WRITECURSOR);
defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!.";
my($val); my($stat) = $cursor->c_get($key,$val,DB_SET);
$stat==0 || $stat==DB_NOTFOUND
or die "BDB C c_get: $BerkeleyDB::Error, $!.";
$cursor->c_put($key, $expires_utc_iso8601.' '.freeze($obj),
$stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0
or die "BDB C c_put: $BerkeleyDB::Error, $!.";
$cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!.";
$self->enqueue($key,$now_utc_iso8601,$expires_utc_iso8601);
}
$obj;
}
1;
__DATA__
package Amavis::Lookup::SQLfield;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
BEGIN { import Amavis::Util qw(ll do_log) }
sub new($$$;$$) {
my($class, $sql_query,$fieldname, $fieldtype,$implied_args) = @_;
return undef if !defined($sql_query);
my($self) = bless {}, $class;
$self->{sql_query} = $sql_query;
$self->{fieldname} = lc($fieldname);
$self->{fieldtype} = uc($fieldtype);
$self->{args} = ref($implied_args) eq 'ARRAY' ? [@$implied_args] : [$implied_args] if defined $implied_args;
$self;
}
sub lookup_sql_field($$$) {
my($self,$addr,$get_all) = @_;
my(@result,@matchingkey);
if (!defined($self)) {
do_log(5, "lookup_sql_field - undefined, \"$addr\" no match");
} elsif (!defined($self->{sql_query})) {
do_log(5, sprintf("lookup_sql_field(%s) - null query, \"%s\" no match",
$self->{fieldname}, $addr));
} else {
my($field) = $self->{fieldname};
my($res_ref,$mk_ref) = $self->{sql_query}->lookup_sql($addr,1,
!exists($self->{args}) ? () : $self->{args});
do_log(5, "lookup_sql_field($field), \"$addr\" no matching record")
if !@$res_ref;
for my $ind (0..$ my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind];
if (!exists($h_ref->{$field})) {
if ( $self->{fieldtype} =~ /^B0/) { $match = 0; do_log(5, "lookup_sql_field($field), no field, \"$addr\" result=$match");
} elsif ($self->{fieldtype} =~ /^B1/) { $match = 1; do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=$match");
} elsif ($self->{fieldtype}=~/^.-/s) { do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=undef");
} else { do_log(1,"lookup_sql_field($field) ".
"(WARN: no such field in the SQL table), ".
"\"$addr\" result=undef");
}
} else { $match = $h_ref->{$field};
if (!defined($match)) { } elsif ($self->{fieldtype} =~ /^B/) { $match = 0 if $match =~ /^([NnFf ]|0+|\000+)[ ]*\z/;
} elsif ($self->{fieldtype} =~ /^N/) { $match = $match + 0; } elsif ($self->{fieldtype} =~ /^S/) { $match =~ s/ +\z//; # trim trailing spaces
}
do_log(5, "lookup_sql_field($field) \"$addr\" result=" .
(defined $match ? $match : 'undef') );
}
if (defined $match) {
push(@result,$match); push(@matchingkey,$mk);
last if !$get_all;
}
}
}
if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
else { !wantarray ? \@result : (\@result, \@matchingkey) }
}
1;
package Amavis::Lookup::SQL;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
use DBI;
BEGIN {
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::Timing qw(section_time);
import Amavis::Util qw(untaint snmp_count ll do_log);
import Amavis::rfc2821_2822_Tools qw(make_query_keys);
}
use vars qw($sql_connected);
sub connect_to_sql(@) {
my(@dsns) = @_; my($dbh);
do_log(3,"Connecting to SQL database server");
for my $tmpdsn (@dsns) {
my($dsn, $username, $password) = @$tmpdsn;
do_log(4, "connect_to_sql: trying '$dsn'");
$dbh = DBI->connect($dsn, $username, $password,
{PrintError => 0, RaiseError => 0, Taint => 1} );
if ($dbh) { do_log(3,"connect_to_sql: '$dsn' succeeded"); last }
do_log(-1,"connect_to_sql: unable to connect to DSN '$dsn': ".$DBI::errstr);
}
do_log(-2,"connect_to_sql: unable to connect to any DSN at all!")
if !$dbh && @dsns > 1;
$sql_connected = 1 if $dbh;
$dbh;
}
sub new {
my($class) = @_; bless {}, $class;
}
sub DESTROY {
my($self) = shift;
eval { do_log(5,"Amavis::Lookup::SQL called") };
if (defined $self && $self->{dbh} && $sql_connected) {
$sql_connected = 0;
eval { $self->{dbh}->disconnect }; $self->{dbh} = undef;
}
}
sub store_dbh($$$) {
my($self, $dbh, $select_clause) = @_;
$self->{dbh} = $dbh; $self->{select_clause} = $select_clause;
$self->clear_cache; $self;
}
sub clear_cache {
my($self) = @_;
delete $self->{cache};
}
sub lookup_sql($$$;$) {
my($self, $addr,$get_all,$extra_args) = @_;
my(@matchingkey,@result);
if (!defined $extra_args &&
exists $self->{cache} && exists $self->{cache}->{$addr})
{ my($c) = $self->{cache}->{$addr}; @result = @$c if ref $c;
@matchingkey = map {'/cached/'} @result; if (!ll(5)) {
} elsif (!@result) {
do_log(5,"lookup_sql (cached): \"$addr\" no match");
} else {
for my $m (@result) {
do_log(5, sprintf("lookup_sql (cached): \"%s\" matches, result=(%s)",
$addr, join(", ", map { sprintf("%s=>%s", $_,
!defined($m->{$_})?'-':'"'.$m->{$_}.'"'
) } sort keys(%$m) ) ));
}
}
if (!$get_all) {
return(!wantarray ? $result[0] : ($result[0], $matchingkey[0]));
} else {
return(!wantarray ? \@result : (\@result, \@matchingkey));
}
}
if (!$sql_connected) {
my($sql_dbh) = connect_to_sql(@lookup_sql_dsn);
section_time('sql-connect');
defined($sql_dbh) or die "SQL server(s) not reachable";
$sql_dbh->{'RaiseError'} = 1;
$Amavis::sql_policy->store_dbh($sql_dbh, $sql_select_policy)
if defined $sql_select_policy;
$Amavis::sql_wblist->store_dbh($sql_dbh, $sql_select_white_black_list)
if defined $sql_select_white_black_list;
}
my($is_local); $is_local = Amavis::Lookup::lookup(0,$addr,
grep {ref ne 'Amavis::Lookup::SQL' &&
ref ne 'Amavis::Lookup::SQLfield' &&
ref ne 'Amavis::Lookup::LDAP' &&
ref ne 'Amavis::Lookup::LDAPattr'}
@{ca('local_domains_maps')});
my($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local);
my(@keys) = @$keys_ref;
my($n) = sprintf("%d",scalar(@keys)); my($sel) = $self->{select_clause};
my($k_cnt) = $sel =~ s/%k/join(',',('?')x$n)/ge;
if (!exists $self->{"sth$n"}) {
do_log(5,"SQL prepare($n): $sel");
$self->{"sth$n"} = $self->{dbh}->prepare($sel);
}
my($sth) = $self->{"sth$n"};
@keys = (@keys) x $k_cnt if $k_cnt != 1;
unshift(@keys,@$extra_args) if ref $extra_args; do_log(4,"lookup_sql \"$addr\", query keys: ".join(', ',map{"\"$_\""}@keys));
do_log(4,"lookup_sql select: $sel");
my($a_ref,$found); my($match) = {};
eval {
snmp_count('OpsSqlSelect');
$sth->execute(map { untaint($_) } @keys); while ( defined($a_ref=$sth->fetchrow_arrayref) ) { my(@names) = @{$sth->{NAME_lc}};
$match = {}; @$match{@names} = @$a_ref;
if (!exists $match->{'local'} && $match->{'email'} eq '@.') {
push(@names,'local'); $match->{'local'} = undef;
do_log(5, "lookup_sql: \"$addr\" matches catchall, local=>undef");
}
push(@result, {%$match}); push(@matchingkey, join(", ", map { sprintf("%s=>%s", $_,
!defined($match->{$_})?'-':'"'.$match->{$_}.'"'
) } @names));
last if !$get_all;
}
$sth->finish();
}; if ($@ ne '') {
my($err) = $@;
do_log(-1, "lookup_sql: $DBI::err, $DBI::errstr");
if (!$sth) {}
elsif ($sth->err eq '2006' || $sth->errstr =~ /\bserver has gone away\b/ ||
$sth->err eq '2013' || $sth->errstr =~ /\bLost connection to\b/) {
do_log(-1,"NOTICE: Disconnected from SQL server");
$sql_connected = 0; $self->{dbh}->disconnect; $self->{dbh} = undef;
}
die $err;
}
if (!ll(4)) {
} elsif (!@result) {
do_log(4, "lookup_sql, \"$addr\" no match")
} else {
do_log(4, "lookup_sql($addr) matches, result=($_)") for @matchingkey;
}
$self->{cache}->{$addr} = \@result;
section_time('lookup_sql');
if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
else { !wantarray ? \@result : (\@result, \@matchingkey) }
}
1;
__DATA__
package Amavis::Lookup::LDAPattr;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
import Amavis::Util qw(ll do_log)
}
sub new($$$;$) {
my($class,$ldap_query,$attrname,$attrtype) = @_;
return undef if !defined($ldap_query);
my($self) = bless {}, $class;
$self->{ldap_query} = $ldap_query;
$self->{attrname} = lc($attrname);
$self->{attrtype} = uc($attrtype);
$self;
}
sub lookup_ldap_attr($$$) {
my($self,$addr,$get_all) = @_;
my(@result,@matchingkey);
if (!defined($self)) {
do_log(5,"lookup_ldap_attr - undefined, \"$addr\" no match");
} elsif (!defined($self->{ldap_query})) {
do_log(5,sprintf("lookup_ldap_attr(%s) - null query, \"%s\" no match",
$self->{attrname}, $addr));
} else {
my($attr) = $self->{attrname};
my($res_ref,$mk_ref) = $self->{ldap_query}->lookup_ldap($addr,1);
do_log(5,"lookup_ldap_attr($attr), \"$addr\" no matching record")
if !@$res_ref;
for my $ind (0..$ my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind];
if (!exists($h_ref->{$attr})) {
if ( $self->{attrtype} =~ /^B0/) { $match = 0; do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match");
} elsif ($self->{attrtype} =~ /^B1/) { $match = 1; do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match");
} elsif ($self->{attrtype}=~/^.-/s) { do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=undef");
} else { do_log(1,"lookup_ldap_attr($attr) ".
"(WARN: no such attribute in LDAP entry), ".
"\"$addr\" result=undef");
}
} else { $match = $h_ref->{$attr};
if (!defined($match)) { } elsif ($self->{attrtype} =~ /^B/) { $match = $match eq "TRUE" ? 1 : 0; } elsif ($self->{attrtype} =~ /^N/) { $match = $match + 0; } elsif ($self->{attrtype} =~ /^S/) { $match =~ s/ +\z//; # trim trailing spaces
} elsif ($self->{attrtype} =~ /^L/) { }
do_log(5,sprintf("lookup_ldap_attr(%s) \"%s\" result=(%s)",
$attr, $addr, defined($match) ? $match : 'undef'));
}
if (defined $match) {
push(@result,$match); push(@matchingkey,$mk);
last if !$get_all;
}
}
}
if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
else { !wantarray ? \@result : (\@result, \@matchingkey) }
}
1;
package Amavis::Lookup::LDAP;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION
$ldap_sys_default @ldap_attrs);
$VERSION = '2.033';
@ISA = qw(Exporter);
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::Timing qw(section_time);
import Amavis::Util qw(untaint snmp_count ll do_log);
import Amavis::rfc2821_2822_Tools qw(make_query_keys);
$ldap_sys_default = {
hostname => 'localhost',
port => 389,
timeout => 120,
tls => 0,
base => undef,
scope => 'sub',
query_filter => '(&(objectClass=amavisAccount)(mail=%m))',
bind_dn => undef,
bind_password => undef,
};
@ldap_attrs = qw(amavisVirusLover amavisSpamLover amavisBannedFilesLover
amavisBadHeaderLover amavisBypassVirusChecks amavisBypassSpamChecks
amavisBypassBannedChecks amavisBypassHeaderChecks amavisSpamTagLevel
amavisSpamTag2Level amavisSpamKillLevel amavisSpamModifiesSubject
amavisVirusQuarantineTo amavisSpamQuarantineTo amavisBannedQuarantineTo
amavisBadHeaderQuarantineTo amavisBlacklistSender amavisWhitelistSender
amavisLocal amavisMessageSizeLimit mail
);
}
use vars qw($ldap_connected);
sub new {
my($class,$default) = @_;
my($self) = bless {}, $class;
for (qw(hostname port timeout tls base scope query_filter
bind_dn bind_password)) {
$self->{$_} = $default->{$_} unless defined($self->{$_});
$self->{$_} = $ldap_sys_default->{$_} unless defined($self->{$_});
}
$self;
}
sub DESTROY {
my($self) = shift;
eval { do_log(5,"Amavis::Lookup::LDAP called") };
if (defined $self && $self->{ldap} && $ldap_connected) {
$ldap_connected = 0;
eval { $self->{ldap}->disconnect }; $self->{ldap} = undef;
}
}
sub connect_to_ldap {
my($self) = @_;
my($ldap);
do_log(3,"Connecting to LDAP host");
my $hostlist = ref $self->{hostname} eq 'ARRAY' ?
join(", ",@{$self->{hostname}}) : $self->{hostname};
do_log(4,"connect_to_ldap: trying $hostlist");
$ldap = Net::LDAP->new($self->{hostname},
port => $self->{port},
timeout => $self->{timeout},
onerror => 'undef');
if ($ldap) {
do_log(3,"connect_to_ldap: connected to $hostlist");
} else {
do_log(-1,"connect_to_ldap: unable to connect to host $hostlist");
return undef;
}
if ($self->{tls}) { my $tlsVer = $ldap->start_tls(verify=>'none');
do_log(3,"connect_to_ldap: TLS version $tlsVer enabled");
}
if ($self->{bind_dn}) { if ($ldap->bind($self->{bind_dn}, password => $self->{bind_password})) {
do_log(3,"connect_to_ldap: bind $self->{bind_dn} succeeded");
} else {
do_log(-1,"connect_to_ldap: bind $self->{bind_dn} failed");
return undef;
}
}
$ldap_connected = 1 if $ldap;
$ldap;
}
sub store_ldap($$) {
my($self,$ldap) = @_;
$self->{ldap} = $ldap; $self->clear_cache; $self;
}
sub clear_cache {
my($self) = @_;
delete $self->{cache};
}
sub lookup_ldap($$$) {
my($self,$addr,$get_all) = @_;
my(@matchingkey,@result);
if (exists $self->{cache} && exists $self->{cache}->{$addr}) { my($c) = $self->{cache}->{$addr}; @result = @$c if ref $c;
@matchingkey = map {'/cached/'} @result; if (!ll(5)) {
} elsif (!@result) {
do_log(5,"lookup_ldap (cached): \"$addr\" no match");
} else {
for my $m (@result) {
do_log(5, sprintf("lookup_ldap (cached): \"%s\" matches, result=(%s)",
$addr, join(", ", map { sprintf("%s=>%s", $_,
!defined($m->{$_})?'-':'"'.$m->{$_}.'"'
) } sort keys(%$m) ) ));
}
}
if (!$get_all) {
return(!wantarray ? $result[0] : ($result[0], $matchingkey[0]));
} else {
return(!wantarray ? \@result : (\@result, \@matchingkey));
}
}
if (!$ldap_connected) {
my($ldap) = $self->connect_to_ldap;
defined($ldap) or die "LDAP server(s) not reachable";
$self->store_ldap($ldap);
section_time('ldap-connect');
}
my($is_local); $is_local = Amavis::Lookup::lookup(0,$addr,
grep {ref ne 'Amavis::Lookup::SQL' &&
ref ne 'Amavis::Lookup::SQLfield' &&
ref ne 'Amavis::Lookup::LDAP' &&
ref ne 'Amavis::Lookup::LDAPattr'}
@{ca('local_domains_maps')});
my($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local);
my(@keys) = @$keys_ref;
$_ = untaint($_) for @keys; do_log(4,sprintf("lookup_ldap \"%s\", query keys: %s, %s",
$addr, join(', ',map{"\"$_\""}@keys), $self->{query_filter}));
my($result);
eval {
snmp_count('OpsLDAPSearch');
for my $key (@keys) {
my($match) = {};
(my $filter = $self->{query_filter}) =~ s/%m/$key/g;
do_log(9,sprintf(
"lookup_ldap: searching base=\"%s\", scope=\"%s\", filter=\"%s\"",
$self->{base}, $self->{scope}, $filter));
$result = $self->{ldap}->search(base => $self->{base},
scope => $self->{scope},
filter => $filter,
attrs => [@ldap_attrs],);
my $entry = $result->entry(0); next unless $entry; for my $attr (@ldap_attrs) {
my($value);
$attr = lc($attr);
do_log(9,"lookup_ldap: reading attribute \"$attr\" from object");
if ($attr =~ /^amavis(white|black)listsender\z/) { $value = $entry->get_value($attr, asref => 1);
} else {
$value = $entry->get_value($attr);
}
$match->{$attr} = $value if $value;
}
if (!exists $match->{'amavislocal'} && $match->{'mail'} eq '@.') {
$match->{'amavislocal'} = undef;
do_log(5,"lookup_ldap: \"$addr\" matches catchall, amavislocal=>undef");
}
push(@result, {%$match}); push(@matchingkey, join(", ", map { sprintf("%s=>%s", $_,
!defined($match->{$_})?'-':'"'.$match->{$_}.'"' ) } keys(%$match)));
last if !$get_all;
}
}; if ($@ ne '') {
my($err) = $@;
do_log(-1,"lookup_ldap: $err");
if (!$result || $result->code() != 'LDAP_SUCCESS') {
my $code = $result?$result->code():-1;
my $errname = Net::LDAP::Util::ldap_error_name->($code);
if ($errname eq 'LDAP_PARAM_ERROR') {
do_log(-1,"NOTICE: LDAP error - LDAP_PARAM_ERROR");
}
else { do_log(-1,"NOTICE: Check LDAP server, lost connection?");
$ldap_connected = 0; $self->{ldap}->disconnect; $self->{ldap} = undef;
}
}
die $err;
}
if (!ll(4)) {
} elsif (!@result) {
do_log(4,"lookup_ldap, \"$addr\" no match")
} else {
do_log(4,"lookup_ldap($addr) matches, result=($_)") for @matchingkey;
}
$self->{cache}->{$addr} = \@result;
section_time('lookup_ldap');
if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
else { !wantarray ? \@result : (\@result, \@matchingkey) }
}
1;
__DATA__
package Amavis::In::AMCL;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
use subs @EXPORT;
use Errno qw(ENOENT);
use IO::File ();
BEGIN {
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::Util qw(ll do_log debug_oneshot snmp_counters_init snmp_count
am_id new_am_id untaint rmdir_recursively);
import Amavis::Lookup qw(lookup);
import Amavis::Timing qw(section_time);
import Amavis::rfc2821_2822_Tools;
import Amavis::In::Message;
import Amavis::In::Connection;
import Amavis::Out::EditHeader qw(hdr);
import Amavis::rfc2821_2822_Tools qw(/^EX_/);
}
sub new($) { my($class) = @_; bless {}, $class }
sub process_policy_request($$$$) {
my($self, $sock, $conn, $check_mail, $old_amcl) = @_;
my(%attr);
$0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
if ($old_amcl) {
my($state) = 0; $attr{'request'} = 'AM.CL'; my($response) = "\001";
my($rv,@recips,@ldaargs,$inbuff); local($1);
my(@attr_names) = qw(tempdir sender recipient ldaargs);
while (defined($rv = recv($sock, $inbuff, 8192, 0))) {
$0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
if ($state < 2) {
$attr{$attr_names[$state]} = $inbuff; $state++;
} elsif ($state == 2 && $inbuff eq "\002") {
$state++;
} elsif ($state >= 2 && $inbuff eq "\003") {
section_time('got data');
$attr{'recipient'} = \@recips; $attr{'ldaargs'} = \@ldaargs;
$attr{'delivery_care_of'} = @ldaargs ? 'client' : 'server';
eval {
my($msginfo) = preprocess_policy_query(\%attr);
$response = (map { /^exit_code=(\d+)\z/ ? $1 : () }
check_amcl_policy($conn,$msginfo,$check_mail,1))[0];
};
if ($@ ne '') {
chomp($@); do_log(-2,"policy_server FAILED: $@");
$response = EX_TEMPFAIL;
}
$state = 4;
} elsif ($state == 2) {
push(@recips, $inbuff);
} else {
push(@ldaargs, $inbuff);
}
defined(send($sock,$response,0))
or die "send failed in state $state: $!";
last if $state >= 4;
$0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
}
if ($state==4 && defined($rv)) {
} elsif (!defined($rv) && $! != 0) {
die "recv failed in state $state: $!";
} else { die "helper client session terminated unexpectedly, state: $state";
}
do_log(2, Amavis::Timing::report());
} else { my(@response); local($1,$2,$3);
local($/) = "\012"; $! = undef;
while(<$sock>) { $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
Amavis::Timing::init(); snmp_counters_init();
if (/^\015?\012\z/) { section_time('got data');
eval {
my($msginfo) = preprocess_policy_query(\%attr);
@response = $attr{'request'} eq 'smtpd_access_policy'
? postfix_policy($conn,$msginfo,\%attr)
: check_amcl_policy($conn,$msginfo,$check_mail,0);
};
if ($@ ne '') {
chomp($@); do_log(-2, "policy_server FAILED: $@");
@response = (proto_encode('setreply','450','4.5.0',"Failure: $@"),
proto_encode('return_value','tempfail'),
proto_encode('exit_code',sprintf("%d",EX_TEMPFAIL)));
}
$sock->print( map { $_."\015\012" } (@response,'') )
or die "Can't write response to socket: $!";
%attr = (); @response = ();
do_log(2, Amavis::Timing::report());
} elsif (/^ ([^=\000\012]*?) (=|:[ \t]*)
([^\012]*?) \015?\012 \z/xsi) {
my($attr_name) = Amavis::tcp_lookup_decode($1);
my($attr_val) = Amavis::tcp_lookup_decode($3);
if (!exists $attr{$attr_name}) {
$attr{$attr_name} = $attr_val;
} else {
if (!ref($attr{$attr_name}))
{ $attr{$attr_name} = [ $attr{$attr_name} ] }
push(@{$attr{$attr_name}}, $attr_val);
}
my($known_attr) = scalar(grep {$_ eq $attr_name} qw(
request helo_name protocol_state protocol_name queue_id
client_name client_address sender recipient) );
do_log(!$known_attr?-1:1, "policy protocol: $attr_name=$attr_val");
} else {
do_log(-1, "policy protocol: INVALID ATTRIBUTE LINE: $_");
}
$0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
$! = undef; }
if (!defined($_) && $! != 0) { die "read from client socket FAILED: $!" }
};
$0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
}
sub preprocess_policy_query($) {
my($attr_ref) = @_;
my($msginfo) = Amavis::In::Message->new;
$msginfo->rx_time(time);
my($sender,@recips);
$msginfo->delivery_method(
lc($attr_ref->{'delivery_care_of'}) eq 'server' ? c('forward_method') :'');
$msginfo->client_delete(lc($attr_ref->{'tempdir_removed_by'}) eq 'client'
? 1 : 0);
$msginfo->queue_id($attr_ref->{'queue_id'})
if exists $attr_ref->{'queue_id'};
$msginfo->client_addr($attr_ref->{'client_address'})
if exists $attr_ref->{'client_address'};
$msginfo->client_name($attr_ref->{'client_name'})
if exists $attr_ref->{'client_name'};
$msginfo->client_proto($attr_ref->{'protocol_name'})
if exists $attr_ref->{'protocol_name'};
$msginfo->client_helo($attr_ref->{'helo_name'})
if exists $attr_ref->{'helo_name'};
if (exists $attr_ref->{'sender'}) {
$sender = $attr_ref->{'sender'};
$sender = unquote_rfc2821_local($sender);
$msginfo->sender($sender);
}
if (exists $attr_ref->{'recipient'}) {
my($r) = $attr_ref->{'recipient'};
@recips = !ref($r) ? $r : @$r;
$_ = unquote_rfc2821_local($_) for @recips;
$msginfo->recips(\@recips);
}
if (!exists $attr_ref->{'tempdir'}) {
$msginfo->mail_tempdir($TEMPBASE); } else {
local($1,$2); my($tempdir) = $attr_ref->{tempdir};
$tempdir =~ /^ (?: \Q$TEMPBASE\E | \Q$MYHOME\E )
\/ (?! \.{1,2} \z) [A-Za-z0-9_.-]+ \z/xso
or die "Invalid/unexpected temporary directory name '$tempdir'";
$msginfo->mail_tempdir(untaint($tempdir));
}
if (!exists $attr_ref->{'mail_file'}) {
$msginfo->mail_text_fn($msginfo->mail_tempdir . '/email.txt');
} else {
$msginfo->mail_text_fn(untaint($attr_ref->{'mail_file'}));
}
if ($msginfo->mail_text_fn ne '') {
my($fname) = $msginfo->mail_text_fn;
do_log(5, "preprocess_policy_query: opening mail file '$fname'");
new_am_id( ($fname =~ m{amavis-(milter-)?([^/]+?)\z}s ? $2 : undef) );
my($fh) = IO::File->new;
$fh->open($fname,'<') or die "Can't open file $fname: $!";
binmode($fh,":bytes") or die "Can't cancel :utf8 mode: $!"
if $unicode_aware;
$msginfo->mail_text($fh); }
if ($attr_ref->{'request'} =~ /^AM\.(CL|PDP)\z/) {
do_log(1, sprintf("%s %s: <%s> -> %s",
$attr_ref->{'request'}, $msginfo->mail_tempdir, $sender,
join(',', qquote_rfc2821_local(@recips)) ));
} else {
do_log(1, sprintf("%s(%s): %s %s: %s[%s] <%s> -> <%s>",
@$attr_ref{qw(request protocol_state protocol_name queue_id
client_name client_address sender recipient)}));
}
$msginfo;
}
sub check_amcl_policy($$$$) {
my($conn,$msginfo,$check_mail,$old_amcl) = @_;
my($smtp_resp, $exit_code, $preserve_evidence);
my(%baseline_policy_bank); my($policy_changed) = 0;
%baseline_policy_bank = %current_policy_bank;
if (!ref($msginfo->per_recip_data) || !defined($msginfo->mail_text)) {
$smtp_resp = '450 4.5.0 Incomplete request'; $exit_code = EX_TEMPFAIL;
} else {
my($cl_ip) = $msginfo->client_addr;
if ($cl_ip ne '' && defined $policy_bank{'MYNETS'}
&& lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) {
Amavis::load_policy_bank('MYNETS'); $policy_changed = 1;
}
debug_oneshot(1) if lookup(0,$msginfo->sender,@{ca('debug_sender_maps')});
my($fh) = $msginfo->mail_text; my($tempdir) = $msginfo->mail_tempdir;
($smtp_resp, $exit_code, $preserve_evidence) =
&$check_mail($conn,$msginfo,0,$tempdir);
$fh->close or die "Can't close temp file: $!" if $fh;
$fh = undef; $msginfo->mail_text(undef);
my($errn) = $tempdir eq '' ? ENOENT : (stat($tempdir) ? 0 : 0+$!);
if ($tempdir eq '' || $errn == ENOENT) {
} elsif ($msginfo->client_delete) {
do_log(4, "AM.PDP: deletion of $tempdir is client's responsibility");
} elsif ($preserve_evidence) {
do_log(-1,"AM.PDP: tempdir is to be PRESERVED: $tempdir");
} else {
do_log(4, "AM.PDP: tempdir being removed: $tempdir");
my($fname) = $msginfo->mail_text_fn;
unlink($fname) or die "Can't remove file $fname: $!" if $fname ne '';
rmdir_recursively($tempdir);
}
}
my(@response); my($rcpt_deletes,$rcpt_count)=(0,0);
if (ref($msginfo->per_recip_data)) {
for my $r (@{$msginfo->per_recip_data})
{ $rcpt_count++; if ($r->recip_done) { $rcpt_deletes++ } }
}
if ($smtp_resp=~/^([1-5]\d\d) ([245]\.\d{1,3}\.\d{1,3})(?: |\z)(.*)\z/s)
{ push(@response, proto_encode('setreply', $1,$2,$3)) }
if ($exit_code == EX_TEMPFAIL) {
push(@response, proto_encode('return_value','tempfail'));
} elsif ($exit_code == EX_NOUSER) { push(@response, proto_encode('return_value','reject'));
} elsif ($exit_code == EX_UNAVAILABLE) { push(@response, proto_encode('return_value','reject'));
} elsif ($exit_code == 99) { push(@response, proto_encode('return_value','discard'));
} elsif ($rcpt_count-$rcpt_deletes <= 0) { do_log(-1, "WARN: no recips left (forgot to set ".
"\$forward_method=undef using milter?), $smtp_resp");
$exit_code = 99;
push(@response, proto_encode('return_value','discard'));
} elsif ($msginfo->delivery_method ne '') {
$exit_code = 99; push(@response, proto_encode('return_value','discard'));
} else { for my $r (@{$msginfo->per_recip_data}) { my($addr,$newaddr) = ($r->recip_addr, $r->recip_final_addr);
if ($r->recip_done) { push(@response, proto_encode('delrcpt',
quote_rfc2821_local($addr)));
} elsif ($newaddr ne $addr) { push(@response, proto_encode('delrcpt',
quote_rfc2821_local($addr)));
push(@response, proto_encode('addrcpt',
quote_rfc2821_local($newaddr)));
}
}
my($hdr_edits) = $msginfo->header_edits;
if ($hdr_edits) { local($1,$2);
for my $hf (@{$hdr_edits->{prepend}}, @{$hdr_edits->{append}}) {
if ($hf =~ /^([^:]+):[ \t]*(.*?)$/s)
{ push(@response, proto_encode('addheader',$1,$2)) }
}
my($field_name,$edit,$field_body);
while ( ($field_name,$edit) = each %{$hdr_edits->{edit}} ) {
$field_body = $msginfo->mime_entity->head->get($field_name,0);
if (!defined($field_body)) {
} elsif (!defined($edit)) { push(@response, proto_encode('delheader',"1",$field_name));
} else { chomp($field_body);
$field_body = hdr($field_name, &$edit($field_name,$field_body));
$field_body = $1 if $field_body =~ /^[^:]+:[ \t]*(.*?)$/s;
push(@response, proto_encode('chgheader', "1",
$field_name, $field_body));
}
}
}
if ($old_amcl) { for (grep { /^(delrcpt|addrcpt)=/ } @response)
{ do_log(-1, "WARN: MTA can't do: $_") }
if ($rcpt_deletes && $rcpt_count-$rcpt_deletes > 0) {
do_log(-1, "WARN: ACCEPT THE WHOLE MESSAGE, ".
"MTA-in can't do selective recips deletion");
}
}
push(@response, proto_encode('return_value','continue'));
}
push(@response, proto_encode('exit_code',"$exit_code"));
do_log(2, "mail checking ended: ".join("\n",@response));
if ($policy_changed) {
%current_policy_bank = %baseline_policy_bank; $policy_changed = 0;
}
@response;
}
sub postfix_policy($$$) {
my($conn,$msginfo,$attr_ref) = @_;
my(@response);
if (!exists($attr_ref->{'request'})) {
die "no 'request' attribute";
} elsif ($attr_ref->{'request'} ne 'smtpd_access_policy') {
die ("unknown 'request' value: " . $attr_ref->{'request'});
} else {
@response = 'action=DUNNO';
}
@response;
}
sub proto_encode($@) {
my($attribute_name,@strings) = @_; local($1);
$attribute_name =~ s/([^0-9a-zA-Z_-])/sprintf("%%%02x",ord($1))/eg;
for (@strings) { s/([^\041-\044\046-\176])/sprintf("%%%02x",ord($1))/eg;
}
$attribute_name . '=' . join(' ',@strings);
}
1;
__DATA__
package Amavis::In::SMTP;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
use Errno qw(ENOENT);
use MIME::Base64;
BEGIN {
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::Util qw(ll do_log am_id new_am_id snmp_counters_init
prolong_timer debug_oneshot sanitize_str
strip_tempdir rmdir_recursively);
import Amavis::Lookup qw(lookup lookup_ip_acl);
import Amavis::Timing qw(section_time);
import Amavis::rfc2821_2822_Tools;
import Amavis::In::Message;
import Amavis::In::Connection;
}
sub new($) {
my($class) = @_;
my($self) = bless {}, $class;
$self->{sock} = undef; $self->{proto} = undef; $self->{pipelining} = undef; $self->{smtp_outbuf} = undef; $self->{fh_pers} = undef; $self->{tempdir_persistent} = undef; $self->{preserve} = undef; $self->{tempdir_empty} = 1; $self->{session_closed_normally} = undef; $self;
}
sub preserve_evidence { my($self)=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift) }
sub DESTROY {
my($self) = shift;
eval { do_log(5,"Amavis::In::SMTP::DESTROY called") };
eval {
$self->{fh_pers}->close
or die "Can't close temp file: $!" if $self->{fh_pers};
my($errn) = $self->{tempdir_pers} eq '' ? ENOENT
: (stat($self->{tempdir_pers}) ? 0 : 0+$!);
if (defined $self->{tempdir_pers} && $errn != ENOENT) {
if ($self->preserve_evidence && !$self->{tempdir_empty}) {
do_log(-1,"SMTP shutdown: tempdir is to be PRESERVED: ".
$self->{tempdir_pers});
} else {
do_log(2, sprintf("SMTP shutdown: %s is being removed: %s%s",
$self->{tempdir_empty} ? 'empty tempdir' : 'tempdir',
$self->{tempdir_pers},
$self->preserve_evidence ? ', nothing to preserve' : ''));
rmdir_recursively($self->{tempdir_pers});
}
}
if (! $self->{session_closed_normally}) {
$self->smtp_resp(1,"421 4.3.2 Service shutting down, closing channel");
}
};
if ($@ ne '')
{ my($eval_stat) = $@; eval { do_log(1,"SMTP shutdown: $eval_stat") } }
}
sub prepare_tempdir($) {
my($self) = @_;
if (! defined $self->{tempdir_pers} ) {
my($now_iso8601) = iso8601_timestamp(time,1); $self->{tempdir_pers} = sprintf("%s/amavis-%s-%05d",
$TEMPBASE, $now_iso8601, $$);
}
my($dname) = $self->{tempdir_pers};
my(@stat_list) = lstat($dname); my($errn) = @stat_list ? 0 : 0+$!;
if (!$errn && ! -d _) { die "prepare_tempdir: $dname is not a directory!!!";
} elsif (!$errn) {
my($dev,$ino) = @stat_list;
if ($dev != $self->{tempdir_dev} || $ino != $self->{tempdir_ino}) {
do_log(-1,"prepare_tempdir: $dname is no longer the same directory!!!");
($self->{tempdir_dev},$self->{tempdir_ino}) = @stat_list;
}
} elsif ($errn == ENOENT) {
do_log(4,"prepare_tempdir: creating directory $dname");
mkdir($dname,0750) or die "Can't create directory $dname: $!";
($self->{tempdir_dev},$self->{tempdir_ino}) = lstat($dname);
$self->{tempdir_empty} = 1;
section_time('mkdir tempdir');
}
my($fname) = $dname . '/email.txt';
@stat_list = lstat($fname); $errn = @stat_list ? 0 : 0+$!;
if (!$errn && ! -f _) { die "prepare_tempdir: $fname is not a regular file!!!";
} elsif ($self->{fh_pers} && !$errn && -f _) {
my($dev,$ino) = @stat_list;
if ($dev != $self->{file_dev} || $ino != $self->{file_ino}) {
do_log(-1,"prepare_tempdir: $fname is no longer the same file!!!");
($self->{file_dev}, $self->{file_ino}) = @stat_list;
}
$self->{fh_pers}->seek(0,0) or die "Can't rewind mail file: $!";
$self->{fh_pers}->truncate(0) or die "Can't truncate mail file: $!";
} else {
do_log(4,"prepare_tempdir: creating file $fname");
$self->{fh_pers} = IO::File->new($fname,'+>',0640)
or die "Can't create file $fname: $!";
($self->{file_dev}, $self->{file_ino}) = lstat($fname);
section_time('create email.txt');
}
}
sub authenticate($$$) {
my($state,$auth_mech,$auth_resp) = @_;
my($result,$newchallenge);
if ($auth_mech eq 'ANONYMOUS') { $result = [$auth_resp,undef];
} elsif ($auth_mech eq 'PLAIN') { if (!defined($auth_resp)) { $newchallenge = '' }
else { $result = [ (split(/\000/,$auth_resp,-1))[0,2] ] }
} elsif ($auth_mech eq 'LOGIN' && !defined $state) {
$newchallenge = 'Username:'; $state = [];
} elsif ($auth_mech eq 'LOGIN' && @$state==0) {
push(@$state, $auth_resp); $newchallenge = 'Password:';
} elsif ($auth_mech eq 'LOGIN' && @$state==1) {
push(@$state, $auth_resp); $result = $state;
} ($state,$result,$newchallenge);
}
sub process_smtp_request($$$$) {
my($self, $sock, $lmtp, $conn, $check_mail) = @_;
my($msginfo,$authenticated,$auth_user,$auth_pass);
$self->{sock} = $sock;
$self->{pipelining} = 0; $self->{smtp_outbuf} = [];
my($myheloname);
$myheloname = '[' . $conn->socket_ip . ']';
new_am_id(undef, $Amavis::child_invocation_count, undef);
my($initial_am_id) = 1; my($sender,@recips); my($got_rcpt);
my($max_recip_size_limit); my($terminating,$aborting,$eof,$voluntary_exit); my($seq) = 0;
my(%xforward_args); my(%baseline_policy_bank); my($policy_changed);
%baseline_policy_bank = %current_policy_bank; $policy_changed = 0;
$conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'SMTP');
my($message_size_limit) = c('smtpd_message_size_limit');
if ($message_size_limit && $message_size_limit < 65536)
{ $message_size_limit = 65536 } my($smtpd_greeting_banner_tmp) = c('smtpd_greeting_banner');
$smtpd_greeting_banner_tmp =~
s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) }
{ { 'helo-name' => $myheloname,
'version' => $myversion,
'version-id' => $myversion_id,
'version-date' => $myversion_date,
'product' => $myproduct_name,
'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
}egx;
$self->smtp_resp(1, "220 $smtpd_greeting_banner_tmp");
$0 = sprintf("amavisd (ch%d-idle)", $Amavis::child_invocation_count);
Amavis::Timing::go_idle(4);
undef $!;
while(<$sock>) {
$0 = sprintf("amavisd (ch%d-%s)",
$Amavis::child_invocation_count, am_id());
Amavis::Timing::go_busy(5);
prolong_timer('reading SMTP command');
{ my($cmd) = $_;
do_log(4, $self->{proto} . "< $cmd");
!/^ \s* ([A-Za-z]+) (?: \s+ (.*?) )? \s* \015\012 \z/xs && do {
$self->smtp_resp(1,"500 5.5.2 Error: bad syntax", 1, $cmd); last;
};
$_ = uc($1); my($args) = $2;
/^RSET|DATA|QUIT\z/ && $args ne '' && do {
$self->smtp_resp(1,"501 5.5.4 Error: $_ does not accept arguments",
1,$cmd);
last;
};
/^RSET\z/ && do { $sender = undef; @recips = (); $got_rcpt = 0;
$max_recip_size_limit = undef; $msginfo = undef;
if ($policy_changed) {
%current_policy_bank = %baseline_policy_bank;
$policy_changed = 0;
}
$self->smtp_resp(0,"250 2.0.0 Ok $_"); last };
/^NOOP\z/ && do { $self->smtp_resp(1,"250 2.0.0 Ok $_"); last };
/^QUIT\z/ && do {
my($smtpd_quit_banner_tmp) = c('smtpd_quit_banner');
$smtpd_quit_banner_tmp =~
s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) }
{ { 'helo-name' => $myheloname,
'version' => $myversion,
'version-id' => $myversion_id,
'version-date' => $myversion_date,
'product' => $myproduct_name,
'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
}egx;
$self->smtp_resp(1,"221 2.0.0 $smtpd_quit_banner_tmp");
$terminating=1; last;
};
/^HELO\z/ && do {
$sender = undef; @recips = (); $got_rcpt = 0; $max_recip_size_limit = undef; $msginfo = undef; if ($policy_changed)
{ %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
$self->{pipelining} = 0; $self->smtp_resp(0,"250 $myheloname");
$lmtp = 0; $conn->smtp_proto($self->{proto} = 'SMTP');
$conn->smtp_helo($args); section_time('SMTP HELO'); last;
};
(/^EHLO\z/ || /^LHLO\z/) && do {
$sender = undef; @recips = (); $got_rcpt = 0; $max_recip_size_limit = undef; $msginfo = undef; if ($policy_changed)
{ %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
$lmtp = /^LHLO\z/ ? 1 : 0;
$conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'ESMTP');
$self->{pipelining} = 1;
$self->smtp_resp(0,"250 $myheloname\n" . join("\n",
'PIPELINING',
!defined($message_size_limit) ? 'SIZE'
: sprintf('SIZE %d',$message_size_limit),
'8BITMIME',
'ENHANCEDSTATUSCODES',
!@{ca('auth_mech_avail')} ? ()
: join(' ','AUTH',@{ca('auth_mech_avail')}),
'XFORWARD NAME ADDR PROTO HELO' ));
$conn->smtp_helo($args); section_time("SMTP $_");
last;
};
/^XFORWARD\z/ && do { if (defined($sender)) {
$self->smtp_resp(0,"503 5.5.1 Error: XFORWARD not allowed within transaction", 1, $cmd);
last;
}
my($bad);
for (split(' ',$args)) {
if (!/^( [A-Za-z0-9] [A-Za-z0-9-]* ) = ( [\041-\176]{0,255} )\z/xs) {
$self->smtp_resp(0,"501 5.5.4 Syntax error in XFORWARD parameters",
1, $cmd);
$bad = 1; last;
} else {
my($name,$val) = (uc($1), $2);
if ($name =~ /^(?:NAME|ADDR|PROTO|HELO)\z/) {
$val = undef if uc($val) eq '[UNAVAILABLE]';
$xforward_args{$name} = $val;
} else {
$self->smtp_resp(0,"501 5.5.4 XFORWARD command parameter error: $name=$val",1,$cmd);
$bad = 1; last;
}
}
}
$self->smtp_resp(1,"250 2.5.0 Ok") if !$bad;
last;
};
/^HELP\z/ && do {
$self->smtp_resp(1,"214 2.0.0 See amavisd-new home page at:\n".
"http://www.ijs.si/software/amavisd/");
last;
};
/^AUTH\z/ && @{ca('auth_mech_avail')} && do { if ($args !~ /^([^ ]+)(?: ([^ ]*))?\z/is) {
$self->smtp_resp(0,"501 5.5.2 Syntax: AUTH mech [initresp]",1,$cmd);
last;
}
my($auth_mech,$auth_resp) = (uc($1), $2);
if ($authenticated) {
$self->smtp_resp(0,"503 5.5.1 Error: session already authenticated", 1, $cmd);
} elsif (defined($sender)) {
$self->smtp_resp(0,"503 5.5.1 Error: AUTH not allowed within transaction", 1, $cmd);
} elsif (!grep {uc($_) eq $auth_mech} @{ca('auth_mech_avail')}) {
$self->smtp_resp(0,"504 5.7.6 Error: requested authentication mechanism not supported", 1, $cmd);
} else {
my($state,$result,$challenge);
if ($auth_resp eq '=') { $auth_resp = '' } elsif ($auth_resp eq '') { $auth_resp = undef }
for (;;) {
if ($auth_resp !~ m{^[A-Za-z0-9+/=]*\z}) {
$self->smtp_resp(0,"501 5.5.4 Authentication failed: malformed authentication response", 1, $cmd);
last;
} else {
$auth_resp = decode_base64($auth_resp) if $auth_resp ne '';
($state,$result,$challenge) =
authenticate($state, $auth_mech, $auth_resp);
if (ref($result) eq 'ARRAY') {
$self->smtp_resp(0,"235 2.7.1 Authentication successful");
$authenticated = 1; ($auth_user,$auth_pass) = @$result;
do_log(2,"AUTH $auth_mech, user=$auth_user");
last;
} elsif (defined $result && !$result) {
$self->smtp_resp(0,"535 5.7.1 Authentication failed", 1, $cmd);
last;
}
}
$self->smtp_resp(1,"334 ".encode_base64($challenge,''));
$auth_resp = <$sock>;
do_log(5, $self->{proto} . "< $auth_resp");
$auth_resp =~ s/\015?\012\z//;
if ($auth_resp eq '*') {
$self->smtp_resp(0,"501 5.7.1 Authentication aborted");
last;
}
}
}
last;
};
/^VRFY\z/ && do {
$self->smtp_resp(1,"502 5.5.1 Command $_ not implemented", 1, $cmd);
last;
};
/^MAIL\z/ && do { if (defined($sender)) {
$self->smtp_resp(0,"503 5.5.1 Error: nested MAIL command", 1, $cmd);
last;
}
if (!$authenticated &&
c('auth_required_inp') && @{ca('auth_mech_avail')} ) {
$self->smtp_resp(0,"530 5.7.1 Authentication required", 1, $cmd);
last;
}
my($now) = time;
prolong_timer('MAIL FROM received - timer reset', $child_timeout);
if (!$seq) { section_time('SMTP pre-MAIL');
} else { Amavis::Timing::init(); snmp_counters_init();
}
$seq++;
new_am_id(undef,$Amavis::child_invocation_count,$seq)
if !$initial_am_id;
$initial_am_id = 0; $self->prepare_tempdir;
my($cl_ip) = $xforward_args{'ADDR'};
if ($cl_ip ne '' && defined $policy_bank{'MYNETS'}
&& lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) {
Amavis::load_policy_bank('MYNETS'); $policy_changed = 1;
}
$msginfo = Amavis::In::Message->new;
$msginfo->rx_time($now);
$msginfo->delivery_method(c('forward_method'));
my($submitter);
if ($authenticated) {
$msginfo->auth_user($auth_user); $msginfo->auth_pass($auth_pass);
$conn->smtp_proto($self->{proto}.'A') if $self->{proto} =~ /^(LMTP|ESMTP)\z/i;
} elsif (c('auth_reauthenticate_forwarded') &&
c('amavis_auth_user') ne '') {
$msginfo->auth_user(c('amavis_auth_user'));
$msginfo->auth_pass(c('amavis_auth_pass'));
$submitter = qquote_rfc2821_local(c('mailfrom_notify_recip'));
}
$msginfo->client_addr($xforward_args{'ADDR'});
$msginfo->client_name($xforward_args{'NAME'});
$msginfo->client_proto($xforward_args{'PROTO'});
$msginfo->client_helo($xforward_args{'HELO'});
%xforward_args = (); if ($args !~ /^FROM: \s*
( < (?: " (?: \\. | [^\\"] )* " | [^"@] )*
(?: @ (?: \[ (?: \\. | [^\]\\] )* \] |
[^\[\]\\>] )* )?
> |
[^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )*
) (?: \s+ ([\040-\176]+) )? \z/isx ) {
$self->smtp_resp(0,"501 5.5.2 Syntax: MAIL FROM: <address>",1,$cmd);
last;
}
my($bad); my($addr,$opt) = ($1,$2);
for (split(' ',$opt)) {
if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* ) =
( [\041-\074\076-\176]+ ) \z/xs) { $self->smtp_resp(0,"501 5.5.4 Syntax error in MAIL FROM parameters",
1,$cmd);
$bad = 1; last;
} else {
my($name,$val) = (uc($1),$2);
if ($name eq 'SIZE' && $val=~/^\d{1,20}\z/) { $msginfo->msg_size($val+0);
if ($message_size_limit && $val > $message_size_limit) {
my($msg) = "552 5.3.4 Declared message size ($val B) exceeds ".
"fixed maximium message size of $message_size_limit B";
do_log(0, $self->{proto}." REJECT 'MAIL FROM': $msg");
$self->smtp_resp(0,$msg, 0,$cmd);
$bad = 1; last;
}
} elsif ($name eq 'BODY' && $val=~/^7BIT|8BITMIME\z/i){
$msginfo->body_type(uc($val));
} elsif ($name eq 'AUTH' && @{ca('auth_mech_avail')} &&
!defined($submitter) ) { $submitter = $val; $submitter =~ s/\+([0-9a-fA-F]{2})/pack("C",hex($1))/eg;
do_log(5, "MAIL command, $authenticated, submitter: $submitter");
} else {
my($msg);
if ($name eq 'AUTH' && !@{ca('auth_mech_avail')}) {
$msg = "503 5.7.4 Error: authentication disabled";
} else {
$msg = "504 5.5.4 MAIL command parameter error: $name=$val";
}
$self->smtp_resp(0,$msg,1,$cmd);
$bad = 1; last;
}
}
}
if (!$bad) {
$addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr;
$self->smtp_resp(0,"250 2.1.0 Sender $addr OK");
$sender = unquote_rfc2821_local($addr);
debug_oneshot(lookup(0,$sender,@{ca('debug_sender_maps')}) ? 1 : 0,
$self->{proto} . "< $cmd");
$submitter = '<>' if !defined($msginfo->auth_user);
$msginfo->auth_submitter($submitter);
};
last;
};
/^RCPT\z/ && do {
if (!defined($sender)) {
$self->smtp_resp(0,"503 5.5.1 Need MAIL command before RCPT",1,$cmd);
@recips = (); $got_rcpt = 0;
last;
}
$got_rcpt++;
if ($args !~ /^TO: \s*
( < (?: " (?: \\. | [^\\"] )* " | [^"@] )*
(?: @ (?: \[ (?: \\. | [^\]\\] )* \] |
[^\[\]\\>] )* )?
> |
[^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )*
) (?: \s+ ([\040-\176]+) )? \z/isx ) {
$self->smtp_resp(0,"501 5.5.2 Syntax: RCPT TO: <address>",1,$cmd);
last;
}
if ($2 ne '') {
$self->smtp_resp(0,"504 5.5.4 RCPT command parameter not implemented: $2",
1, $cmd);
} elsif ($got_rcpt > $smtpd_recipient_limit) {
$self->smtp_resp(0,"452 4.5.3 Too many recipients");
} else {
my($addr,$opt) = ($1, $2);
$addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr;
my($addr_unq) = unquote_rfc2821_local($addr);
my($recip_size_limit); my($mslm) = ca('message_size_limit_maps');
$recip_size_limit = lookup(0,$addr_unq, @$mslm) if @$mslm;
if ($recip_size_limit && $recip_size_limit < 65536)
{ $recip_size_limit = 65536 } if ($recip_size_limit > $max_recip_size_limit)
{ $max_recip_size_limit = $recip_size_limit }
my($mail_size) = $msginfo->msg_size;
if (defined $mail_size && $recip_size_limit && $mail_size > $recip_size_limit) {
my($msg) = "552 5.3.4 Declared message size ($mail_size B) ".
"exceeds fixed maximium message size of $recip_size_limit B, ".
"recipient $addr";
do_log(0, $self->{proto}." REJECT 'RCPT TO': $msg");
$self->smtp_resp(0,$msg, 0,$cmd);
} else {
push(@recips,$addr_unq);
$self->smtp_resp(0,"250 2.1.5 Recipient $addr OK");
}
};
last;
};
/^DATA\z/ && !@recips && do {
if (!defined($sender)) {
$self->smtp_resp(1,"503 5.5.1 Need MAIL command before DATA",1,$cmd);
} elsif (!$got_rcpt) {
$self->smtp_resp(1,"503 5.5.1 Need RCPT command before DATA",1,$cmd);
} elsif ($lmtp) { $self->smtp_resp(1,"503 5.1.1 Error (DATA): no valid recipients",0,$cmd);
} else {
$self->smtp_resp(1,"554 5.1.1 Error (DATA): no valid recipients",0,$cmd);
}
last;
};
/^DATA\z/ && do {
prolong_timer('DATA received - timer reset', $child_timeout);
if ($message_size_limit) { if (!$max_recip_size_limit ||
$max_recip_size_limit > $message_size_limit) {
$max_recip_size_limit = $message_size_limit;
}
}
my($within_data_transfer,$complete);
my($size) = 0; my($over_size) = 0;
eval {
$msginfo->sender($sender); $msginfo->recips(\@recips);
ll(1) && do_log(1, sprintf("%s:%s:%s %s: <%s> -> %s Received: %s",
$conn->smtp_proto,
$conn->socket_ip eq $inet_socket_bind ? ''
: '['.$conn->socket_ip.']',
$conn->socket_port, $self->{tempdir_pers},
$sender, join(',', qquote_rfc2821_local(@recips)),
join(' ', ($msginfo->msg_size eq '' ? ()
: 'SIZE='.$msginfo->msg_size),
($msginfo->body_type eq '' ? ()
: 'BODY='.$msginfo->body_type),
received_line($conn,$msginfo,am_id(),0) )
) );
$self->smtp_resp(1,"354 End data with <CR><LF>.<CR><LF>");
$within_data_transfer = 1;
section_time('SMTP pre-DATA-flush') if $self->{pipelining};
$self->{tempdir_empty} = 0;
do { local($/) = "\015\012"; while(<$sock>) { if (/^\./) {
if ($_ eq ".\015\012")
{ $complete = 1; $within_data_transfer = 0; last }
s/^\.(.+\015\012)\z/$1/s; }
$size += length($_); if (!$over_size) {
chomp; print {$self->{fh_pers}} $_,$eol
or die "Can't write to mail file: $!";
if ($max_recip_size_limit && $size > $max_recip_size_limit) {
do_log(1,"Message size exceeded $max_recip_size_limit B, ".
"skiping further input");
print {$self->{fh_pers}} $eol,"***TRUNCATED***",$eol
or die "Can't write to mail file: $!";
$over_size = 1;
}
}
}
$eof = 1 if !$complete;
}; do_log(4, $self->{proto} . "< .\015\012") if $complete;
$self->{fh_pers}->flush or die "Can't flush mail file: $!";
$self->{fh_pers}->seek(0,1) or die "Can't seek on file: $!";
section_time('SMTP DATA');
};
if ($@ ne '' || !$complete || $over_size) { chomp($@);
if ($over_size && $@ eq '' && !$within_data_transfer) {
my($msg) = "552 5.3.4 Message size ($size B) exceeds ".
"fixed maximium message size of $max_recip_size_limit B";
do_log(0, $self->{proto}." REJECT: $msg");
$self->smtp_resp(0,$msg, 0,$cmd);
} elsif (!$within_data_transfer) {
my($msg) = "Error in processing: " .
!$complete && $@ eq '' ? 'incomplete' : $@;
do_log(-2, $self->{proto}." TROUBLE: 451 4.5.0 $msg");
$self->smtp_resp(1, "451 4.5.0 $msg");
} else {
$aborting = "client broke the connection ".
"during data transfer" if $eof;
$aborting .= ', ' if $aborting ne '' && $@ ne '';
$aborting .= $@;
$aborting = '???' if $aborting eq '';
do_log($@ ne '' ? -1 : 3,
$self->{proto}." TROUBLE, ABORTING: $aborting");
}
} else { $msginfo->mail_tempdir($self->{tempdir_pers});
$msginfo->mail_text_fn($self->{tempdir_pers} . '/email.txt');
$msginfo->mail_text($self->{fh_pers});
my($declared_size) = $msginfo->msg_size;
if (!defined($declared_size)) {
$msginfo->msg_size($size);
} elsif ($size != $declared_size) {
do_log(3,"Actual message size $size B, declared $declared_size B");
$msginfo->msg_size($size);
}
my($smtp_resp, $exit_code, $preserve_evidence) =
&$check_mail($conn,$msginfo, $lmtp,$self->{tempdir_pers});
if ($preserve_evidence) { $self->preserve_evidence(1) }
if ($smtp_resp !~ /^4/ &&
grep { !$_->recip_done } @{$msginfo->per_recip_data}) {
die "TROUBLE: (MISCONFIG) not all recipients done, " .
"forward_method is: " . $msginfo->delivery_method;
}
if (!$lmtp) {
do_log(4, "sending SMTP response: \"$smtp_resp\"");
$self->smtp_resp(0, $smtp_resp);
} else {
my($bounced) = $msginfo->dsn_sent;
for my $r (@{$msginfo->per_recip_data}) {
my($resp) = $r->recip_smtp_response;
if ($bounced && $smtp_resp=~/^2/ && $resp!~/^2/) {
$resp = sprintf("250 2.5.0 Ok, DSN %s (%s)",
$bounced==1 ? 'sent' : 'muted', $resp);
}
do_log(4, sprintf("sending LMTP response for <%s>: \"%s\"",
$r->recip_addr, $resp));
$self->smtp_resp(0, $resp);
}
}
};
alarm(0); do_log(5,"timer stopped after DATA end");
if ($self->preserve_evidence && !$self->{tempdir_empty}) {
do_log(-1,"PRESERVING EVIDENCE in ".$self->{tempdir_pers});
$self->{fh_pers}->close or die "Can't close mail file: $!";
$self->{fh_pers} = undef; $self->{tempdir_pers} = undef;
$self->{tempdir_empty} = 1;
}
if ($self->{fh_pers} && !$can_truncate) {
$self->{fh_pers}->close or die "Can't close mail file: $!";
$self->{fh_pers} = undef;
unlink($self->{tempdir_pers}.'/email.txt')
or die "Can't delete file ".$self->{tempdir_pers}."/email.txt: $!";
section_time('delete email.txt');
}
if (defined $self->{tempdir_pers}) { strip_tempdir($self->{tempdir_pers}); $self->{tempdir_empty} = 1;
}
$sender = undef; @recips = (); $got_rcpt = 0; $max_recip_size_limit = undef; $msginfo = undef; if ($policy_changed)
{ %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
$self->preserve_evidence(0); do_log(2, Amavis::Timing::report());
Amavis::Timing::init(); snmp_counters_init();
last;
}; $self->smtp_resp(1,"502 5.5.1 Error: command ($_) not implemented",1,$cmd);
}; if ($terminating || defined $aborting) { $voluntary_exit = 1; last;
}
$self->smtp_resp_flush;
$0 = sprintf("amavisd (ch%d-%s-idle)",
$Amavis::child_invocation_count, am_id());
Amavis::Timing::go_idle(6);
undef $!;
} my($errn,$errs);
if (!$voluntary_exit) {
$eof = 1;
if (!defined($_)) { $errn = 0+$!; $errs = "$!" }
}
$0 = sprintf("amavisd (ch%d)", $Amavis::child_invocation_count);
Amavis::Timing::go_busy(7);
$self->smtp_resp_flush; my($msg) =
defined $aborting && !$eof ? "ABORTING the session: $aborting" :
defined $aborting ? $aborting :
!$terminating ? "client broke the connection without a QUIT ($errs)" : '';
do_log($aborting?-1:2, $self->{proto}.': NOTICE: '.$msg) if $msg ne '';
if (defined $aborting && !$eof)
{ $self->smtp_resp(1,"421 4.3.2 Service shutting down, ".$aborting) }
$self->{session_closed_normally} = 1;
}
sub smtp_resp($$$;$$) {
my($self, $flush,$resp, $penalize,$line) = @_;
if ($penalize) {
do_log(-1, $self->{proto} . ": $resp; PENALIZE: $line");
sleep 5;
section_time('SMTP penalty wait');
}
$resp = sanitize_str($resp,1);
local($1,$2,$3,$4);
if ($resp !~ /^ ([1-5]\d\d) (\ |-|\z)
([245] \. \d{1,3} \. \d{1,3} (?: \ |\z) )?
(.*) \z/xs)
{ die "Internal error(2): bad SMTP response code: '$resp'" }
my($resp_code,$continuation,$enhanced,$tail) = ($1,$2,$3, $4);
my($lead_len) = length($resp_code) + 1 + length($enhanced);
while (length($tail) > 512-2-$lead_len || $tail =~ /\n/) {
my($head) = substr($tail,0,512-2-$lead_len);
if ($head =~ /^([^\n]*\n)/) { $head = $1 }
$tail = substr($tail,length($head)); chomp($head);
push(@{$self->{smtp_outbuf}}, $resp_code.'-'.$enhanced.$head);
}
push(@{$self->{smtp_outbuf}},$resp_code.$continuation.$enhanced.$tail);
$self->smtp_resp_flush if $flush || !$self->{pipelining} ||
@{$self->{smtp_outbuf}} > 200;
}
sub smtp_resp_flush($) {
my($self) = shift;
if (@{$self->{smtp_outbuf}}) {
for my $resp (@{$self->{smtp_outbuf}}) {
do_log(4, $self->{proto} . "> $resp");
};
my($stat) =
$self->{sock}->print(map { $_."\015\012" } @{$self->{smtp_outbuf}} );
@{$self->{smtp_outbuf}} = (); $stat or die "Error writing a SMTP response to the socket: $!";
}
}
1;
__DATA__
package Amavis::AV;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
@EXPORT_OK = qw(&sophos_savi_init);
}
use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
WEXITSTATUS WTERMSIG WSTOPSIG);
use Errno qw(EPIPE ENOTCONN ENOENT);
use Socket;
use IO::Socket;
use IO::Socket::UNIX;
use subs @EXPORT_OK;
use vars @EXPORT;
BEGIN {
import Amavis::Conf qw(:platform :confvars c cr ca);
import Amavis::Util qw(ll untaint min max do_log am_id
exit_status_str run_command);
import Amavis::Timing qw(section_time);
}
use vars qw(%st_socket_created %st_sock);
sub ask_daemon { ask_av(\&ask_daemon_internal, @_) }
sub clamav_module_init($) {
my($av_name) = @_;
my($clamav_version) = Mail::ClamAV->VERSION;
my($dbdir) = Mail::ClamAV::retdbdir();
my($clamav_obj) = Mail::ClamAV->new($dbdir);
ref $clamav_obj
or die "$av_name: Can't load db from $dbdir: $Mail::ClamAV::Error";
$clamav_obj->buildtrie;
$clamav_obj->maxreclevel($MAXLEVELS) if $MAXLEVELS;
$clamav_obj->maxfiles($MAXFILES);
$clamav_obj->maxfilesize($MAX_EXPANSION_QUOTA || 30*1024*1024);
if ($clamav_version >= 0.12) {
$clamav_obj->maxratio($MAX_EXPANSION_FACTOR);
}
do_log(2,"$av_name init");
section_time('clamav_module_init');
($clamav_obj,$clamav_version);
}
use vars qw($clamav_obj $clamav_version);
sub clamav_module_internal($@) {
my($query, $bare_fnames,$names_to_parts,$tempdir, $av_name) = @_;
if (!defined $clamav_obj) {
($clamav_obj,$clamav_version) = clamav_module_init($av_name); } elsif ($clamav_obj->statchkdir) { do_log(2, "$av_name: reloading virus database");
($clamav_obj,$clamav_version) = clamav_module_init($av_name);
}
my($fname) = "$tempdir/parts/$query"; my($part) = $names_to_parts->{$query}; my($options) = 0; my($opt_archive,$opt_mail);
if ($clamav_version < 0.12) {
$opt_archive = &Mail::ClamAV::CL_ARCHIVE;
$opt_mail = &Mail::ClamAV::CL_MAIL;
} else { $opt_archive = &Mail::ClamAV::CL_SCAN_ARCHIVE;
$opt_mail = &Mail::ClamAV::CL_SCAN_MAIL;
}
$options |= &Mail::ClamAV::CL_SCAN_STDOPT if $clamav_version >= 0.13;
$options |= $opt_archive; $options &= ~$opt_mail; if (ref($part) && (lc($part->type_short) eq 'mail' ||
lc($part->type_declared) eq 'message/rfc822')) {
do_log(2, "$av_name: $query - enabling option CL_MAIL");
$options |= $opt_mail; }
my($ret) = $clamav_obj->scan(untaint($fname), $options);
my($output,$status);
if ($ret->virus) { $status = 1; $output = "INFECTED: $ret" }
elsif ($ret->clean) { $status = 0; $output = "CLEAN" }
else { $status = 2; $output = $ret->error.", errno=".$ret->errno }
($status,$output); }
sub ask_clamav { ask_av(\&clamav_module_internal, @_) }
use vars qw($savi_obj);
sub sophos_savi_init {
my($av_name, $command) = @_;
my(@savi_bool_options) = qw(
FullSweep DynamicDecompression FullMacroSweep OLE2Handling
IgnoreTemplateBit VBA3Handling VBA5Handling OF95DecryptHandling
HelpHandling DecompressVBA5 Emulation PEHandling ExcelFormulaHandling
PowerPointMacroHandling PowerPointEmbeddedHandling ProjectHandling
ZipDecompression ArjDecompression RarDecompression UueDecompression
GZipDecompression TarDecompression CmzDecompression HqxDecompression
MbinDecompression !LoopBackEnabled
Lha SfxArchives MSCabinet TnefAttachmentHandling MSCompress
!DeleteAllMacros Vbe !ExecFileDisinfection VisioFileHandling
ActiveMimeHandling !DelVBA5Project
ScrapObjectHandling SrpStreamHandling Office2001Handling
Upx PalmPilotHandling HqxDecompression
Pdf Rtf Html Elf WordB OutlookExpress
);
my($savi_obj) = SAVI->new;
ref $savi_obj or die "$av_name: Can't create SAVI object, err=$savi_obj";
my($version) = $savi_obj->version;
ref $version or die "$av_name: Can't get SAVI version, err=$version";
do_log(2,sprintf("$av_name init: Version %s (engine %d.%d) recognizing %d viruses\n",
$version->string, $version->major, $version->minor, $version->count));
my($error) = $savi_obj->set('MaxRecursionDepth', $MAXLEVELS, 1);
!defined $error or die "$av_name: error setting MaxRecursionDepth: err=$error";
$error = $savi_obj->set('NamespaceSupport', 3); !defined $error
or do_log(-1,"$av_name: error setting NamespaceSupport: err=$error");
for (@savi_bool_options) {
my($value) = /^!/ ? 0 : 1; s/^!+//;
$error = $savi_obj->set($_, $value);
!defined $error or die "$av_name: Error setting $_: err=$error";
}
section_time('sophos_savi_init');
$savi_obj;
}
sub sophos_savi_internal {
my($query,
$bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args) = @_;
$savi_obj = sophos_savi_init($av_name,$command) if !defined $savi_obj;
my($fname) = "$tempdir/parts/$query"; my($part) = $names_to_parts->{$query}; my($mime_option_value) = 0;
if (ref($part) && (lc($part->type_short) eq 'mail' ||
lc($part->type_declared) eq 'message/rfc822')) {
do_log(2, "$av_name: $query - enabling option MIME");
$mime_option_value = 1;
}
my($error) = $savi_obj->set('MIME', $mime_option_value);
!defined $error or die sprintf("%s: Error %s option MIME: err=%s",
$av_name, $mime_option_value ? 'setting' : 'clearing', $error);
my($output,$status); my($result) = $savi_obj->scan($fname);
if (!ref($result)) { my($msg) = "$av_name: error scanning file $fname, " .
$savi_obj->error_string($result) . " ($result) $!";
if (! grep {$result == $_} (514,527,530,538,549) ) {
$status = 2; $output = "ERROR: $msg\n";
} else { $status = 0; $output = "CLEAN: $msg\n";
}
do_log(-1,$output);
} elsif ($result->infected) {
$status = 1; $output = "INFECTED $query\n";
for my $virus_name ($result->viruses) { $output .= "$virus_name FOUND\n" }
} else {
$status = 0; $output = "CLEAN $query\n";
}
($status,$output); }
sub ask_sophos_savi {
my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
$sts_clean,$sts_infected,$how_to_get_names) = @_;
if (@_ < 3+6) { $args = ["*"]; $sts_clean = [0]; $sts_infected = [1];
$how_to_get_names = qr/^(.*) FOUND$/;
}
ask_av(\&sophos_savi_internal,
$bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
$sts_clean, $sts_infected, $how_to_get_names);
}
sub ask_daemon_internal {
my($query, $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
$sts_clean,$sts_infected,$how_to_get_names, ) = @_;
my($query_template_orig,$sockets) = @$args;
my($output); my($socketname,$is_inet);
if (!ref($sockets)) { $sockets = [ $sockets ] }
my($max_retries) = 2 * @$sockets; my($retries) = 0;
$SIG{PIPE} = 'IGNORE'; for (;;) { @$sockets >= 1 or die "no sockets specified!?"; $socketname = $sockets->[0]; $is_inet = $socketname =~ m{^/} ? 0 : 1; eval {
if (!$st_socket_created{$socketname}) {
ll(3) && do_log(3, "$av_name: Connecting to socket " .
join(' ',$daemon_chroot_dir,$socketname).
(!$retries ? '' : ", retry #$retries") );
if ($is_inet) { $st_sock{$socketname} = IO::Socket::INET->new($socketname)
or die "Can't connect to INET socket $socketname: $!\n";
$st_socket_created{$socketname} = 1;
} else { $st_sock{$socketname} = IO::Socket::UNIX->new(Type => SOCK_STREAM)
or die "Can't create UNIX socket: $!\n";
$st_socket_created{$socketname} = 1;
$st_sock{$socketname}->connect( pack_sockaddr_un($socketname) )
or die "Can't connect to UNIX socket $socketname: $!\n";
}
}
ll(3) && do_log(3,sprintf("$av_name: Sending %s to %s socket %s",
$query, $is_inet?"INET":"UNIX", $socketname));
defined send($st_sock{$socketname}, $query, 0)
or die "Can't send to socket $socketname: $!\n";
if ($av_name =~ /^(Sophie|Trophie)/i) {
defined $st_sock{$socketname}->recv($output, 1024)
or die "Can't receive from socket $socketname: $!\n";
} else {
$output = join('', $st_sock{$socketname}->getlines);
$st_sock{$socketname}->close
or die "Can't close socket $socketname: $!\n";
$st_sock{$socketname}=undef; $st_socket_created{$socketname}=0;
}
$! = undef;
$output ne '' or die "Empty result from $socketname\n";
};
last if $@ eq '';
chomp($@); my($err) = "$!"; my($errn) = 0+$!;
++$retries <= $max_retries
or die "Too many retries to talk to $socketname ($@)";
if ($retries <= 1 && $errn == EPIPE) { do_log(2,"$av_name broken pipe (don't worry), retrying ($retries)");
} else {
do_log( ($retries>1?-1:1), "$av_name: $@, retrying ($retries)");
if ($retries % @$sockets == 0) { my($dly) = min(20, 1 + 5 * ($retries/@$sockets - 1));
do_log(3,"$av_name: sleeping for $dly s");
sleep($dly); }
}
if ($st_socket_created{$socketname}) {
$st_sock{$socketname}->close;
$st_sock{$socketname} = undef; $st_socket_created{$socketname} = 0;
}
push(@$sockets, shift @$sockets) if @$sockets>1; }
(0,$output); }
sub ask_av {
my($code) = shift; my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
$sts_clean,$sts_infected,$how_to_get_names) = @_;
my($query_template) = ref $args eq 'ARRAY' ? $args->[0] : $args;
do_log(5, "ask_av ($av_name): query template1: $query_template");
my($checking_each_file) = $query_template =~ /\*/;
my($scan_status,@virusname); my($output) = '';
for my $f ($checking_each_file ? @$bare_fnames : ("$tempdir/parts")) {
my($query) = $query_template;
if (!$checking_each_file) { $query =~ s[{}][$tempdir/parts]g; do_log(3,"Using ($av_name) on dir: $query");
} else { $query =~ s[ ({}/)? \* ][ $1 eq '' ? $f : "$tempdir/parts/$f" ]gesx;
do_log(3,"Using ($av_name) on file: $query");
}
my($t_status,$t_output) = &$code($query, @_);
do_log(4,"ask_av ($av_name) result: $t_output");
if (ref($sts_infected) eq 'ARRAY'
? (grep {$_==$t_status} @$sts_infected)
: $t_output =~ /$sts_infected/m) { $scan_status = 1; my(@t_virusnames) = ref($how_to_get_names) eq 'CODE'
? &$how_to_get_names($t_output)
: $t_output =~ /$how_to_get_names/gm;
@t_virusnames = map { defined $_ ? $_ : () } @t_virusnames;
push(@virusname, @t_virusnames);
$output .= $t_output . $eol;
do_log(2,"ask_av ($av_name): $f INFECTED: ".join(", ",@t_virusnames));
} elsif (ref($sts_clean) eq 'ARRAY'
? (grep {$_==$t_status} @$sts_clean)
: $t_output =~ /$sts_clean/m) { $scan_status = 0 if !$scan_status; do_log(3,"ask_av ($av_name): $f CLEAN");
} else {
do_log(-2,"ask_av ($av_name) FAILED - unexpected result: $t_output");
last; }
}
if (!@$bare_fnames) { $scan_status = 0 } do_log(3,"$av_name result: clean") if defined($scan_status) && !$scan_status;
($scan_status,$output,\@virusname);
}
sub run_av {
my($bare_fnames, $names_to_parts, $tempdir, $av_name, $command, $args,
$sts_clean, $sts_infected, $how_to_get_names, $pre_code, $post_code, ) = @_;
my($scan_status,$virusnames,$error_str); my($output) = '';
&$pre_code(@_) if defined $pre_code;
if (ref($command) eq 'CODE') {
do_log(3,"Using $av_name: (built-in interface)");
($scan_status,$output,$virusnames) = &$command(@_);
} else {
my(@args) = split(' ',$args);
if (grep { m{^({}/)?\*\z} } @args) { local($1);
@args = map { !m{^({}/)?\*\z} ? $_
: map {$1.untaint($_)} @$bare_fnames } @args;
}
for (@args) { s[{}][$tempdir/parts]g } ll(3) && do_log(3, "Using ($av_name): " . join(' ',$command,@args));
my($proc_fh,$pid) = run_command(undef, "&1", $command, @args);
while( defined($_ = $proc_fh->getline) ) { $output .= $_ }
my($err); $proc_fh->close or $err=$!; my($child_stat) = $?;
$error_str = exit_status_str($child_stat,$err);
my($retval) = WEXITSTATUS($child_stat);
local($1); chomp($output); my($output_trimmed) = $output;
$output_trimmed =~ s/\r\n/\n/gs;
$output_trimmed =~ s/([ \t\n\r])[ \t\n\r]{4,}/$1.../gs;
$output_trimmed = "..." . substr($output_trimmed,-800)
if length($output_trimmed) > 800;
do_log(3, "run_av: $command $error_str, $output_trimmed");
if (!WIFEXITED($child_stat)) {
} elsif (ref($sts_infected) eq 'ARRAY'
? (grep {$_==$retval} @$sts_infected)
: $output =~ /$sts_infected/m) { $virusnames = []; @$virusnames = ref($how_to_get_names) eq 'CODE'
? &$how_to_get_names($output)
: $output =~ /$how_to_get_names/gm;
@$virusnames = map { defined $_ ? $_ : () } @$virusnames;
$scan_status = 1; do_log(2,"run_av ($av_name): INFECTED: ".join(", ",@$virusnames));
} elsif (ref($sts_clean) eq 'ARRAY' ? (grep {$_==$retval} @$sts_clean)
: $output =~ /$sts_clean/m) { $scan_status = 0; do_log(5,"run_av ($av_name): clean");
} else {
$error_str = "unexpected $error_str, output=\"$output_trimmed\"";
}
$output = $output_trimmed if length($output) > 900;
}
&$post_code(@_) if defined $post_code;
$virusnames = [] if !defined $virusnames;
@$virusnames = (undef) if $scan_status && !@$virusnames; if (!defined($scan_status) && defined($error_str)) {
die "$command $error_str"; }
($scan_status, $output, $virusnames);
}
sub virus_scan($$$) {
my($tempdir,$firsttime,$parts_root) = @_;
my($scan_status,$output,@virusname,@detecting_scanners);
my($anyone_done); my($anyone_tried);
my($bare_fnames_ref,$names_to_parts);
my(@errors); my($j); my($tier) = 'primary';
for my $av (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) {
next if !defined $av;
if ($av eq "\000") { last if $anyone_done;
do_log(-2,"WARN: all $tier virus scanners failed, considering backups");
$tier = 'secondary'; next;
}
next if !ref $av || !defined $av->[1];
if (!defined $bare_fnames_ref) { ($bare_fnames_ref,$names_to_parts) =
files_to_scan("$tempdir/parts",$parts_root);
do_log(2, "Not calling virus scanners, ".
"no files to scan in $tempdir/parts") if !@$bare_fnames_ref;
}
$anyone_tried++; my($this_status,$this_output,$this_vn);
if (!@$bare_fnames_ref) { ($this_status,$this_output,$this_vn) = (0, '', []); } else { eval {
($this_status,$this_output,$this_vn) =
run_av($bare_fnames_ref,$names_to_parts,$tempdir, @$av);
};
if ($@ ne '') {
my($err) = $@; chomp($err);
$err = "$av->[0] av-scanner FAILED: $err";
do_log(-2,$err); push(@errors,$err);
$this_status = undef;
};
}
$anyone_done++ if defined $this_status;
$j++; section_time("AV-scan-$j");
if ($this_status) { push(@detecting_scanners, $av->[0]);
if (!@virusname) { @virusname = @$this_vn;
$scan_status = $this_status; $output = $this_output;
}
last if c('first_infected_stops_scan'); } elsif (!defined($scan_status)) { $scan_status = $this_status; $output = $this_output;
}
}
if (@virusname && @detecting_scanners) {
my(@ds) = @detecting_scanners; for (@ds) { s/,/;/ } ll(2) && do_log(2, sprintf("virus_scan: (%s), detected by %d scanners: %s",
join(', ',@virusname), scalar(@ds), join(', ',@ds)));
}
$output =~ s{\Q$tempdir\E/parts/?}{}gs if defined $output; if (!$anyone_tried) { die "NO VIRUS SCANNERS AVAILABLE\n" }
elsif (!$anyone_done)
{ die ("ALL VIRUS SCANNERS FAILED: ".join("; ",@errors)."\n") }
($scan_status, $output, \@virusname, \@detecting_scanners); }
sub files_to_scan($$) {
my($dir,$parts_root) = @_;
local(*DIR); my($f); my($bare_fnames_ref) = [];
opendir(DIR, $dir) or die "Can't open directory $dir: $!";
while (defined($f = readdir(DIR))) {
my($fname) = "$dir/$f";
my($errn) = lstat($fname) ? 0 : 0+$!;
next if $errn == ENOENT;
if ($errn) { die "files_to_scan: file $fname inaccessible: $!" }
if (!-r _) { die "files_to_scan: file $fname not readable" }
next if ($f eq '.' || $f eq '..') && -d _; if (!-f _) { my($what) = -l _ ? 'symlink' : -d _ ? 'directory' : 'non-regular file';
do_log(-1, "WARN: files_to_scan: removing unexpected $what $fname");
unlink(untaint($fname)) or die "Can't delete $what $fname: $!";
} elsif (-z _) {
} else {
if ($f !~ /^[A-Za-z0-9_.-]+\z/s)
{do_log(-1,"WARN: files_to_scan: unexpected/suspicious file name: $f")}
push(@$bare_fnames_ref, $f);
}
}
closedir(DIR) or die "Can't close directory $dir: $!";
my($names_to_parts) = {}; my($part);
for (my(@unvisited)=($parts_root);
@unvisited and $part=shift(@unvisited);
push(@unvisited,@{$part->children}))
{ next if $part eq $parts_root;
my($fname) = $part->base_name;
if (grep {$_ eq $fname} @$bare_fnames_ref) {
$names_to_parts->{$fname} = $part;
} elsif ($part->exists) {
my($type_short) = $part->type_short;
ll(4) && do_log(4,sprintf(
"files_to_scan: info: part %s (%s) no longer present",
$fname, (!ref $type_short ? $type_short : join(', ',@$type_short)) ));
}
}
for my $fname (@$bare_fnames_ref) {
if (!exists $names_to_parts->{$fname}) {
do_log(-1,"files_to_scan: $fname has no corresponding parts object");
}
}
($bare_fnames_ref, $names_to_parts);
}
1;
__DATA__
package Amavis::SpamControl;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
}
use FileHandle;
use Mail::SpamAssassin;
BEGIN {
import Amavis::Conf qw(:platform :sa $daemon_user c cr ca);
import Amavis::Util qw(ll do_log retcode exit_status_str run_command
prolong_timer);
import Amavis::rfc2821_2822_Tools;
import Amavis::Timing qw(section_time);
import Amavis::Lookup qw(lookup);
}
use subs @EXPORT_OK;
use vars qw($spamassassin_obj);
sub init() {
do_log(1, "SpamControl: initializing Mail::SpamAssassin");
my($saved_umask) = umask;
$spamassassin_obj = Mail::SpamAssassin->new({
debug => $sa_debug,
save_pattern_hits => $sa_debug,
dont_copy_prefs => 1,
local_tests_only => $sa_local_tests_only,
home_dir_for_helpers => $helpers_home,
stop_at_threshold => 0,
});
if ($sa_auto_whitelist && Mail::SpamAssassin::Version() < 3) {
do_log(1, "SpamControl: turning on SA auto-whitelisting (AWL)");
my($addrlstfactory) = Mail::SpamAssassin::DBBasedAddrList->new;
$spamassassin_obj->set_persistent_address_list_factory($addrlstfactory);
}
$spamassassin_obj->compile_now; alarm(0); umask($saved_umask); do_log(1, "SpamControl: done");
}
sub white_black_list($$$$$) {
my($conn,$msginfo,$sql_wblist,$user_id_sql,$ldap_policy) = @_;
my($any_w)=0; my($any_b)=0; my($all)=1; my($wr,$br);
my($sender) = $msginfo->sender;
do_log(4,"wbl: checking sender <$sender>");
for my $r (@{$msginfo->per_recip_data}) {
next if $r->recip_done; my($found,$wb,$boost); my($recip) = $r->recip_addr;
my($user_id_ref,$mk_ref) = !defined $sql_wblist ? ([],[])
: lookup(1,$recip,$user_id_sql);
do_log(5,"wbl: (SQL) recip <$recip>, ".scalar(@$user_id_ref)." matches")
if defined $sql_wblist && ll(5);
for my $ind (0..$ my($user_id) = $user_id_ref->[$ind]; my($mkey);
($wb,$mkey) = lookup(0,$sender,
Amavis::Lookup::SQLfield->new($sql_wblist,'wb','S',$user_id) );
do_log(4,"wbl: (SQL) recip <$recip>, rid=$user_id, got: \"$wb\"");
if (!defined($wb)) { } elsif ($wb =~ /^ *([+-]?\d+(?:\.\d*)?) *\z/) { my($val) = 0+$1; $boost += $val;
ll(2) && do_log(2,sprintf(
"wbl: (SQL) soft-%slisted (%s) sender <%s> => <%s> (rid=%s)",
($val<0?'white':'black'), $val, $sender, $recip, $user_id));
$wb = undef; } elsif ($wb =~ /^[ \000]*\z/) { $found++; $wb = 0;
do_log(5,"wbl: (SQL) recip <$recip> is neutral to sender <$sender>");
} elsif ($wb =~ /^([BbNnFf])[ ]*\z/) { $found++; $wb = -1; $any_b++; $br = $recip;
$r->recip_blacklisted_sender(1);
do_log(5,"wbl: (SQL) recip <$recip> blacklisted sender <$sender>");
} else { if ($wb =~ /^([WwYyTt])[ ]*\z/) {
do_log(5, "wbl: (SQL) recip <$recip> whitelisted sender <$sender>");
} else {
do_log(-1,"wbl: (SQL) recip <$recip> whitelisted sender <$sender>, ".
"unexpected wb field value: \"$wb\"");
}
$found++; $wb = +1; $any_w++; $wr = $recip;
$r->recip_whitelisted_sender(1);
}
last if $found;
}
if (!$found && defined($ldap_policy)) {
my($wblist);
my($keys_ref,$rhs_ref) = make_query_keys($sender,0,0);
my(@keys) = @$keys_ref;
do_log(5,sprintf("wbl: (LDAP) query keys: %s",
join(', ',map{"\"$_\""}@keys)));
$wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new($ldap_policy,'amavisBlacklistSender','L-'));
for my $key (@keys) {
if (grep {/^\Q$key\E\z/i} @$wblist) {
$found++; $wb = -1; $br = $recip; $any_b++;
$r->recip_blacklisted_sender(1);
do_log(5,"wbl: (LDAP) recip <$recip> blacklisted sender <$sender>");
}
}
$wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new($ldap_policy,'amavisWhitelistSender','L-'));
for my $key (@keys) {
if (grep {/^\Q$key\E\z/i} @$wblist) {
$found++; $wb = +1; $wr = $recip; $any_w++;
$r->recip_whitelisted_sender(1);
do_log(5,"wbl: (LDAP) recip <$recip> whitelisted sender <$sender>");
}
}
}
if (!$found) { my($val); my($r_ref,$mk_ref,@t);
($r_ref,$mk_ref) = lookup(0,$recip,
Amavis::Lookup::Label->new("blacklist_recip<$recip>"),
cr('per_recip_blacklist_sender_lookup_tables'));
@t = ( (defined $r_ref ? $r_ref : ()), @{ca('blacklist_sender_maps')} );
$val = lookup(0,$sender,
Amavis::Lookup::Label->new("blacklist_sender<$sender>"),
@t) if @t;
if ($val) {
$found++; $wb = -1; $br = $recip; $any_b++;
$r->recip_blacklisted_sender(1);
do_log(5,"wbl: recip <$recip> blacklisted sender <$sender>");
}
($r_ref,$mk_ref) = lookup(0,$recip,
Amavis::Lookup::Label->new("whitelist_recip<$recip>"),
cr('per_recip_whitelist_sender_lookup_tables'));
@t = ( (defined $r_ref ? $r_ref : ()), @{ca('whitelist_sender_maps')} );
$val = lookup(0,$sender,
Amavis::Lookup::Label->new("whitelist_sender<$sender>"),
@t) if @t;
if ($val) {
$found++; $wb = +1; $wr = $recip; $any_w++;
$r->recip_whitelisted_sender(1);
do_log(5,"wbl: recip <$recip> whitelisted sender <$sender>");
}
}
if (!defined($boost)) { my($r_ref,$mk_ref) = lookup(1,$recip,
Amavis::Lookup::Label->new("score_recip<$recip>"),
@{ca('score_sender_maps')});
for my $j (0..$ my($val,$key) = lookup(0,$sender,
Amavis::Lookup::Label->new("score_sender<$sender>"),
@{$r_ref->[$j]} );
if ($val != 0) {
$boost += $val;
ll(2) && do_log(2,
sprintf("wbl: soft-%slisted (%s) sender <%s> => <%s>, ".
"recip_key=\"%s\"", ($val<0?'white':'black'),
$val, $sender, $recip, $mk_ref->[$j]));
}
}
}
$r->recip_score_boost($boost) if defined $boost;
$all = 0 if !$wb;
}
if (!ll(2)) {
} else {
my($msg) = '';
if ($all && $any_w && !$any_b) { $msg = "whitelisted" }
elsif ($all && $any_b && !$any_w) { $msg = "blacklisted" }
elsif ($all) { $msg = "black or whitelisted by all recips" }
elsif ($any_b || $any_w) {
$msg .= "whitelisted by ".($any_w>1?"$any_w recips, ":"$wr, ") if $any_w;
$msg .= "blacklisted by ".($any_b>1?"$any_b recips, ":"$br, ") if $any_b;
$msg .= "but not by all,";
}
do_log(2,"wbl: $msg sender <$sender>") if $msg ne '';
}
($any_w+$any_b, $all);
}
sub spam_scan($$) {
my($conn,$msginfo) = @_;
my($spam_level, $spam_status, $spam_report); my(@lines);
my($hdr_edits) = $msginfo->header_edits;
if (!$hdr_edits) {
$hdr_edits = Amavis::Out::EditHeader->new;
$msginfo->header_edits($hdr_edits);
}
my($dspam_signature,$dspam_result,$dspam_fname);
push(@lines, sprintf("Return-Path: %s\n", qquote_rfc2821_local($msginfo->sender)));
push(@lines, sprintf("X-Envelope-To: %s\n",
join(",\n ",qquote_rfc2821_local(@{$msginfo->recips}))));
my($fh) = $msginfo->mail_text;
my($mbsl) = c('sa_mail_body_size_limit');
if ( defined $mbsl &&
($msginfo->orig_body_size > $mbsl ||
$msginfo->orig_header_size + 1 + $msginfo->orig_body_size
> 5*1024 + $mbsl)
) {
do_log(1,"spam_scan: not wasting time on SA, message ".
"longer than $mbsl bytes: ".
$msginfo->orig_header_size .'+'. $msginfo->orig_body_size);
} else {
if ($dspam eq '') {
do_log(5,"spam_scan: DSPAM not available, skipping it");
} else {
$dspam_fname = $msginfo->mail_tempdir . '/dspam.msg';
my($dspam_fh) = IO::File->new; $dspam_fh->open($dspam_fname,'>',0640)
or die "Can't create file $dspam_fname: $!";
$fh->seek(0,0) or die "Can't rewind mail file: $!";
my($proc_fh,$pid) = run_command('&'.fileno($fh), "&1", $dspam,
qw(--stdout --deliver=spam,innocent
--mode=tum --feature=chained,noise
--enable-signature-headers
--user), $daemon_user,
); my($all_local) = !grep { !lookup(0,$_,@{ca('local_domains_maps')}) }
@{$msginfo->recips};
my($first_line);
while (defined($_ = $proc_fh->getline)) { $dspam_fh->print($_) or die "Can't write to $dspam_fname: $!";
if (!defined($first_line))
{ $first_line = $_; do_log(5,"spam_scan: from DSPAM: $first_line") }
last if $_ eq $eol;
local($1,$2);
if (/^(X-DSPAM[^:]*):[ \t]*(.*)$/) { my($hh,$hb) = ($1,$2);
$dspam_signature = $hb if /^X-DSPAM-Signature:/i;
$dspam_result = $hb if /^X-DSPAM-Result:/i;
do_log(3,$_); push(@lines,$_); $hdr_edits->append_header($hh,$hb) if $all_local;
}
}
while ($proc_fh->read($_,16384) > 0) { $dspam_fh->print($_) or die "Can't write to $dspam_fname: $!";
}
my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
$dspam_fh->close or die "Can't close $dspam_fname: $!";
do_log(-1,sprintf("WARN: DSPAM problem, %s, result=%s",
exit_status_str($?,$err), $first_line)
) if $retval || !defined $first_line;
do_log(4,"spam_scan: DSPAM gave: $dspam_signature, $dspam_result");
section_time('DSPAM');
}
my($body_lines) = 0;
$fh->seek(0,0) or die "Can't rewind mail file: $!";
while (<$fh>) { push(@lines,$_); last if $_ eq $eol } while (<$fh>) { push(@lines,$_); $body_lines++ } section_time('SA msg read');
my($sa_required, $sa_tests);
my($saved_umask) = umask;
my($remaining_time) = alarm(0); eval {
local $SIG{ALRM} = sub {
my($s) = Carp::longmess("SA TIMED OUT, backtrace:");
if (length($s) > 900) { $s = substr($s,0,900-3) . "..." }
do_log(-1,$s);
};
alarm($sa_timeout) if $sa_timeout > 0;
my($mail_obj); my($sa_version) = Mail::SpamAssassin::Version();
do_log(5,"calling SA parse, SA version $sa_version");
if ($sa_version >= 3) {
$mail_obj = $spamassassin_obj->parse(\@lines);
} else { $mail_obj = Mail::SpamAssassin::NoMailAudit->new(data => \@lines,
add_From_line => 0);
}
section_time('SA parse');
do_log(4,"CALLING SA check");
my($per_msg_status);
{ local($1,$2,$3,$4,$5,$6); $per_msg_status = $spamassassin_obj->check($mail_obj);
}
my($rem_t) = alarm(0);
do_log(4,"RETURNED FROM SA check, time left: $rem_t s");
{ local($1,$2,$3,$4); $spam_level = $per_msg_status->get_hits;
$sa_required = $per_msg_status->get_required_hits; $sa_tests = $per_msg_status->get_names_of_tests_hit;
$spam_report = $per_msg_status->get_report;
$per_msg_status->finish();
}
};
section_time('SA check');
umask($saved_umask); prolong_timer('spam_scan_SA', $remaining_time); if ($@ ne '') { chomp($@);
die "$@\n" if $@ ne "timed out";
}
$sa_tests =~ s/,\s*/,/g; $spam_status = "tests=".$sa_tests;
if ($dspam ne '' && defined $spam_level) { my($eat,@options);
@options = (qw(--stdout --mode=tum --user), $daemon_user); if ( $spam_level > 5.0 && $dspam_result eq 'Innocent') {
$eat = 'SPAM'; push(@options, qw(--class=spam --source=error));
}
elsif ($spam_level < 1.0 && $dspam_result eq 'Spam') {
$eat = 'HAM'; push(@options, qw(--class=innocent --source=error));
}
if (defined $eat && $dspam_signature ne '') {
do_log(2,"DSPAM learn $eat ($spam_level), $dspam_signature");
my($proc_fh,$pid) = run_command($dspam_fname, "&1", $dspam, @options);
while (defined($_ = $proc_fh->getline)) {} my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
$retval==0 or die ("DSPAM learn $eat FAILED: ".exit_status_str($?,$err));
section_time('DSPAM learn');
}
}
}
if (defined $dspam_fname) {
if (($spam_level > 5.0 ? 1 : 0) != ($dspam_result eq 'Spam' ? 1 : 0))
{ do_log(2,"DSPAM: different opinions: $dspam_result, $spam_level") }
unlink($dspam_fname) or die "Can't delete file $dspam_fname: $!";
}
do_log(3,"spam_scan: hits=$spam_level $spam_status");
($spam_level, $spam_status, $spam_report);
}
1;
__DATA__
package Amavis::Unpackers;
use strict;
use re 'taint';
BEGIN {
use Exporter ();
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
$VERSION = '2.033';
@ISA = qw(Exporter);
%EXPORT_TAGS = ();
@EXPORT = ();
@EXPORT_OK = qw(&init &decompose_part &determine_file_types);
}
use Errno qw(ENOENT);
use File::Basename qw(basename);
use Convert::TNEF;
use Convert::UUlib qw(:constants);
use Compress::Zlib;
use Archive::Tar;
use Archive::Zip qw(:CONSTANTS :ERROR_CODES);
use File::Copy;
BEGIN {
import Amavis::Util qw(untaint min max ll do_log retcode exit_status_str
snmp_count prolong_timer sanitize_str run_command);
import Amavis::Conf qw(:platform :confvars :unpack c cr ca);
import Amavis::Timing qw(section_time);
import Amavis::Lookup qw(lookup);
import Amavis::Unpackers::MIME qw(mime_decode);
import Amavis::Unpackers::NewFilename qw(consumed_bytes);
}
use subs @EXPORT_OK;
sub flatten_and_tidy_dir($$$;$); sub flatten_and_tidy_dir($$$;$) {
my($dir, $outdir, $parent_obj, $item_num_offset) = @_;
do_log(4, "flatten_and_tidy_dir: processing directory \"$dir\"");
my($cnt_r,$cnt_u) = (0,0); my($consumed_bytes) = 0;
local(*DIR); my($f);
chmod(0750, $dir) or die "Can't change protection of \"$dir\": $!";
opendir(DIR, $dir) or die "Can't open directory \"$dir\": $!";
my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement;
while (defined($f = readdir(DIR))) {
my($msg);
my($errn) = lstat("$dir/$f") ? 0 : 0+$!;
if ($errn == ENOENT) { $msg = "does not exist" }
elsif ($errn) { $msg = "inaccessible: $!" }
if (defined $msg) { die "flatten_and_tidy_dir: \"$dir/$f\" $msg" }
next if ($f eq '.' || $f eq '..') && -d _;
my($newpart_obj) = Amavis::Unpackers::Part->new($outdir,$parent_obj);
$item_num++;
$newpart_obj->mime_placement(sprintf("%s/%d",$parent_placement,
$item_num+$item_num_offset) );
$newpart_obj->name_declared($f); $f = untaint($f);
if (-d _) {
$newpart_obj->attributes_add('D');
my($bytes,$cnt) = flatten_and_tidy_dir("$dir/$f", $outdir, $parent_obj,
$item_num+$item_num_offset);
$consumed_bytes += $bytes; $item_num += $cnt;
} elsif (-l _) {
$cnt_u++; $newpart_obj->attributes_add('L');
unlink("$dir/$f") or die "Can't remove soft link \"$dir/$f\": $!";
} elsif (!-f _) {
do_log(4, "flatten_and_tidy_dir: NONREGULAR FILE \"$dir/$f\"");
$cnt_u++; $newpart_obj->attributes_add('S');
unlink("$dir/$f") or die "Can't remove nonregular file \"$dir/$f\": $!";
} elsif (-z _) {
$cnt_u++;
unlink("$dir/$f") or die "Can't remove empty file \"$dir/$f\": $!";
} else {
chmod(0750, "$dir/$f")
or die "Can't change protection of file \"$dir/$f\": $!";
my($size) = 0 + (-s _);
$newpart_obj->size($size);
$consumed_bytes += $size;
my($newpart) = $newpart_obj->full_name;
do_log(5, "flatten_and_tidy_dir: renaming \"$dir/$f\" to $newpart");
$cnt_r++;
rename("$dir/$f", $newpart)
or die "Can't rename \"$dir/$f\" to $newpart: $!";
}
}
closedir(DIR) or die "Can't close directory \"$dir\": $!";
rmdir($dir) or die "Can't remove directory \"$dir\": $!";
section_time("ren${cnt_r}-unl${cnt_u}-files");
($consumed_bytes, $item_num);
}
sub determine_file_types($$) {
my($tempdir, $partslist_ref) = @_;
$file ne '' or die "Unix utility file(1) not available, but is needed";
my($cwd) = "$tempdir/parts";
my(@part_list) = grep { $_->exists } @$partslist_ref;
if (!@part_list) { do_log(5, "no parts, file(1) not called") }
else {
local($1,$2); my(@file_list);
for my $part (@part_list) {
my($arg) = $part->full_name;
$arg =~ s{^\Q$cwd\E/(.*)\z}{$1}s; push(@file_list, $arg);
}
chdir($cwd) or die "Can't chdir to $cwd: $!";
my($proc_fh,$pid) = run_command(undef, "&1", $file, @file_list);
chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
local($_); my($index) = 0;
while (defined($_ = $proc_fh->getline)) {
chomp;
do_log(5, "result line from file(1): ".$_);
if ($index > $ do_log(-1, "NOTICE: Skipping extra output from file(1): $_");
} else {
my($part) = $part_list[$index]; my($expect) = $file_list[$index]; if (!/^(\Q$expect\E):[ \t]*(.*)\z/s) { do_log(-1,"NOTICE: Skipping bad output from file(1) ".
"at [$index, $expect], got: $_");
} else {
my($type_short); my($actual_name) = $1; my($type_long) = $2;
$type_short = lookup(0,$type_long,@map_full_type_to_short_type_maps);
do_log(4, sprintf("File-type of %s: %s%s",
$part->base_name, $type_long,
(!defined $type_short ? ''
: !ref $type_short ? "; ($type_short)"
: '; (' . join(', ',@$type_short) . ')'
) ));
$part->type_long($type_long); $part->type_short($type_short);
$part->attributes_add('C')
if !ref($type_short) ? $type_short eq 'pgp' : grep {$_ eq 'pgp'} @$type_short;
$index++;
}
}
}
if ($index < @part_list) {
die sprintf("parsing file(1) results - missing last %d results",
@part_list - $index);
}
my($err); $proc_fh->close or $err = $!;
$?==0 or die ("'file' utility ($file) failed, ".exit_status_str($?,$err));
section_time(sprintf('get-file-type%d', scalar(@part_list)));
}
}
sub decompose_mail($$) {
my($tempdir,$file_generator_object) = @_;
my($hold); my(@parts); my($depth) = 1; my($any_undecipherable) = 0;
my($which_section) = "parts_decode";
TIER:
while (@parts = @{$file_generator_object->parts_list}) {
if ($MAXLEVELS && $depth > $MAXLEVELS) {
$hold = "Maximum decoding depth ($MAXLEVELS) exceeded";
last;
}
$file_generator_object->parts_list_reset; my(@chopped_parts) = @parts > 5 ? @parts[0..4] : @parts;
ll(4) && do_log(4,sprintf("decode_parts: level=%d, #parts=%d : %s",
$depth, scalar(@parts),
join(', ', (map { $_->base_name } @chopped_parts),
(@chopped_parts >= @parts ? () : "...")) ));
for my $part (@parts) { my($fname) = $part->full_name;
my($errn) = lstat($fname) ? 0 : 0+$!;
if ($errn == ENOENT) {
$part->exists(0);
} elsif ($errn) {
die "decompose_mail: inaccessible file $fname: $!";
} elsif (!-f _) { my($what) = -l _ ? 'symlink' : -d _ ? 'dir' : 'special';
do_log(-1, "WARN: decompose_mail: removing unexpected $what $fname");
unlink($fname) or die "Can't delete $what $fname: $!";
$part->exists(0);
$part->type_short($what) if !defined $part->type_short;
} elsif (-z _) { unlink($fname) or die "Can't remove \"$fname\": $!";
$part->exists(0);
$part->type_short('empty') if !defined $part->type_short;
$part->type_long('empty') if !defined $part->type_long;
} else {
$part->exists(1);
}
}
determine_file_types($tempdir, \@parts);
for my $part (@parts) {
if ($part->exists && !defined($hold))
{ $hold = decompose_part($part, $tempdir) }
$any_undecipherable++ if grep {$_ eq 'U'} @{ $part->attributes || [] };
}
last TIER if defined $hold;
$depth++;
}
section_time($which_section); prolong_timer($which_section);
($hold, $any_undecipherable);
}
sub decompose_part($$) {
my($part, $tempdir) = @_;
my($hold);
my($none_called);
my($sts) = eval {
my($type_short) = $part->type_short;
my(@ts) = !defined $type_short ? ()
: !ref $type_short ? ($type_short) : @$type_short;
return 0 if !@ts; snmp_count("OpsDecType-".join('.',@ts));
grep(/^mail\z/,@ts) && return do {mime_decode($part,$tempdir,$part); 2};
grep(/^(asc|uue|hqx|ync)\z/,@ts) && return do_ascii($part,$tempdir);
grep(/^F\z/,@ts) && defined $unfreeze
&& return do_uncompress($part,$tempdir,$unfreeze);
grep(/^Z\z/,@ts) && defined $uncompress
&& return do_uncompress($part,$tempdir,$uncompress);
grep(/^bz2?\z/,@ts) && defined $bzip2
&& return do_uncompress($part,$tempdir,"$bzip2 -d");
grep(/^gz\z/,@ts) && defined $gzip
&& return do_uncompress($part,$tempdir,"$gzip -d");
grep(/^gz\z/,@ts) && return do_gunzip($part,$tempdir); grep(/^lzo\z/,@ts) && defined $lzop
&& return do_uncompress($part,$tempdir,"$lzop -d");
grep(/^rpm\z/,@ts) && defined $rpm2cpio && defined $cpio
&& return do_uncompress($part,$tempdir,$rpm2cpio);
grep(/^cpio\z/,@ts) && defined $cpio
&& return do_cpio($part,$tempdir);
grep(/^deb\z/,@ts) && defined $ar
&& return do_ar($part,$tempdir);
grep(/^tar\z/,@ts) && defined $cpio
&& return do_cpio($part,$tempdir);
grep(/^tar\z/,@ts) && return do_tar($part,$tempdir); grep(/^zip\z/,@ts) && return do_unzip($part,$tempdir);
grep(/^rar\z/,@ts) && defined $unrar
&& return do_unrar($part,$tempdir);
grep(/^(lha|lzh)\z/,@ts) && defined $lha
&& return do_lha($part,$tempdir);
grep(/^arc\z/,@ts) && defined $arc
&& return do_arc($part,$tempdir);
grep(/^arj\z/,@ts) && defined $unarj
&& return do_unarj($part,$tempdir);
grep(/^zoo\z/,@ts) && defined $zoo
&& return do_zoo($part,$tempdir);
grep(/^cab\z/,@ts) && defined $cabextract
&& return do_cabextract($part,$tempdir);
grep(/^tnef\z/,@ts) && return do_tnef($part,$tempdir);
grep(/^exe\z/,@ts) && return do_executable($part,$tempdir);
$none_called = 1;
return 0; };
if ($@ ne '') {
chomp($@);
if ($@ =~ /^Exceeded storage quota/ ||
$@ =~ /^Maximum number of files.*exceeded/) { $hold = $@ }
else {
do_log(-1,sprintf("Decoding of %s (%s) failed, leaving it unpacked: %s",
$part->base_name, $part->type_long, $@));
}
$sts = 2;
}
if ($sts == 1 && lookup(0,$part->type_long, @keep_decoded_original_maps)) {
ll(4) && do_log(4,sprintf("file type is %s, retain original %s",
$part->type_long, $part->base_name));
$sts = 2;
}
if ($sts == 1) {
ll(5) && do_log(5, "decompose_part: deleting ".$part->full_name);
unlink($part->full_name)
or die sprintf("Can't unlink %s: %s", $part->full_name, $!);
}
ll(4) && do_log(4,sprintf("decompose_part: %s - %s", $part->base_name,
['atomic','archive, unpacked','source retained']->[$sts]));
section_time('decompose_part') unless $none_called;
$hold;
}
sub do_ascii($$) {
my($part, $tempdir) = @_;
snmp_count('OpsDecByUUlibAttempt');
my($envtmpdir_changed);
if ($ENV{TMPDIR} eq '') { $ENV{TMPDIR} = $TEMPBASE; $envtmpdir_changed = 1 }
my($any_errors,$any_decoded);
eval { my($sts,$count);
$sts = Convert::UUlib::Initialize();
$sts==RET_OK or die "Convert::UUlib::Initialize failed: "
. Convert::UUlib::strerror($sts);
my($uulib_version) = Convert::UUlib::GetOption(OPT_VERSION);
!Convert::UUlib::SetOption(OPT_IGNMODE,1) or die "bad uulib OPT_IGNMODE";
($sts, $count) = Convert::UUlib::LoadFile($part->full_name);
if ($sts != RET_OK) {
my($errmsg) = Convert::UUlib::strerror($sts) . ": $!";
$errmsg .= ", (???"
. Convert::UUlib::strerror(Convert::UUlib::GetOption(OPT_ERRNO))."???)"
if $sts == RET_IOERR;
die "Convert::UUlib::LoadFile (uulib V$uulib_version) failed: $errmsg";
}
ll(4) && do_log(4,sprintf(
"do_ascii: Decoding part %s (%d items), uulib V%s",
$part->base_name, $count, $uulib_version));
my($uu);
my($item_num) = 0; my($parent_placement) = $part->mime_placement;
for (my($j) = 0; $uu = Convert::UUlib::GetFileListItem($j); $j++) {
$item_num++;
ll(4) && do_log(4,sprintf(
"do_ascii(%d): state=0x%02x, enc=%s%s, est.size=%s, name=%s",
$j, $uu->state, Convert::UUlib::strencoding($uu->uudet),
($uu->mimetype ne '' ? ", mimetype=" . $uu->mimetype : ''),
$uu->size, $uu->filename));
if (!($uu->state & FILE_OK)) {
$any_errors++;
do_log(1,"do_ascii: Convert::UUlib info: $j not decodable, ".$uu->state);
} else {
my($newpart_obj)=Amavis::Unpackers::Part->new("$tempdir/parts",$part);
$newpart_obj->mime_placement("$parent_placement/$item_num");
$newpart_obj->name_declared($uu->filename);
my($newpart) = $newpart_obj->full_name;
$! = undef;
$sts = $uu->decode($newpart); my($err_decode) = "$!";
chmod(0750, $newpart) or $! == ENOENT or die "Can't change protection of \"$newpart\": $!";
my($statmsg);
my($errn) = lstat($newpart) ? 0 : 0+$!;
if ($errn == ENOENT) { $statmsg = "does not exist" }
elsif ($errn) { $statmsg = "inaccessible: $!" }
elsif ( -l _) { $statmsg = "is a symlink" }
elsif ( -d _) { $statmsg = "is a directory" }
elsif (!-f _) { $statmsg = "not a regular file" }
if (defined $statmsg) { $statmsg = "; file status: $newpart $statmsg" }
my($size) = 0 + (-s _);
$newpart_obj->size($size);
consumed_bytes($size, 'do_ascii');
if ($sts == RET_OK && $errn==0) {
$any_decoded++;
do_log(4,"do_ascii: RET_OK" . $statmsg) if defined $statmsg;
} elsif ($sts == RET_NODATA || $sts == RET_NOEND) {
$any_errors++;
do_log(-1,"do_ascii: Convert::UUlib error: "
. Convert::UUlib::strerror($sts) . $statmsg);
} else {
$any_errors++;
my($errmsg) = Convert::UUlib::strerror($sts) . ":: $err_decode";
$errmsg .= ", " . Convert::UUlib::strerror(
Convert::UUlib::GetOption(OPT_ERRNO) ) if $sts == RET_IOERR;
die ("Convert::UUlib failed: " . $errmsg . $statmsg);
}
}
}
};
my($eval_stat) = $@;
Convert::UUlib::CleanUp();
snmp_count('OpsDecByUUlib') if $any_decoded;
delete $ENV{TMPDIR} if $envtmpdir_changed; if ($eval_stat ne '') { chomp($eval_stat); die "do_ascii: $eval_stat\n" }
($any_decoded && !$any_errors) ? 1 : $any_errors ? 2 : 0;
}
sub do_unzip($$) {
my($part, $tempdir) = @_;
ll(4) && do_log(4, "Unzipping " . $part->base_name);
snmp_count('OpsDecByArZipAttempt');
my($zip) = Archive::Zip->new;
my(@err_nm) = qw(AZ_OK AZ_STREAM_END AZ_ERROR AZ_FORMAT_ERROR AZ_IO_ERROR);
Archive::Zip::setErrorHandler(sub { return 5 });
my($sts) = $zip->read($part->full_name);
Archive::Zip::setErrorHandler(sub { die @_ });
if ($sts != AZ_OK) {
do_log(4, "do_unzip: not a zip: $err_nm[$sts] ($sts)");
return 0;
}
my($any_unsupp_compmeth,$any_zero_length);
my($encryptedcount,$extractedcount) = (0,0);
my($item_num) = 0; my($parent_placement) = $part->mime_placement;
for my $mem ($zip->members()) {
my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
$item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
$newpart_obj->name_declared($mem->fileName);
my($compmeth) = $mem->compressionMethod;
if ($compmeth != COMPRESSION_DEFLATED && $compmeth != COMPRESSION_STORED) {
$any_unsupp_compmeth = $compmeth;
$newpart_obj->attributes_add('U');
} elsif ($mem->isEncrypted) {
$encryptedcount++;
$newpart_obj->attributes_add('U','C');
} elsif ($mem->isDirectory) {
$newpart_obj->attributes_add('D');
} else {
my($oldc) = $mem->desiredCompressionMethod(COMPRESSION_STORED);
$sts = $mem->rewindData();
$sts == AZ_OK or die sprintf("%s: error rew. member data: %s (%s)",
$part->base_name, $err_nm[$sts], $sts);
my($newpart) = $newpart_obj->full_name;
my($outpart) = IO::File->new;
$outpart->open($newpart,'>') or die "Can't create file $newpart: $!";
binmode($outpart) or die "Can't set file $newpart to binmode: $!";
my($size) = 0;
while ($sts == AZ_OK) {
my($buf_ref);
($buf_ref, $sts) = $mem->readChunk();
$sts == AZ_OK || $sts == AZ_STREAM_END
or die sprintf("%s: error reading member: %s (%s)",
$part->base_name, $err_nm[$sts], $sts);
my($buf_len) = length($$buf_ref);
if ($buf_len > 0) {
$size += $buf_len;
$outpart->print($$buf_ref) or die "Can't write to $newpart: $!";
consumed_bytes($buf_len, 'do_unzip');
}
}
$any_zero_length = 1 if $size == 0;
$newpart_obj->size($size);
$outpart->close or die "Can't close $newpart: $!";
$mem->desiredCompressionMethod($oldc);
$mem->endRead();
$extractedcount++;
}
}
snmp_count('OpsDecByArZip');
my($retval) = 1;
if ($any_unsupp_compmeth) {
$retval = 2;
do_log(-1, sprintf("do_unzip: %s, unsupported compr. method: %s",
$part->base_name, $any_unsupp_compmeth));
} elsif ($any_zero_length) { $retval = 2;
do_log(1, sprintf("do_unzip: %s, zero length members, archive retained",
$part->base_name));
} elsif ($encryptedcount) {
$retval = 2;
do_log(1, sprintf(
"do_unzip: %s, %d members are encrypted, %s extracted, archive retained",
$part->base_name, $encryptedcount,
!$extractedcount ? 'none' : $extractedcount));
}
$retval;
}
sub do_uncompress($$$) {
my($part, $tempdir, $decompressor) = @_;
ll(4) && do_log(4,sprintf("do_uncompress %s by %s",
$part->base_name,$decompressor));
my($decompressor_name) = basename((split(' ',$decompressor))[0]);
snmp_count("OpsDecBy\u${decompressor_name}");
my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
$newpart_obj->mime_placement($part->mime_placement."/1");
my($newpart) = $newpart_obj->full_name;
my($type_short, $name_declared) = ($part->type_short, $part->name_declared);
my(@rn); push(@rn,$1)
if $part->type_long =~ /^\S+\s+compressed data, was "(.+)"(\z|, from\b)/;
for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) {
next if $name_d eq '';
my($name) = $name_d;
for (!ref $type_short ? ($type_short) : @$type_short) {
/^F\z/ and $name=~s/\.F\z//;
/^Z\z/ and $name=~s/\.Z\z// || $name=~s/\.tg?z\z/.tar/;
/^gz\z/ and $name=~s/\.gz\z// || $name=~s/\.tgz\z/.tar/;
/^bz\z/ and $name=~s/\.bz\z// || $name=~s/\.tbz\z/.tar/;
/^bz2\z/ and $name=~s/\.bz2?\z// || $name=~s/\.tbz\z/.tar/;
/^lzo\z/ and $name=~s/\.lzo\z//;
/^rpm\z/ and $name=~s/\.rpm\z/.cpio/;
}
push(@rn,$name) if !grep { $_ eq $name } @rn;
}
$newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
my($proc_fh,$pid) =
run_command($part->full_name, undef, split(' ',$decompressor));
my($rv,$rerr) = run_command_copy($newpart,$proc_fh);
if ($rv) {
die sprintf('Error running decompressor %s on %s, %s',
$decompressor, $part->base_name, exit_status_str($rv,$rerr));
}
1;
}
sub do_gunzip($$) {
my($part, $tempdir) = @_;
do_log(4, "Inflating gzip archive " . $part->base_name);
snmp_count('OpsDecByZlib');
my($gz) = gzopen($part->full_name, "rb")
or die sprintf("do_gunzip: Error opening %s: %s",
$part->full_name, $gzerrno);
my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
$newpart_obj->mime_placement($part->mime_placement."/1");
my($newpart) = $newpart_obj->full_name;
my($outpart) = IO::File->new;
$outpart->open($newpart,'>') or die "Can't create file $newpart: $!";
binmode($outpart) or die "Can't set file $newpart to binmode: $!";
my($buffer); my($size) = 0;
while ($gz->gzread($buffer) > 0) {
$outpart->print($buffer) or die "Can't write to $newpart: $!";
$size += length($buffer);
consumed_bytes(length($buffer), 'do_gunzip');
}
$newpart_obj->size($size);
$outpart->close or die "Can't close $newpart: $!";
my(@rn); my($name_declared) = $part->name_declared;
for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) {
next if $name_d eq '';
my($name) = $name_d;
$name=~s/\.(gz|Z)\z// || $name=~s/\.tgz\z/.tar/;
push(@rn,$name) if !grep { $_ eq $name } @rn;
}
$newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn;
if ($gzerrno != Z_STREAM_END) {
do_log(-1,sprintf("do_gunzip: Error reading %s: %s",
$part->full_name, $gzerrno));
unlink($newpart) or die "Can't unlink $newpart: $!";
$newpart_obj->size(undef);
$gz->gzclose();
return 0;
}
$gz->gzclose();
1;
}
sub do_tar($$) {
my($part, $tempdir) = @_;
snmp_count('OpsDecByArTar');
my $tar = eval { Archive::Tar->new($part->full_name) };
if (!defined($tar)) {
chomp($@);
do_log(4, sprintf("Faulty archive %s: %s", $part->full_name, $@));
return 0;
}
do_log(4,"Untarring ".$part->base_name);
my($item_num) = 0; my($parent_placement) = $part->mime_placement;
my(@list) = $tar->list_files();
for (@list) {
next if /\/\z/; my $data = $tar->get_content($_);
my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
$item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
my($newpart) = $newpart_obj->full_name;
my($outpart) = IO::File->new;
$outpart->open($newpart,'>') or die "Can't create file $newpart: $!";
binmode($outpart) or die "Can't set file $newpart to binmode: $!";
$outpart->print($data) or die "Can't write to $newpart: $!";
$newpart_obj->size(length($data));
consumed_bytes(length($data), 'do_tar');
$outpart->close or die "Can't close $newpart: $!";
}
1;
}
sub do_unrar($$) {
my($part, $tempdir) = @_;
ll(4) && do_log(4, "Attempting to expand RAR archive " . $part->base_name);
my($decompressor_name) = basename((split(' ',$unrar))[0]);
snmp_count("OpsDecBy\u${decompressor_name}Attempt");
my(@common_rar_switches) = qw(-c- -p- -av- -idp);
my($err, $retval, $rv1);
$rv1 =
system($unrar, 't', '-inul', @common_rar_switches, '--', $part->full_name);
$err = $!; $retval = retcode($rv1);
if ($retval == 7) { do_log(-1,"do_unrar: $unrar does not recognize all switches, "
. "it is probably too old. Retrying without '-av- -idp'. "
. "Upgrade: http://www.rarlab.com/");
@common_rar_switches = qw(-c- -p-); $rv1 = system($unrar, 't', '-inul', @common_rar_switches, '--',
$part->full_name);
$err = $!; $retval = retcode($rv1);
}
if (!grep { $_ == $retval } (0,1,3)) {
do_log(4,sprintf("unrar 't' %s, command: %s",
exit_status_str($rv1,$err), $unrar));
return 0;
}
ll(4) && do_log(4, "Expanding RAR archive " . $part->base_name);
my(@list); my($hypcount) = 0; my($encryptedcount) = 0;
my($lcnt) = 0; my($member_name); my($bytes) = 0; my($last_line);
my($item_num) = 0; my($parent_placement) = $part->mime_placement;
my($proc_fh,$pid) =
run_command(undef, "&1", $unrar, 'v', @common_rar_switches, '--',
$part->full_name);
while (defined($_ = $proc_fh->getline)) {
$last_line = $_ if !/^\s*$/; chomp;
if (/^unexpected end of archive/) {
last;
} elsif (/^------/) {
$hypcount++;
last if $hypcount >= 2;
} elsif ($hypcount < 1 && /^Encrypted file:/) {
do_log(4,"do_unrar: ".$_);
$part->attributes_add('U','C');
} elsif ($hypcount == 1) {
$lcnt++;
if ($lcnt % 2 == 0) { if (!/^\s+(\d+)\s+(\d+)\s+(\d+%|-->|<--)/) {
do_log(-1,"do_unrar: can't parse info line for \"$member_name\" $_");
} elsif (defined $member_name) {
do_log(5,"do_unrar: member: \"$member_name\", size: $1");
if ($1 > 0) { $bytes += $1; push(@list, $member_name) }
}
$member_name = undef;
} elsif (/^(.)(.*)\z/s) {
$member_name = $2; if ($1 eq '*') { $encryptedcount++; $item_num++;
my($newpart_obj)=Amavis::Unpackers::Part->new("$tempdir/parts",$part);
$newpart_obj->mime_placement("$parent_placement/$item_num");
$newpart_obj->name_declared($member_name);
$newpart_obj->attributes_add('U','C');
$member_name = undef; }
}
}
}
while (defined($_ = $proc_fh->getline)) { $last_line = $_ if !/^\s*$/ }
$err = undef; $proc_fh->close or $err = $!; $retval = retcode($?);
if ($retval == 3) { do_log(4,"do_unrar: CRC_ERROR - undecipherable");
$part->attributes_add('U');
}
my($fn) = $part->full_name;
if (!$bytes && $retval==0 && $last_line =~ /^\Q$fn\E is not RAR archive$/) {
do_log(4,"do_unrar: ".$last_line);
return 0;
} elsif ($last_line !~ /^\s*(\d+)\s+(\d+)/s) {
do_log(4,"do_unrar: unable to obtain orig total size: $last_line");
} else {
do_log(4,"do_unrar: summary size: $2, sum of sizes: $bytes")
if abs($bytes - $2) > 100;
$bytes = $2 if $2 > $bytes;
}
consumed_bytes($bytes, 'do_unrar-pre', 1); if (!grep { $_ == $retval } (0,1)) { die ("unrar: can't get a list of archive members: " .
exit_status_str($?,$err));
}
snmp_count("OpsDecBy\u${decompressor_name}");
if (!@list) {
do_log(4,"do_unrar: no archive members, or not an archive at all");
} else {
my($proc_fh,$pid) =
run_command(undef, "&1", $unrar, qw(x -inul -ver -o- -kb),
@common_rar_switches, '--',
$part->full_name, "$tempdir/parts/rar/");
my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ }
my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
if (!grep { $_ == $retval } (0,1,3)) { do_log(-1, 'unrar '.exit_status_str($?,$err));
}
my($errn) = lstat("$tempdir/parts/rar") ? 0 : 0+$!;
if ($errn != ENOENT) {
my($b) = flatten_and_tidy_dir("$tempdir/parts/rar","$tempdir/parts",$part);
consumed_bytes($b, 'do_unrar');
}
}
if ($encryptedcount) {
do_log(1, sprintf(
"do_unrar: %s, %d members are encrypted, %s extracted, archive retained",
$part->base_name, $encryptedcount, !@list ? 'none' : 0+@list ));
return 2;
}
1;
}
sub do_lha($$) {
my($part, $tempdir) = @_;
ll(4) && do_log(4, "Attempting to expand LHA archive " . $part->base_name);
my($decompressor_name) = basename((split(' ',$lha))[0]);
snmp_count("OpsDecBy\u${decompressor_name}Attempt");
symlink($part->full_name, $part->full_name.".exe")
or die sprintf("Can't symlink %s %s.exe: %s",
$part->full_name, $part->full_name, $!);
my($checkerr); my($retval) = 1;
my($proc_fh,$pid) = run_command(undef, "&1", $lha, 'lq',
$part->full_name.".exe");
while (defined($_ = $proc_fh->getline)) {
$checkerr = 1 if /Checksum error/i;
}
my($err); $proc_fh->close or $err = $!;
if ($? || $checkerr) {
$retval = 0; do_log(4, "do_lha: not a LHA archive($checkerr) ? ".
exit_status_str($?,$err));
} else {
do_log(4, "Expanding LHA archive " . $part->base_name . ".exe");
snmp_count("OpsDecBy\u${decompressor_name}");
($proc_fh,$pid) =
run_command(undef, undef, $lha, 'lq', $part->full_name.".exe");
my(@list);
while (defined($_ = $proc_fh->getline)) {
chomp;
next if /\/\z/; push(@list, (split(/\s+/))[-1]); }
$err=undef; $proc_fh->close or $err = $!;
$?==0 or do_log(-1, 'do_lha: '.exit_status_str($?,$err));
if (!@list) {
do_log(4, "do_lha: no archive members, or not an archive at all");
} else {
my $rv = store_mgr($tempdir, $part, \@list, $lha, 'pq',
$part->full_name.".exe");
do_log(-1, 'do_lha '.exit_status_str($rv)) if $rv;
$retval = 1; }
}
unlink($part->full_name.".exe")
or die "Can't unlink " . $part->full_name . ".exe: $!";
$retval;
}
sub do_arc($$) {
my($part, $tempdir) = @_;
my($decompressor_name) = basename((split(' ',$arc))[0]);
snmp_count("OpsDecBy\u${decompressor_name}");
my($is_nomarch) = $arc =~ /nomarch/i;
ll(4) && do_log(4,sprintf("Unarcing %s, using %s",
$part->base_name, ($is_nomarch ? "nomarch" : "arc") ));
my($cmdargs) = ($is_nomarch ? "-l -U" : "ln") . " " . $part->full_name;
my($proc_fh,$pid) =
run_command(undef, '/dev/null', $arc, split(' ',$cmdargs));
my(@list) = $proc_fh->getlines;
my($err); $proc_fh->close or $err = $!;
$?==0 or do_log(-1, 'do_arc: '.exit_status_str($?,$err));
map { s/^([^ \t\r\n]*).*\z/$1/s } @list; if (@list) {
my $rv = store_mgr($tempdir, $part, \@list, $arc,
($is_nomarch ? ('-p', '-U') : 'p'), $part->full_name);
do_log(-1, 'arc '.exit_status_str($rv)) if $rv;
}
1;
}
sub do_zoo($$) {
my($part, $tempdir) = @_;
do_log(4, "Expanding ZOO archive " . $part->full_name);
my($decompressor_name) = basename((split(' ',$zoo))[0]);
snmp_count("OpsDecBy\u${decompressor_name}");
symlink($part->full_name, $part->full_name.".zoo")
or die sprintf("Can't symlink %s %s.zoo: %s",
$part->full_name, $part->full_name, $!);
my($proc_fh,$pid) =
run_command(undef, undef, $zoo, 'lf1q', $part->full_name.".zoo");
my(@list) = $proc_fh->getlines;
my($err); $proc_fh->close or $err = $!;
$?==0 or do_log(-1, 'do_zoo: '.exit_status_str($?,$err));
if (@list) {
chomp(@list);
my $rv = store_mgr($tempdir, $part, \@list, $zoo, 'xpqqq:',
$part->full_name . ".zoo");
do_log(-1, 'zoo '.exit_status_str($rv)) if $rv;
}
unlink($part->full_name.".zoo")
or die "Can't unlink " . $part->full_name . ".zoo: $!";
1;
}
sub do_unarj($$) {
my($part, $tempdir) = @_;
do_log(4, "Expanding ARJ archive " . $part->base_name);
my($decompressor_name) = basename((split(' ',$unrar))[0]);
snmp_count("OpsDecBy\u${decompressor_name}");
$ENV{ARJ_SW} = "-i -jo -b5 -2h -jyc -ja1 -gsecret -w$TEMPBASE";
symlink($part->full_name, $part->full_name.".arj")
or die sprintf("Can't symlink %s %s.arj: %s",
$part->full_name, $part->full_name, $!);
my($proc_fh,$pid) =
run_command(undef,'/dev/null', $unarj, 'l', $part->full_name.".arj");
my($last_line);
while (defined($_ = $proc_fh->getline)) { $last_line = $_ if !/^\s*$/ }
my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
if (!grep { $_ == $retval } (0,1,3)) { die ("unarj: can't get a list of archive members: ".
exit_status_str($?,$err));
}
if ($last_line !~ /^\s*(\d+)\s*files\s*(\d+)/s) {
do_log(-1,"do_unarj: WARN: unable to obtain orig size of files: $last_line");
} else {
consumed_bytes($2, 'do_unarj-pre', 1); }
mkdir("$tempdir/parts/arj", 0750) or die "Can't mkdir $tempdir/parts/arj: $!";
chdir("$tempdir/parts/arj") or die "Can't chdir to $tempdir/parts/arj: $!";
($proc_fh,$pid) =
run_command(undef, "&1", $unarj, 'e', $part->full_name.".arj");
my($encryptedcount,$skippedcount) = (0,0);
while (defined($_ = $proc_fh->getline)) {
$encryptedcount++
if /^(Extracting.*\bBad file data or bad password|File is password encrypted, Skipped)\b/s;
$skippedcount++
if /(\bexists|^File is password encrypted|^Unsupported .*), Skipped\b/s;
}
$err = undef; $proc_fh->close or $err = $!; $retval = retcode($?);
chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
if (!grep { $_ == $retval } (0,1,3)) { do_log(0, "unarj: error extracting: ".exit_status_str($?,$err));
}
$part->attributes_add('U') if $skippedcount;
$part->attributes_add('C') if $encryptedcount;
my($errn) = lstat("$tempdir/parts/arj") ? 0 : 0+$!;
if ($errn != ENOENT) {
my($b) = flatten_and_tidy_dir("$tempdir/parts/arj","$tempdir/parts",$part);
consumed_bytes($b, 'do_unarj');
snmp_count("OpsDecBy\u${decompressor_name}");
}
unlink($part->full_name.".arj")
or die "Can't unlink " . $part->full_name . ".arj: $!";
if (!grep { $_ == $retval } (0,1,3)) { die ("unarj: can't extract archive members: ".exit_status_str($?,$err));
}
if ($encryptedcount || $skippedcount) {
do_log(1, sprintf(
"do_unarj: %s, %d members are encrypted, %d skipped, archive retained",
$part->base_name, $encryptedcount, $skippedcount));
return 2;
}
1;
}
sub do_tnef($$) {
my($part, $tempdir) = @_;
do_log(4, "Extracting TNEF attachment " . $part->base_name);
snmp_count('OpsDecByTnef');
chdir("$tempdir/parts") or die "Can't chdir to $tempdir/parts: $!";
my $tnef =
Convert::TNEF->read_in($part->full_name, {ignore_checksum => "true"});
if (!$tnef) {
chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
return 0; }
my($item_num) = 0; my($parent_placement) = $part->mime_placement;
for my $a ($tnef->message, $tnef->attachments) {
if (my $dh = $a->datahandle) {
my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
$item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
$newpart_obj->name_declared([$a->name, $a->longname]);
$newpart_obj->size($a->size);
consumed_bytes($a->size, 'do_tnef');
my($newpart) = $newpart_obj->full_name;
my($outpart) = IO::File->new;
$outpart->open($newpart,'>') or die "Can't create file $newpart: $!";
binmode($outpart) or die "Can't set file $newpart to binmode: $!";
if (defined(my $file = $dh->path)) {
copy($file, $outpart);
} else {
my($s) = $dh->as_string;
$outpart->print($s) or die "Can't write to $newpart: $!";
}
$outpart->close or die "Can't close $newpart: $!";
}
}
$tnef->purge;
chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
1;
}
sub do_cpio($$) {
my($part, $tempdir) = @_;
ll(4) && do_log(4,"Expanding cpio archive ".$part->full_name);
my($decompressor_name) = basename((split(' ',$cpio))[0]);
snmp_count("OpsDecBy\u${decompressor_name}");
my($proc_fh,$pid) = run_command($part->full_name, undef, $cpio, qw(-t -v));
my($bytes) = 0; local($1,$2,$3);
while (defined($_ = $proc_fh->getline)) {
chomp;
next if /^\d+ blocks\z/; if (!/^(?:\S+\s+){4}(\d+)\s+((?:\S+\s+){2}\S+)\s+(.*)\z/) {
do_log(-1,"do_cpio: can't parse toc line: $_");
} else {
do_log(5,"do_cpio: member: \"$3\", size: $1");
$bytes += $1 if $1 > 0;
}
}
while (defined($proc_fh->getline)) { }
my($err); $proc_fh->close or $err = $!;
$?==0 or do_log(-1, 'cpio-1 '.exit_status_str($?,$err));
consumed_bytes($bytes, 'do_cpio-pre', 1); mkdir("$tempdir/parts/cpio", 0750)
or die "Can't mkdir $tempdir/parts/cpio: $!";
chdir("$tempdir/parts/cpio") or die "Can't chdir to $tempdir/parts/cpio: $!";
($proc_fh,$pid) = run_command($part->full_name, '/dev/null', $cpio,
qw(-i -d --no-absolute-filenames --no-preserve-owner));
my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ }
$err = undef; $proc_fh->close or $err = $!;
$?==0 or do_log(-1, 'cpio-2 '.exit_status_str($?,$err).' '.$output);
chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
my($b) = flatten_and_tidy_dir("$tempdir/parts/cpio","$tempdir/parts",$part);
consumed_bytes($b, 'do_cpio');
1;
}
sub do_ar($$) {
my($part, $tempdir) = @_;
ll(4) && do_log(4,"Expanding Unix ar archive ".$part->full_name);
my($decompressor_name) = basename((split(' ',$ar))[0]);
snmp_count("OpsDecBy\u${decompressor_name}");
my($proc_fh,$pid) = run_command(undef, undef, $ar, 'tv', $part->full_name);
my($bytes) = 0; local($1,$2,$3);
while (defined($_ = $proc_fh->getline)) {
chomp;
if (!/^(?:\S+\s+){2}(\d+)\s+((?:\S+\s+){3}\S+)\s+(.*)\z/) {
do_log(-1,"do_ar: can't parse contents listing line: $_");
} else {
do_log(5,"do_ar: member: \"$3\", size: $1");
$bytes += $1 if $1 > 0;
}
}
while (defined($proc_fh->getline)) { }
my($err); $proc_fh->close or $err = $!;
$?==0 or do_log(-1, 'ar-1 '.exit_status_str($?,$err));
consumed_bytes($bytes, 'do_ar-pre', 1); mkdir("$tempdir/parts/ar", 0750)
or die "Can't mkddir $tempdir/parts/ar: $!";
chdir("$tempdir/parts/ar") or die "Can't chdir to $tempdir/parts/ar: $!";
($proc_fh,$pid) = run_command(undef, "&1", $ar, 'x', $part->full_name);
my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ }
$err = undef; $proc_fh->close or $err = $!;
$?==0 or do_log(-1, 'ar-2 '.exit_status_str($?,$err).' '.$output);
chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
my($b) = flatten_and_tidy_dir("$tempdir/parts/ar","$tempdir/parts",$part);
consumed_bytes($b, 'do_ar');
1;
}
sub do_cabextract($$) {
my($part, $tempdir) = @_;
do_log(4, "Expanding cab archive " . $part->base_name);
my($decompressor_name) = basename((split(' ',$cabextract))[0]);
snmp_count("OpsDecBy\u${decompressor_name}");
my($bytes) = 0;
my($proc_fh,$pid) =
run_command(undef,undef,$cabextract,'-l',$part->full_name);
while (defined($_ = $proc_fh->getline)) {
chomp;
next if /^(File size|----|Viewing cabinet:|\z)/;
if (!/^\s* (\d+) \s* \| [^|]* \| \s (.*) \z/x) {
do_log(-1, "do_cabextract: can't parse toc line: $_");
} else {
do_log(5, "do_cabextract: member: \"$2\", size: $1");
$bytes += $1 if $1 > 0;
}
}
while (defined($proc_fh->getline)) { }
my($err); $proc_fh->close or $err = $!;
$?==0 or do_log(-1, 'cabextract-1 '.exit_status_str($?,$err));
consumed_bytes($bytes, 'do_cabextract-pre', 1); mkdir("$tempdir/parts/cab", 0750) or die "Can't mkdir $tempdir/parts/cab: $!";
($proc_fh,$pid) = run_command(undef, '/dev/null', $cabextract, '-q', '-d',
"$tempdir/parts/cab", $part->full_name);
my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ }
$err = undef; $proc_fh->close or $err = $!;
$?==0 or do_log(-1, 'cabextract-2 '.exit_status_str($?,$err).' '.$output);
my($b) = flatten_and_tidy_dir("$tempdir/parts/cab", "$tempdir/parts", $part);
consumed_bytes($b, 'do_cabextract');
1;
}
sub do_executable($$) {
my($part, $tempdir) = @_;
ll(4) && do_log(4,"Check whether ".$part->base_name.
" is a self-extracting archive");
return 2 if eval { do_unzip($part, $tempdir) };
chomp($@);
do_log(-1,"do_executable/do_unzip failed, ignoring: $@") if $@ ne '';
return 2 if defined $unrar && eval { do_unrar($part, $tempdir) };
chomp($@);
do_log(-1,"do_executable/do_unrar failed, ignoring: $@") if $@ ne '';
return 2 if defined $lha && eval { do_lha($part, $tempdir) };
chomp($@);
do_log(-1,"do_executable/do_lha failed, ignoring: $@") if $@ ne '';
return 0;
}
sub run_command_copy($$) {
my($outfile, $ifh) = @_;
my($ofh) = IO::File->new;
$ofh->open($outfile,'>') or die "Can't create file $outfile: $!";
binmode($ofh) or die "Can't set file $outfile to binmode: $!";
binmode($ifh) or die "Can't set binmode on pipe: $!";
my($len, $buf, $offset, $written);
while ($len = $ifh->sysread($buf, 16384)) {
$offset = 0;
while ($len > 0) { $written = syswrite($ofh, $buf, $len, $offset);
defined($written) or die "syswrite to $outfile failed: $!";
consumed_bytes($written, 'run_command_copy');
$len -= $written; $offset += $written;
}
}
my($rerr); $ifh->close or $rerr=$!; my($rv) = $?;
$ofh->close or die "Can't close $outfile: $!";
($rv,$rerr); }
sub store_mgr($$$@) {
my($tempdir, $parent_obj, $list, $cmd, @args) = @_;
my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement;
my(@rv);
for my $f (@$list) {
next if $f =~ m{/\z}; my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",
$parent_obj);
$item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
$newpart_obj->name_declared($f); my($newpart) = $newpart_obj->full_name;
do_log(5,sprintf('store_mgr: extracting "%s" to file %s using %s',
$f, $newpart, $cmd));
if ($f =~ m{^\.?[A-Za-z0-9_][A-Za-z0-9/._=~-]*\z}) { } else { do_log(1, "store_mgr: NOTICE: untainting funny argument \"$f\"");
}
my($proc_fh,$pid) = run_command(undef,undef,$cmd,@args,untaint($f));
my($rv,$rerr) = run_command_copy($newpart,$proc_fh);
do_log(5,"store_mgr: extracted by $cmd, ".exit_status_str($rv,$rerr));
push(@rv, $rv);
}
@rv = grep { $_ != 0 } @rv;
@rv ? $rv[0] : 0; }
1;
__DATA__
[?%[? [?%[? [?%[? [? %2|1] |SPAM|[? [?%, [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%D|,][? %q ||, quarantine: %i][? %Q ||, Queue-ID: %Q][? %m ||, Message-ID: %m][? %r ||, Resent-Message-ID: %r], Hits: %c, %y ms]
[?%[? [?%[? [?%[? [? %2|1] |SPAM|[? [?%, [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%O|,][? %q ||, quarantine: %i][? %Q ||, Queue-ID: %Q][? %m ||, Message-ID: %m][? %r ||, Resent-Message-ID: %r], Hits: %c, %y ms]
__DATA__
[?%[? [?%[? [?%[? [? %2|1] |SPAM|[? [?%, <%o> -> [%D|,], Hits: %c, tag=%3, tag2=%4, kill=%5, %0/%1/%2/%k]
[?%[? [?%[? [?%[? [? %2|1] |SPAM|[? [?%, <%o> -> [%O|,], Hits: %c, tag=%3, tag2=%4, kill=%5, %0/%1/%2/%k]
__DATA__
Subject: Undeliverable mail[?%Message-ID: <DSN%n@%h>
[? %
[%X\n]
]\
This nondelivery report was generated by the amavisd-new program
at host %h. Our internal reference code for your message
is %n.
[? %WHAT IS AN INVALID CHARACTER IN MAIL HEADER?
The RFC 2822 standard specifies rules for forming internet messages.
It does not allow the use of characters with codes above 127 to be used
directly (non-encoded) in mail header (it also prohibits NUL and bare CR).
If characters (e.g. with diacritics) from ISO Latin or other alphabets
need to be included in the header, these characters need to be properly
encoded according to RFC 2047. This encoding is often done transparently
by mail reader (MUA), but if automatic encoding is not available (e.g.
by some older MUA) it is the user's responsibility to avoid the use
of such characters in mail header, or to encode them manually. Typically
the offending header fields in this category are 'Subject', 'Organization',
and comment fields in e-mail addresses of the 'From', 'To' and 'Cc'.
Sometimes such invalid header fields are inserted automatically
by some MUA, MTA, content checker, or other mail handling service.
If this is the case, that service needs to be fixed or properly configured.
Typically the offending header fields in this category are 'Date',
'Received', 'X-Mailer', 'X-Priority', 'X-Scanned', etc.
If you don't know how to fix or avoid the problem, please report it
to _your_ postmaster or system manager.
]\
Return-Path: %s
Your message[?%m|| %m][?%r|| (Resent-Message-ID: %r)]
could not be delivered to:[\n %N]
__DATA__
Subject: [? %[? %m |Message-ID: <VS%n@%h>
[? %
Our content checker found
[? %[? %[? %in email presumably from you (%s),
to the following [? %-> %R]
Our internal reference code for your message is %n.
[? %or ask your system administrator to do so.
][? %
][? %The message has been blocked because it contains a component
(as a MIME part or nested within) with declared name
or MIME type or contents type violating our access policy.
To transfer contents that may be considered risky or unwanted
by site policies, or simply too large for mailing, please consider
publishing your content on the web, and only sending an URL of the
document to the recipient.
Depending on the recipient and sender site policies, with a little
effort it might still be possible to send any contents (including
viruses) using one of the following methods:
- encrypted using pgp, gpg or other encryption methods;
- wrapped in a password-protected or scrambled container or archive
(e.g.: zip -e, arj -g, arc g, rar -p, or other methods)
Note that if the contents is not intended to be secret, the
encryption key or password may be included in the same message
for recipient's convenience.
We are sorry for inconvenience if the contents was not malicious.
The purpose of these restrictions is to cut the most common propagation
methods used by viruses and other malware. These often exploit automatic
mechanisms and security holes in certain mail readers (Microsoft mail
readers and browsers are a common and easy target). By requiring an
explicit and decisive action from the recipient to decode mail,
the dangers of automatic malware propagation is largely reduced.
#
# Details of our mail restrictions policy are available at ...
]]#
For your reference, here are headers from your email:
------------------------- BEGIN HEADERS -----------------------------
Return-Path: %s
[%H
]\
-------------------------- END HEADERS ------------------------------
__DATA__
#
# =============================================================================
# This is a template for non-spam (VIRUS,...) ADMINISTRATOR NOTIFICATIONS.
# For syntax and customization instructions see README.customize.
# Note that only valid header fields are allowed; non-standard header
# field heads must begin with "X-" .
#
Date: %d
From: %f
Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED (%F)]|VIRUS (%V)]#
FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>]
To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
[? %#C |#|Cc: [<%C>|, ]]
Message-ID: <VA%n@%h>
[? %#V |No viruses were found.
|A virus was found: %V
|Two viruses were found:\n %V
|%#V viruses were found:\n %V
]
[? %#F |#\
|A banned name was found:\n %F
|Two banned names were found:\n %F
|%#F banned names were found:\n %F
]
[? %#X |#\
|Bad header was found:[\n %X]
]
[? %#W |#\
|Scanner detecting a virus: %W
|Scanners detecting a virus: %W
]
The mail originated from: <%o>
[? %a |#|First upstream SMTP client IP address: \[%a\] %g
]
[? %t |#|According to the 'Received:' trace, the message originated at:
\[%e\]
%t
]
[? %#S |Notification to sender will not be mailed.
]#
[? %#D |#|The message WILL BE delivered to:[\n%D]
]
[? %#N |#|The message WAS NOT delivered to:[\n%N]
]
[? %#V |#|[? %#v |#|Virus scanner output:[\n %v]
]]
[? %q |Not quarantined.|The message has been quarantined as:\n %q
]
------------------------- BEGIN HEADERS -----------------------------
Return-Path: %s
[%H
]\
-------------------------- END HEADERS ------------------------------
__DATA__
#
# =============================================================================
# This is a template for VIRUS/BANNED/BAD-HEADER RECIPIENTS NOTIFICATIONS.
# For syntax and customization instructions see README.customize.
# Note that only valid header fields are allowed; non-standard header
# field heads must begin with "X-" .
#
Date: %d
From: %f
Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED]|VIRUS (%V)]#
IN MAIL TO YOU (from [?%o|(?)|<%o>])
To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
[? %#C |#|Cc: [<%C>|, ]]
Message-ID: <VR%n@%h>
[? %#V |[? %#F ||BANNED CONTENTS ALERT]|VIRUS ALERT]
Our content checker found
[? %#V |#| [? %#V |viruses|virus|viruses]: %V]
[? %#F |#| banned [? %#F |names|name|names]: %F]
[? %#X |#|\n[%X\n]]
in an email to you [? %S |from unknown sender:|from:]
%o
[? %S |claiming to be: %s|#]
[? %a |#|First upstream SMTP client IP address: \[%a\] %g
]
[? %t |#|According to the 'Received:' trace, the message originated at:
\[%e\]
%t
]
Our internal reference code for this message is %n.
[? %q |Not quarantined.|The message has been quarantined as:
%q]
Please contact your system administrator for details.
__DATA__
#
# =============================================================================
# This is a template for SPAM SENDER NOTIFICATIONS.
# For syntax and customization instructions see README.customize.
# Note that only valid header fields are allowed;
# non-standard header field heads must begin with "X-" .
# The From, To and Date header fields will be provided automatically.
#
Subject: Considered UNSOLICITED BULK EMAIL from you
[? %m |#|In-Reply-To: %m]
Message-ID: <SS%n@%h>
Your message to:[
-> %R]
was considered unsolicited bulk e-mail (UBE).
[? %#X |#|\n[%X\n]]
Subject: %j
Return-Path: %s
Our internal reference code for your message is %n.
[? %#D |Delivery of the email was stopped!
]#
#
# SpamAssassin report:
# [%A
# ]\
__DATA__
#
# =============================================================================
# This is a template for SPAM ADMINISTRATOR NOTIFICATIONS.
# For syntax and customization instructions see README.customize.
# Note that only valid header fields are allowed; non-standard header
# field heads must begin with "X-" .
#
Date: %d
From: %f
Subject: SPAM FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>]
To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
[? %#C |#|Cc: [<%C>|, ]]
[? %#B |#|Bcc: [<%B>|, ]]
Message-ID: <SA%n@%h>
Unsolicited bulk email [? %S |from unknown or forged sender:|from:]
%o
Subject: %j
[? %a |#|First upstream SMTP client IP address: \[%a\] %g
]
[? %t |#|According to the 'Received:' trace, the message originated at:
\[%e\]
%t
]
[? %#D |#|The message WILL BE delivered to:[\n%D]
]
[? %#N |#|The message WAS NOT delivered to:[\n%N]
]
[? %q |Not quarantined.|The message has been quarantined as:\n %q
]
SpamAssassin report:
[%A
]\
------------------------- BEGIN HEADERS -----------------------------
Return-Path: %s
[%H
]\
-------------------------- END HEADERS ------------------------------