[c5c522c] | 1 | #!/usr/bin/perl -T |
---|
| 2 | |
---|
| 3 | #------------------------------------------------------------------------------ |
---|
| 4 | # This is amavisd-new. |
---|
| 5 | # It is an interface between message transfer agent (MTA) and virus |
---|
| 6 | # scanners and/or spam scanners, functioning as a mail content filter. |
---|
| 7 | # |
---|
| 8 | # It is a performance-enhanced and feature-enriched version of amavisd |
---|
| 9 | # (which in turn is a daemonized version of AMaViS), initially based |
---|
| 10 | # on amavisd-snapshot-20020300). |
---|
| 11 | # |
---|
| 12 | # All work since amavisd-snapshot-20020300: |
---|
| 13 | # Copyright (C) 2002,2003,2004 Mark Martinec, All Rights Reserved. |
---|
| 14 | # with contributions from the amavis-* mailing lists and individuals, |
---|
| 15 | # as acknowledged in the release notes. |
---|
| 16 | # |
---|
| 17 | # This program is free software; you can redistribute it and/or modify |
---|
| 18 | # it under the terms of the GNU General Public License as published by |
---|
| 19 | # the Free Software Foundation; either version 2 of the License, or |
---|
| 20 | # (at your option) any later version. |
---|
| 21 | # |
---|
| 22 | # This program is distributed in the hope that it will be useful, |
---|
| 23 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
| 24 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
---|
| 25 | # GNU General Public License for details. |
---|
| 26 | # |
---|
| 27 | # You should have received a copy of the GNU General Public License |
---|
| 28 | # along with this program; if not, write to the Free Software |
---|
| 29 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
---|
| 30 | |
---|
| 31 | # Author: Mark Martinec <mark.martinec@ijs.si> |
---|
| 32 | # Patches and problem reports are welcome. |
---|
| 33 | # |
---|
| 34 | # The latest version of this program is available at: |
---|
| 35 | # http://www.ijs.si/software/amavisd/ |
---|
| 36 | #------------------------------------------------------------------------------ |
---|
| 37 | |
---|
| 38 | # Here is a boilerplate from the amavisd(-snapshot) version, |
---|
| 39 | # which is the version that served as a base code for the initial |
---|
| 40 | # version of amavisd-new. License terms were the same: |
---|
| 41 | # |
---|
| 42 | # Author: Chris Mason <cmason@unixzone.com> |
---|
| 43 | # Current maintainer: Lars Hecking <lhecking@users.sourceforge.net> |
---|
| 44 | # Based on work by: |
---|
| 45 | # Mogens Kjaer, Carlsberg Laboratory, <mk@crc.dk> |
---|
| 46 | # Juergen Quade, Softing GmbH, <quade@softing.com> |
---|
| 47 | # Christian Bricart <shiva@aachalon.de> |
---|
| 48 | # Rainer Link <link@foo.fh-furtwangen.de> |
---|
| 49 | # This script is part of the AMaViS package. For more information see: |
---|
| 50 | # http://amavis.org/ |
---|
| 51 | # Copyright (C) 2000 - 2002 the people mentioned above |
---|
| 52 | # This software is licensed under the GNU General Public License (GPL) |
---|
| 53 | # See: http://www.gnu.org/copyleft/gpl.html |
---|
| 54 | #------------------------------------------------------------------------------ |
---|
| 55 | |
---|
| 56 | #------------------------------------------------------------------------------ |
---|
| 57 | #Index of packages in this file |
---|
| 58 | # Amavis::Boot |
---|
| 59 | # Amavis::Conf |
---|
| 60 | # Amavis::Lock |
---|
| 61 | # Amavis::Log |
---|
| 62 | # Amavis::Timing |
---|
| 63 | # Amavis::Util |
---|
| 64 | # Amavis::rfc2821_2822_Tools |
---|
| 65 | # Amavis::Lookup::RE |
---|
| 66 | # Amavis::Lookup::Label |
---|
| 67 | # Amavis::Lookup |
---|
| 68 | # Amavis::Expand |
---|
| 69 | # Amavis::In::Connection |
---|
| 70 | # Amavis::In::Message::PerRecip |
---|
| 71 | # Amavis::In::Message |
---|
| 72 | # Amavis::Out::EditHeader |
---|
| 73 | # Amavis::Out::Local |
---|
| 74 | # Amavis::Out |
---|
| 75 | # Amavis::UnmangleSender |
---|
| 76 | # Amavis::Unpackers::NewFilename |
---|
| 77 | # Amavis::Unpackers::Part |
---|
| 78 | # Amavis::Unpackers::OurFiler |
---|
| 79 | # Amavis::Unpackers::Validity |
---|
| 80 | # Amavis::Unpackers::MIME |
---|
| 81 | # Amavis::Notify |
---|
| 82 | # Amavis::Cache |
---|
| 83 | # Amavis |
---|
| 84 | #optionally compiled-in packages: --------------------------------------------- |
---|
| 85 | # Amavis::DB::SNMP |
---|
| 86 | # Amavis::DB |
---|
| 87 | # Amavis::Cache |
---|
| 88 | # Amavis::Lookup::SQLfield |
---|
| 89 | # Amavis::Lookup::SQL |
---|
| 90 | # Amavis::Lookup::LDAP |
---|
| 91 | # Amavis::Lookup::LDAPattr |
---|
| 92 | # Amavis::In::AMCL |
---|
| 93 | # Amavis::In::SMTP |
---|
| 94 | # Amavis::In::QMQPqq |
---|
| 95 | # Amavis::AV |
---|
| 96 | # Amavis::SpamControl |
---|
| 97 | # Amavis::Unpackers |
---|
| 98 | #------------------------------------------------------------------------------ |
---|
| 99 | |
---|
| 100 | # |
---|
| 101 | package Amavis::Boot; |
---|
| 102 | use strict; |
---|
| 103 | use re 'taint'; |
---|
| 104 | |
---|
| 105 | # Fetch all required modules (or nicely report missing ones), and compile them |
---|
| 106 | # once-and-for-all at the parent process, so that forked children can inherit |
---|
| 107 | # and share already compiled code in memory. Children will still need to 'use' |
---|
| 108 | # modules if they want to inherit from their name space. |
---|
| 109 | # |
---|
| 110 | sub fetch_modules($$@) { |
---|
| 111 | my($reason, $required, @modules) = @_; |
---|
| 112 | my(@missing); |
---|
| 113 | for my $m (@modules) { |
---|
| 114 | local($_) = $m; |
---|
| 115 | $_ .= /^auto::/ ? '.al' : '.pm' if !/\.(pm|pl|al)\z/; |
---|
| 116 | s[::][/]g; |
---|
| 117 | eval { require $_ } or push(@missing, $m); |
---|
| 118 | } |
---|
| 119 | die "ERROR: MISSING $reason:\n" . join('', map { " $_\n" } @missing) |
---|
| 120 | if $required && @missing; |
---|
| 121 | \@missing; |
---|
| 122 | } |
---|
| 123 | |
---|
| 124 | BEGIN { |
---|
| 125 | fetch_modules('REQUIRED BASIC MODULES', 1, qw( |
---|
| 126 | Exporter POSIX Fcntl Socket Errno Carp Time::HiRes |
---|
| 127 | IO::Handle IO::File IO::Socket IO::Socket::UNIX IO::Socket::INET |
---|
| 128 | IO::Wrap IO::Stringy Digest::MD5 Unix::Syslog File::Basename File::Copy |
---|
| 129 | Mail::Field Mail::Address Mail::Header Mail::Internet |
---|
| 130 | MIME::Base64 MIME::QuotedPrint MIME::Words |
---|
| 131 | MIME::Head MIME::Body MIME::Entity MIME::Parser MIME::Decoder |
---|
| 132 | MIME::Decoder::Base64 MIME::Decoder::Binary MIME::Decoder::QuotedPrint |
---|
| 133 | MIME::Decoder::NBit MIME::Decoder::UU MIME::Decoder::Gzip64 |
---|
| 134 | Net::Cmd Net::SMTP Net::Server Net::Server::PreForkSimple |
---|
| 135 | )); |
---|
| 136 | # with earlier versions of Perl one may need to add additional modules |
---|
| 137 | # to the list, such as: auto::POSIX::setgid auto::POSIX::setuid ... |
---|
| 138 | fetch_modules('OPTIONAL BASIC MODULES', 0, qw( |
---|
| 139 | Carp::Heavy auto::POSIX::setgid auto::POSIX::setuid |
---|
| 140 | MIME::Decoder::BinHex |
---|
| 141 | )); |
---|
| 142 | } |
---|
| 143 | |
---|
| 144 | 1; |
---|
| 145 | |
---|
| 146 | # |
---|
| 147 | package Amavis::Conf; |
---|
| 148 | use strict; |
---|
| 149 | use re 'taint'; |
---|
| 150 | |
---|
| 151 | # prototypes |
---|
| 152 | sub D_REJECT(); |
---|
| 153 | sub D_BOUNCE(); |
---|
| 154 | sub D_DISCARD(); |
---|
| 155 | sub D_PASS(); |
---|
| 156 | |
---|
| 157 | BEGIN { |
---|
| 158 | use Exporter (); |
---|
| 159 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 160 | $VERSION = '2.034'; |
---|
| 161 | @ISA = qw(Exporter); |
---|
| 162 | @EXPORT = (); |
---|
| 163 | @EXPORT_OK = (); |
---|
| 164 | %EXPORT_TAGS = ( |
---|
| 165 | 'dynamic_confvars' => [qw( |
---|
| 166 | $policy_bank_name $protocol @inet_acl |
---|
| 167 | $log_level $log_templ $log_recip_templ $forward_method $notify_method |
---|
| 168 | |
---|
| 169 | $amavis_auth_user $amavis_auth_pass $auth_reauthenticate_forwarded |
---|
| 170 | $auth_required_out $auth_required_inp @auth_mech_avail |
---|
| 171 | $local_client_bind_address |
---|
| 172 | $localhost_name $smtpd_greeting_banner $smtpd_quit_banner |
---|
| 173 | $smtpd_message_size_limit |
---|
| 174 | |
---|
| 175 | $final_virus_destiny $final_spam_destiny |
---|
| 176 | $final_banned_destiny $final_bad_header_destiny |
---|
| 177 | $warnvirussender $warnspamsender $warnbannedsender $warnbadhsender |
---|
| 178 | $warn_offsite |
---|
| 179 | |
---|
| 180 | @av_scanners @av_scanners_backup $first_infected_stops_scan |
---|
| 181 | $bypass_decode_parts |
---|
| 182 | |
---|
| 183 | $defang_virus $defang_banned $defang_spam |
---|
| 184 | $defang_bad_header $defang_undecipherable $defang_all |
---|
| 185 | $undecipherable_subject_tag |
---|
| 186 | $sa_spam_report_header $sa_spam_level_char |
---|
| 187 | $sa_mail_body_size_limit |
---|
| 188 | |
---|
| 189 | $localpart_is_case_sensitive |
---|
| 190 | $recipient_delimiter $replace_existing_extension |
---|
| 191 | $hdr_encoding $bdy_encoding $hdr_encoding_qb |
---|
| 192 | $notify_xmailer_header $X_HEADER_TAG $X_HEADER_LINE |
---|
| 193 | $remove_existing_x_scanned_headers $remove_existing_spam_headers |
---|
| 194 | |
---|
| 195 | $hdrfrom_notify_sender $hdrfrom_notify_recip |
---|
| 196 | $hdrfrom_notify_admin $hdrfrom_notify_spamadmin |
---|
| 197 | $mailfrom_notify_sender $mailfrom_notify_recip |
---|
| 198 | $mailfrom_notify_admin $mailfrom_notify_spamadmin |
---|
| 199 | $mailfrom_to_quarantine |
---|
| 200 | $virus_quarantine_method $spam_quarantine_method |
---|
| 201 | $banned_files_quarantine_method $bad_header_quarantine_method |
---|
| 202 | %local_delivery_aliases |
---|
| 203 | |
---|
| 204 | $notify_sender_templ |
---|
| 205 | $notify_virus_sender_templ $notify_spam_sender_templ |
---|
| 206 | $notify_virus_admin_templ $notify_spam_admin_templ |
---|
| 207 | $notify_virus_recips_templ $notify_spam_recips_templ |
---|
| 208 | |
---|
| 209 | $banned_namepath_re |
---|
| 210 | $per_recip_whitelist_sender_lookup_tables |
---|
| 211 | $per_recip_blacklist_sender_lookup_tables |
---|
| 212 | |
---|
| 213 | @local_domains_maps @mynetworks_maps |
---|
| 214 | @bypass_virus_checks_maps @bypass_spam_checks_maps |
---|
| 215 | @bypass_banned_checks_maps @bypass_header_checks_maps |
---|
| 216 | @virus_lovers_maps @spam_lovers_maps |
---|
| 217 | @banned_files_lovers_maps @bad_header_lovers_maps |
---|
| 218 | @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps |
---|
| 219 | @newvirus_admin_maps @virus_admin_maps |
---|
| 220 | @banned_admin_maps @bad_header_admin_maps @spam_admin_maps |
---|
| 221 | @virus_quarantine_to_maps |
---|
| 222 | @banned_quarantine_to_maps @bad_header_quarantine_to_maps |
---|
| 223 | @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps |
---|
| 224 | @banned_filename_maps |
---|
| 225 | @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps |
---|
| 226 | @spam_dsn_cutoff_level_maps @spam_modifies_subj_maps |
---|
| 227 | @spam_subject_tag_maps @spam_subject_tag2_maps |
---|
| 228 | @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps |
---|
| 229 | @message_size_limit_maps |
---|
| 230 | @addr_extension_virus_maps @addr_extension_spam_maps |
---|
| 231 | @addr_extension_banned_maps @addr_extension_bad_header_maps |
---|
| 232 | @debug_sender_maps |
---|
| 233 | )], |
---|
| 234 | 'confvars' => [qw( |
---|
| 235 | $myproduct_name $myversion_id $myversion_id_numeric $myversion_date |
---|
| 236 | $myversion $myhostname |
---|
| 237 | $MYHOME $TEMPBASE $QUARANTINEDIR |
---|
| 238 | $daemonize $pid_file $lock_file $db_home |
---|
| 239 | $enable_db $enable_global_cache |
---|
| 240 | $daemon_user $daemon_group $daemon_chroot_dir $path |
---|
| 241 | $DEBUG $DO_SYSLOG $SYSLOG_LEVEL $LOGFILE |
---|
| 242 | $max_servers $max_requests $child_timeout |
---|
| 243 | %current_policy_bank %policy_bank %interface_policy |
---|
| 244 | $unix_socketname $inet_socket_port $inet_socket_bind |
---|
| 245 | $insert_received_line $relayhost_is_client $smtpd_recipient_limit |
---|
| 246 | $MAXLEVELS $MAXFILES |
---|
| 247 | $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR |
---|
| 248 | $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR |
---|
| 249 | @lookup_sql_dsn |
---|
| 250 | $sql_select_policy $sql_select_white_black_list |
---|
| 251 | $virus_check_negative_ttl $virus_check_positive_ttl |
---|
| 252 | $spam_check_negative_ttl $spam_check_positive_ttl |
---|
| 253 | $enable_ldap $default_ldap |
---|
| 254 | @keep_decoded_original_maps @map_full_type_to_short_type_maps |
---|
| 255 | @viruses_that_fake_sender_maps |
---|
| 256 | )], |
---|
| 257 | 'unpack' => [qw( |
---|
| 258 | $file $arc $gzip $bzip2 $lzop $lha $unarj $uncompress $unfreeze |
---|
| 259 | $unrar $zoo $pax $cpio $ar $rpm2cpio $cabextract $ripole |
---|
| 260 | )], |
---|
| 261 | 'sa' => [qw( |
---|
| 262 | $helpers_home $dspam |
---|
| 263 | $sa_local_tests_only $sa_auto_whitelist $sa_timeout $sa_debug |
---|
| 264 | )], |
---|
| 265 | 'platform' => [qw( |
---|
| 266 | $can_truncate $unicode_aware $eol |
---|
| 267 | &D_REJECT &D_BOUNCE &D_DISCARD &D_PASS |
---|
| 268 | )], |
---|
| 269 | |
---|
| 270 | # other variables settable by user in amavisd.conf, |
---|
| 271 | # but not directly accessible by the program |
---|
| 272 | 'hidden_confvars' => [qw( |
---|
| 273 | $mydomain |
---|
| 274 | )], |
---|
| 275 | |
---|
| 276 | # legacy variables, predeclared for compatibility of amavisd.conf |
---|
| 277 | # The rest of the program does not use them directly and they should not be |
---|
| 278 | # visible in other modules, but may be referenced throgh @*_maps variables. |
---|
| 279 | 'legacy_confvars' => [qw( |
---|
| 280 | %local_domains @local_domains_acl $local_domains_re @mynetworks |
---|
| 281 | %bypass_virus_checks @bypass_virus_checks_acl $bypass_virus_checks_re |
---|
| 282 | %bypass_spam_checks @bypass_spam_checks_acl $bypass_spam_checks_re |
---|
| 283 | %bypass_banned_checks @bypass_banned_checks_acl $bypass_banned_checks_re |
---|
| 284 | %bypass_header_checks @bypass_header_checks_acl $bypass_header_checks_re |
---|
| 285 | %virus_lovers @virus_lovers_acl $virus_lovers_re |
---|
| 286 | %spam_lovers @spam_lovers_acl $spam_lovers_re |
---|
| 287 | %banned_files_lovers @banned_files_lovers_acl $banned_files_lovers_re |
---|
| 288 | %bad_header_lovers @bad_header_lovers_acl $bad_header_lovers_re |
---|
| 289 | %virus_admin %spam_admin |
---|
| 290 | $newvirus_admin $virus_admin $banned_admin $bad_header_admin $spam_admin |
---|
| 291 | $warnvirusrecip $warnbannedrecip $warnbadhrecip |
---|
| 292 | $virus_quarantine_to $banned_quarantine_to $bad_header_quarantine_to |
---|
| 293 | $spam_quarantine_to $spam_quarantine_bysender_to |
---|
| 294 | $keep_decoded_original_re $map_full_type_to_short_type_re |
---|
| 295 | $banned_filename_re $viruses_that_fake_sender_re |
---|
| 296 | $sa_tag_level_deflt $sa_tag2_level_deflt $sa_kill_level_deflt |
---|
| 297 | $sa_dsn_cutoff_level $sa_spam_modifies_subj |
---|
| 298 | $sa_spam_subject_tag1 $sa_spam_subject_tag |
---|
| 299 | %whitelist_sender @whitelist_sender_acl $whitelist_sender_re |
---|
| 300 | %blacklist_sender @blacklist_sender_acl $blacklist_sender_re |
---|
| 301 | $addr_extension_virus $addr_extension_spam |
---|
| 302 | $addr_extension_banned $addr_extension_bad_header |
---|
| 303 | $gets_addr_in_quoted_form @debug_sender_acl |
---|
| 304 | )], |
---|
| 305 | ); |
---|
| 306 | Exporter::export_tags qw(dynamic_confvars confvars unpack sa platform |
---|
| 307 | hidden_confvars legacy_confvars); |
---|
| 308 | } # BEGIN |
---|
| 309 | |
---|
| 310 | use POSIX qw(uname); |
---|
| 311 | use Carp (); |
---|
| 312 | use Errno qw(ENOENT EACCES); |
---|
| 313 | |
---|
| 314 | use vars @EXPORT; |
---|
| 315 | |
---|
| 316 | sub c($); sub cr($); sub ca($); # prototypes |
---|
| 317 | use subs qw(c cr ca); # access subroutine to new-style config variables |
---|
| 318 | BEGIN { push(@EXPORT,qw(c cr ca)) } |
---|
| 319 | |
---|
| 320 | { # initialize new-style hash (policy bank) containing dynamic config settings |
---|
| 321 | for my $tag (@EXPORT_TAGS{'dynamic_confvars'}) { |
---|
| 322 | for my $v (@$tag) { |
---|
| 323 | if ($v !~ /^([%\$\@])(.*)\z/) { die "Unsupported variable type: $v" } |
---|
| 324 | else { |
---|
| 325 | no strict 'refs'; my($type,$name) = ($1,$2); |
---|
| 326 | $current_policy_bank{$name} = $type eq '$' ? \${"Amavis::Conf::$name"} |
---|
| 327 | : $type eq '@' ? \@{"Amavis::Conf::$name"} |
---|
| 328 | : $type eq '%' ? \%{"Amavis::Conf::$name"} |
---|
| 329 | : undef; |
---|
| 330 | } |
---|
| 331 | } |
---|
| 332 | } |
---|
| 333 | $current_policy_bank{'policy_bank_name'} = ''; # builtin policy |
---|
| 334 | $current_policy_bank{'policy_bank_path'} = ''; |
---|
| 335 | $policy_bank{''} = { %current_policy_bank }; # copy |
---|
| 336 | } |
---|
| 337 | |
---|
| 338 | # new-style access to dynamic config variables |
---|
| 339 | # return a config variable value - usually a scalar; |
---|
| 340 | # one level of indirection for scalars is allowed |
---|
| 341 | sub c($) { |
---|
| 342 | my($name) = @_; |
---|
| 343 | if (!exists $current_policy_bank{$name}) { |
---|
| 344 | Carp::croak(sprintf('No entry "%s" in policy bank "%s"', |
---|
| 345 | $name, $current_policy_bank{'policy_bank_name'})); |
---|
| 346 | } |
---|
| 347 | my($var) = $current_policy_bank{$name}; my($r) = ref($var); |
---|
| 348 | !$r ? $var : $r eq 'SCALAR' ? $$var |
---|
| 349 | : $r eq 'ARRAY' ? @$var : $r eq 'HASH' ? %$var : $var; |
---|
| 350 | } |
---|
| 351 | |
---|
| 352 | # return a ref to a config variable value, or undef if var is undefined |
---|
| 353 | sub cr($) { |
---|
| 354 | my($name) = @_; |
---|
| 355 | if (!exists $current_policy_bank{$name}) { |
---|
| 356 | Carp::croak(sprintf('No entry "%s" in policy bank "%s"', |
---|
| 357 | $name, $current_policy_bank{'policy_bank_name'})); |
---|
| 358 | } |
---|
| 359 | my($var) = $current_policy_bank{$name}; |
---|
| 360 | !defined($var) ? undef : !ref($var) ? \$var : $var; |
---|
| 361 | } |
---|
| 362 | |
---|
| 363 | # return a ref to a config variable value (which is supposed to be an array), |
---|
| 364 | # converting undef to an empty array, and a scalar to a one-element array |
---|
| 365 | # if necessary |
---|
| 366 | sub ca($) { |
---|
| 367 | my($name) = @_; |
---|
| 368 | if (!exists $current_policy_bank{$name}) { |
---|
| 369 | Carp::croak(sprintf('No entry "%s" in policy bank "%s"', |
---|
| 370 | $name, $current_policy_bank{'policy_bank_name'})); |
---|
| 371 | } |
---|
| 372 | my($var) = $current_policy_bank{$name}; |
---|
| 373 | !defined($var) ? [] : !ref($var) ? [$var] : $var; |
---|
| 374 | } |
---|
| 375 | |
---|
| 376 | $myproduct_name = 'amavisd-new'; |
---|
| 377 | $myversion_id = '2.2.1'; $myversion_date = '20041222'; |
---|
| 378 | |
---|
| 379 | $myversion = "$myproduct_name-$myversion_id ($myversion_date)"; |
---|
| 380 | $myversion_id_numeric = # x.yyyzzz, allows numerical comparision, like Perl $] |
---|
| 381 | sprintf("%8.6f", $1 + ($2 + $3/1000)/1000) |
---|
| 382 | if $myversion_id =~ /^(\d+)(?:\.(\d*)(?:\.(\d*))?)?(.*)$/; |
---|
| 383 | |
---|
| 384 | $eol = "\n"; # native record separator in files: LF or CRLF or even CR |
---|
| 385 | $unicode_aware = $]>=5.008 && length("\x{263a}")==1 && eval { require Encode }; |
---|
| 386 | |
---|
| 387 | # serves only as a quick default for other configuration settings |
---|
| 388 | $MYHOME = '/var/amavis'; |
---|
| 389 | $mydomain = '!change-mydomain-variable!.example.com';#intentionally bad default |
---|
| 390 | |
---|
| 391 | # Create debugging output - true: log to stderr; false: log to syslog/file |
---|
| 392 | $DEBUG = 0; |
---|
| 393 | |
---|
| 394 | # Cause Net::Server parameters 'background' and 'setsid' to be set, |
---|
| 395 | # resulting in the program to detach itself from the terminal |
---|
| 396 | $daemonize = 1; |
---|
| 397 | |
---|
| 398 | # Net::Server pre-forking settings - defaults, overruled by amavisd.conf |
---|
| 399 | $max_servers = 2; # number of pre-forked children |
---|
| 400 | $max_requests = 10; # retire a child after that many accepts |
---|
| 401 | |
---|
| 402 | $child_timeout = 8*60; # abort child if it does not complete each task in n sec |
---|
| 403 | |
---|
| 404 | # Can file be truncated? |
---|
| 405 | # Set to 1 if 'truncate' works (it is XPG4-UNIX standard feature, |
---|
| 406 | # not required by Posix). |
---|
| 407 | # Things will go faster with SMTP-in, otherwise (e.g. with milter) |
---|
| 408 | # it makes no difference as file truncation will not be used. |
---|
| 409 | $can_truncate = 1; |
---|
| 410 | |
---|
| 411 | # expiration time of cached results: time to live in seconds |
---|
| 412 | # (how long the result of a virus/spam test remains valid) |
---|
| 413 | $virus_check_negative_ttl= 3*60; # time to remember that mail was not infected |
---|
| 414 | $virus_check_positive_ttl= 30*60; # time to remember that mail was infected |
---|
| 415 | $spam_check_negative_ttl = 30*60; # time to remember that mail was not spam |
---|
| 416 | $spam_check_positive_ttl = 30*60; # time to remember that mail was spam |
---|
| 417 | # |
---|
| 418 | # NOTE: |
---|
| 419 | # Cache size will be determined by the largest of the $*_ttl values. |
---|
| 420 | # Depending on the mail rate, the cache database may grow quite large. |
---|
| 421 | # Reasonable compromise for the max value is 15 minutes to 2 hours. |
---|
| 422 | |
---|
| 423 | # Customizable notification messages, logging |
---|
| 424 | |
---|
| 425 | $SYSLOG_LEVEL = 'mail.debug'; |
---|
| 426 | |
---|
| 427 | $enable_db = 0; # load optional modules Amavis::DB & Amavis::DB::SNMP |
---|
| 428 | $enable_global_cache = 0; # enable use of bdb-based Amavis::Cache |
---|
| 429 | |
---|
| 430 | # Where to find SQL server(s) and database to support SQL lookups? |
---|
| 431 | # A list of triples: (dsn,user,passw). Specify more than one |
---|
| 432 | # for multiple (backup) SQL servers. |
---|
| 433 | # |
---|
| 434 | #@lookup_sql_dsn = |
---|
| 435 | # ( ['DBI:mysql:mail:host1', 'some-username1', 'some-password1'], |
---|
| 436 | # ['DBI:mysql:mail:host2', 'some-username2', 'some-password2'] ); |
---|
| 437 | |
---|
| 438 | # The SQL select clause to fetch per-recipient policy settings |
---|
| 439 | # The %k will be replaced by a comma-separated list of query addresses |
---|
| 440 | # (e.g. full address, domain only, catchall). Use ORDER, if there |
---|
| 441 | # is a chance that multiple records will match - the first match wins |
---|
| 442 | # If field names are not unique (e.g. 'id'), the later field overwrites the |
---|
| 443 | # earlier in a hash returned by lookup, which is why we use '*,users.id'. |
---|
| 444 | $sql_select_policy = |
---|
| 445 | 'SELECT *,users.id FROM users,policy' |
---|
| 446 | . ' WHERE (users.policy_id=policy.id) AND (users.email IN (%k))' |
---|
| 447 | . ' ORDER BY users.priority DESC'; |
---|
| 448 | |
---|
| 449 | # The SQL select clause to check sender in per-recipient whitelist/blacklist |
---|
| 450 | # The first SELECT argument '?' will be users.id from recipient SQL lookup, |
---|
| 451 | # the %k will be sender addresses (e.g. full address, domain only, catchall). |
---|
| 452 | # Only the first occurrence of '?' will be replaced by users.id, subsequent |
---|
| 453 | # occurrences of '?' will see empty string as an argument. There can be zero |
---|
| 454 | # or more occurrences of %k, lookup keys will be multiplied accordingly. |
---|
| 455 | # Up until version 2.2.0 the '?' had to be placed before the '%k'; |
---|
| 456 | # starting with 2.2.1 this restriction is lifted. |
---|
| 457 | $sql_select_white_black_list = |
---|
| 458 | 'SELECT wb FROM wblist,mailaddr' |
---|
| 459 | . ' WHERE (wblist.rid=?) AND (wblist.sid=mailaddr.id)' |
---|
| 460 | . ' AND (mailaddr.email IN (%k))' |
---|
| 461 | . ' ORDER BY mailaddr.priority DESC'; |
---|
| 462 | |
---|
| 463 | # |
---|
| 464 | # Receiving mail related |
---|
| 465 | |
---|
| 466 | # $unix_socketname = '/var/amavis/amavisd.sock'; # traditional amavis client protocol |
---|
| 467 | # $inet_socket_port = 10024; # accept SMTP on this TCP port |
---|
| 468 | # $inet_socket_port = [10024,10026,10027]; # ...possibly on more than one |
---|
| 469 | $inet_socket_bind = '127.0.0.1'; # limit socket bind to loopback interface |
---|
| 470 | |
---|
| 471 | @inet_acl = qw( 127.0.0.1 ::1 ); # allow SMTP access only from localhost |
---|
| 472 | @mynetworks = qw( 127.0.0.0/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 ); |
---|
| 473 | |
---|
| 474 | $notify_method = 'smtp:[127.0.0.1]:10025'; |
---|
| 475 | $forward_method = 'smtp:[127.0.0.1]:10025'; |
---|
| 476 | |
---|
| 477 | $virus_quarantine_method = 'local:virus-%i-%n'; |
---|
| 478 | $spam_quarantine_method = 'local:spam-%b-%i-%n'; |
---|
| 479 | $banned_files_quarantine_method = 'local:banned-%i-%n'; |
---|
| 480 | $bad_header_quarantine_method = 'local:badh-%i-%n'; |
---|
| 481 | |
---|
| 482 | $insert_received_line = 1; # insert 'Received:' header field? (not with milter) |
---|
| 483 | $remove_existing_x_scanned_headers = 0; |
---|
| 484 | $remove_existing_spam_headers = 1; |
---|
| 485 | |
---|
| 486 | # encoding (charset in MIME terminology) |
---|
| 487 | # to be used in RFC 2047-encoded ... |
---|
| 488 | $hdr_encoding = 'iso-8859-1'; # ... header field bodies |
---|
| 489 | $bdy_encoding = 'iso-8859-1'; # ... notification body text |
---|
| 490 | |
---|
| 491 | # encoding (encoding in MIME terminology) |
---|
| 492 | $hdr_encoding_qb = 'Q'; # quoted-printable (default) |
---|
| 493 | #$hdr_encoding_qb = 'B'; # base64 (usual for far east charsets) |
---|
| 494 | |
---|
| 495 | $smtpd_recipient_limit = 1100; # max recipients (RCPT TO) - sanity limit |
---|
| 496 | |
---|
| 497 | # $myhostname is used by SMTP server module in the initial SMTP welcome line, |
---|
| 498 | # in inserted 'Received:' lines, Message-ID in notifications, log entries, ... |
---|
| 499 | $myhostname = (uname)[1]; # should be a FQDN ! |
---|
| 500 | |
---|
| 501 | $smtpd_greeting_banner = '${helo-name} ${protocol} ${product} service ready'; |
---|
| 502 | $smtpd_quit_banner = '${helo-name} ${product} closing transmission channel'; |
---|
| 503 | |
---|
| 504 | # $localhost_name is the name of THIS host running amavisd |
---|
| 505 | # (typically 'localhost'). It is used in HELO SMTP command |
---|
| 506 | # when reinjecting mail back to MTA via SMTP for final delivery. |
---|
| 507 | $localhost_name = 'localhost'; |
---|
| 508 | |
---|
| 509 | # @auth_mech_avail = ('PLAIN','LOGIN'); # empty list disables incoming AUTH |
---|
| 510 | #$auth_required_inp = 1; # incoming SMTP authentication required by amavisd? |
---|
| 511 | #$auth_required_out = 1; # SMTP authentication required by MTA |
---|
| 512 | |
---|
| 513 | # SMTP AUTH username and password for notification submissions |
---|
| 514 | # (and reauthentication of forwarded mail if requested) |
---|
| 515 | #$amavis_auth_user = undef; # perhaps: 'amavisd' |
---|
| 516 | #$amavis_auth_pass = undef; |
---|
| 517 | #$auth_reauthenticate_forwarded = undef; # supply our own credentials also |
---|
| 518 | # for forwarded (passed) mail |
---|
| 519 | |
---|
| 520 | # whom quarantined messages appear to be sent from (envelope sender) |
---|
| 521 | # $mailfrom_to_quarantine = undef; # original sender if undef, or set explicitly |
---|
| 522 | |
---|
| 523 | # where to send quarantined malware |
---|
| 524 | # Specify undef to disable, or e-mail address containing '@', |
---|
| 525 | # or just a local part, which will be mapped by %local_delivery_aliases |
---|
| 526 | # into local mailbox name or directory. The lookup key is a recipient address |
---|
| 527 | $virus_quarantine_to = 'virus-quarantine'; # %local_delivery_aliases mapped |
---|
| 528 | $banned_quarantine_to = 'banned-quarantine'; # %local_delivery_aliases mapped |
---|
| 529 | $bad_header_quarantine_to = 'bad-header-quarantine'; # %local_delivery_aliases |
---|
| 530 | $spam_quarantine_to = 'spam-quarantine'; # %local_delivery_aliases mapped |
---|
| 531 | |
---|
| 532 | $banned_admin = \@virus_admin_maps; # compatibility |
---|
| 533 | $bad_header_admin = \@virus_admin_maps; # compatibility |
---|
| 534 | |
---|
| 535 | # similar to $spam_quarantine_to, but the lookup key is the sender address |
---|
| 536 | $spam_quarantine_bysender_to = undef; # dflt: no by-sender spam quarantine |
---|
| 537 | |
---|
| 538 | # quarantine directory or mailbox file or empty |
---|
| 539 | # (only used if $virus_quarantine_to specifies direct local delivery) |
---|
| 540 | $QUARANTINEDIR = undef; # no quarantine unless overridden by config |
---|
| 541 | |
---|
| 542 | $undecipherable_subject_tag = '***UNCHECKED*** '; |
---|
| 543 | |
---|
| 544 | # string to prepend to Subject header field when message qualifies as spam |
---|
| 545 | # $sa_spam_subject_tag1 = undef; # example: '***possible SPAM*** ' |
---|
| 546 | # $sa_spam_subject_tag = undef; # example: '***SPAM*** ' |
---|
| 547 | $sa_spam_modifies_subj = 1; # true for compatibility; can be a |
---|
| 548 | # lookup table indicating per-recip settings |
---|
| 549 | $sa_spam_level_char = '*'; # character to be used in X-Spam-Level bar; |
---|
| 550 | # empty or undef disables adding this header field |
---|
| 551 | # $sa_spam_report_header = undef; # insert X-Spam-Report header field? |
---|
| 552 | $sa_local_tests_only = 0; |
---|
| 553 | $sa_debug = 0; |
---|
| 554 | $sa_timeout = 30; # timeout in seconds for a call to SpamAssassin |
---|
| 555 | |
---|
| 556 | # MIME defanging is only activated when enabled and malware is allowed to pass |
---|
| 557 | # $defang_virus = undef; |
---|
| 558 | # $defang_banned = undef; |
---|
| 559 | # $defang_spam = undef; |
---|
| 560 | # $defang_bad_header = undef; |
---|
| 561 | # $defang_undecipherable = undef; |
---|
| 562 | # $defang_all = undef; |
---|
| 563 | |
---|
| 564 | $MIN_EXPANSION_FACTOR = 5; # times original mail size |
---|
| 565 | $MAX_EXPANSION_FACTOR = 500; # times original mail size |
---|
| 566 | |
---|
| 567 | # See amavisd.conf and README.lookups for details. |
---|
| 568 | |
---|
| 569 | # What to do with the message (this is independent of quarantining): |
---|
| 570 | # Reject: tell MTA to generate a non-delivery notification, MTA gets 5xx |
---|
| 571 | # Bounce: generate a non-delivery notification by ourselves, MTA gets 250 |
---|
| 572 | # Discard: drop the message and pretend it was delivered, MTA gets 250 |
---|
| 573 | # Pass: deliver/accept the message |
---|
| 574 | # |
---|
| 575 | # Bounce and Reject are similar: in both cases sender gets a non-delivery |
---|
| 576 | # notification, either generated by amavisd-new, or by MTA. The notification |
---|
| 577 | # issued by amavisd-new may be more informative, while on the other hand |
---|
| 578 | # MTA may be able to do a true reject on the original SMTP session |
---|
| 579 | # (e.g. with sendmail milter), or else it just generates normal non-delivery |
---|
| 580 | # notification / bounce (e.g. with Postfix, Exim). As a consequence, |
---|
| 581 | # with Postfix and Exim and dual-sendmail setup the Bounce is more informative |
---|
| 582 | # than Reject, but sendmail-milter users may prefer Reject. |
---|
| 583 | # |
---|
| 584 | # Bounce and Discard are similar: in both cases amavisd-new confirms |
---|
| 585 | # to MTA the message reception with success code 250. The difference is |
---|
| 586 | # in sender notification: Bounce sends a non-delivery notification to sender, |
---|
| 587 | # Discard does not, the message is silently dropped. Quarantine and |
---|
| 588 | # admin notifications are not affected by any of these settings. |
---|
| 589 | # |
---|
| 590 | # COMPATIBITITY NOTE: the separation of *_destiny values into |
---|
| 591 | # D_BOUNCE, D_REJECT, D_DISCARD and D_PASS made settings $warnvirussender |
---|
| 592 | # and $warnspamsender only still useful with D_PASS. The combination of |
---|
| 593 | # D_DISCARD + $warn*sender=1 is mapped into D_BOUNCE for compatibility. |
---|
| 594 | |
---|
| 595 | # intentionally leave value -1 unassigned for compatibility |
---|
| 596 | sub D_REJECT () { -3 } |
---|
| 597 | sub D_BOUNCE () { -2 } |
---|
| 598 | sub D_DISCARD() { 0 } |
---|
| 599 | sub D_PASS () { 1 } |
---|
| 600 | |
---|
| 601 | # The following symbolic constants can be used in *destiny settings: |
---|
| 602 | # |
---|
| 603 | # D_PASS mail will pass to recipients, regardless of contents; |
---|
| 604 | # |
---|
| 605 | # D_DISCARD mail will not be delivered to its recipients, sender will NOT be |
---|
| 606 | # notified. Effectively we lose mail (but it will be quarantined |
---|
| 607 | # unless disabled). Not a decent thing to do for a mailer. |
---|
| 608 | # |
---|
| 609 | # D_BOUNCE mail will not be delivered to its recipients, a non-delivery |
---|
| 610 | # notification (bounce) will be sent to the sender by amavisd-new; |
---|
| 611 | # Exception: bounce (DSN) will not be sent if a virus name matches |
---|
| 612 | # $viruses_that_fake_sender_maps, or to messages from mailing lists |
---|
| 613 | # (Precedence: bulk|list|junk), or for spam exceeding |
---|
| 614 | # spam_dsn_cutoff_level |
---|
| 615 | # |
---|
| 616 | # D_REJECT mail will not be delivered to its recipients, sender should |
---|
| 617 | # preferably get a reject, e.g. SMTP permanent reject response |
---|
| 618 | # (e.g. with milter), or non-delivery notification from MTA |
---|
| 619 | # (e.g. Postfix). If this is not possible (e.g. different recipients |
---|
| 620 | # have different tolerances to bad mail contents and not using LMTP) |
---|
| 621 | # amavisd-new sends a bounce by itself (same as D_BOUNCE). |
---|
| 622 | # |
---|
| 623 | # Notes: |
---|
| 624 | # D_REJECT and D_BOUNCE are similar, the difference is in who is responsible |
---|
| 625 | # for informing the sender about non-delivery, and how informative |
---|
| 626 | # the notification can be (amavisd-new knows more than MTA); |
---|
| 627 | # With D_REJECT, MTA may reject original SMTP, or send DSN (delivery status |
---|
| 628 | # notification, colloquially called 'bounce') - depending on MTA; |
---|
| 629 | # Best suited for sendmail milter, especially for spam. |
---|
| 630 | # With D_BOUNCE, amavisd-new (not MTA) sends DSN (can better explain the |
---|
| 631 | # reason for mail non-delivery but unable to reject the original |
---|
| 632 | # SMTP session, is in position to suppress DSN if considered |
---|
| 633 | # unsuitable). Best suited for Postfix and other dual-MTA setups. |
---|
| 634 | |
---|
| 635 | $final_virus_destiny = D_DISCARD; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS |
---|
| 636 | $final_banned_destiny = D_BOUNCE; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS |
---|
| 637 | $final_spam_destiny = D_BOUNCE; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS |
---|
| 638 | $final_bad_header_destiny = D_PASS; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS |
---|
| 639 | |
---|
| 640 | # If you decide to pass viruses (or spam) to certain users using |
---|
| 641 | # %virus_lovers/@virus_lovers_acl/$virus_lovers_re, (or *spam_lovers*), |
---|
| 642 | # %bypass_virus_checks/@bypass_virus_checks_acl, or $final_virus_destiny=D_PASS |
---|
| 643 | # ($final_spam_destiny=D_PASS), you can set the variable $addr_extension_virus |
---|
| 644 | # ($addr_extension_spam) to some string, and the recipient address will have |
---|
| 645 | # this string appended as an address extension to the local-part of the |
---|
| 646 | # address. This extension can be used by final local delivery agent to place |
---|
| 647 | # such mail in different folders. Leave these variables undefined or empty |
---|
| 648 | # strings to prevent appending address extensions. Setting has no effect |
---|
| 649 | # on users which will not be receiving viruses (spam). Recipients which |
---|
| 650 | # do not match access lists in @local_domains_maps are not affected (i.e. |
---|
| 651 | # non-local recipients). |
---|
| 652 | # |
---|
| 653 | # LDAs usually default to stripping away address extension if no special |
---|
| 654 | # handling for it is specified, so having this option enabled normally |
---|
| 655 | # does no harm, provided the $recipients_delimiter character matches |
---|
| 656 | # the setting at the final MTA's local delivery agent (LDA). |
---|
| 657 | # |
---|
| 658 | # $addr_extension_virus = 'virus'; # for example |
---|
| 659 | # $addr_extension_spam = 'spam'; |
---|
| 660 | # $addr_extension_banned = 'banned'; |
---|
| 661 | # $addr_extension_bad_header = 'badh'; |
---|
| 662 | |
---|
| 663 | # Delimiter between local part of the recipient address and address extension |
---|
| 664 | # (which can optionally be added, see variables $addr_extension_virus and |
---|
| 665 | # $addr_extension_spam). E.g. recipient address <user@domain.example> gets |
---|
| 666 | # changed to <user+virus@domain.example>. |
---|
| 667 | # |
---|
| 668 | # Delimiter should match equivalent (final) MTA delimiter setting. |
---|
| 669 | # (e.g. for Postfix add 'recipient_delimiter = +' to main.cf). |
---|
| 670 | # Setting it to an empty string or to undef disables this feature |
---|
| 671 | # regardless of $addr_extension_virus and $addr_extension_spam settings. |
---|
| 672 | |
---|
| 673 | # $recipient_delimiter = '+'; |
---|
| 674 | $replace_existing_extension = 1; # true: replace ext; false: append ext |
---|
| 675 | |
---|
| 676 | # Affects matching of localpart of e-mail addresses (left of '@') |
---|
| 677 | # in lookups: true = case sensitive, false = case insensitive |
---|
| 678 | $localpart_is_case_sensitive = 0; |
---|
| 679 | |
---|
| 680 | # first match wins, more specific entries should precede general ones! |
---|
| 681 | # the result may be a string or a ref to a list of strings; |
---|
| 682 | # see also sub decompose_part() |
---|
| 683 | $map_full_type_to_short_type_re = Amavis::Lookup::RE->new( |
---|
| 684 | [qr/^empty\z/ => 'empty'], |
---|
| 685 | [qr/^directory\z/ => 'dir'], |
---|
| 686 | [qr/^can't (stat|read)\b/ => 'dat'], # file(1) diagnostics |
---|
| 687 | [qr/^cannot open\b/ => 'dat'], # file(1) diagnostics |
---|
| 688 | [qr/^ERROR: Corrupted\b/ => 'dat'], # file(1) diagnostics |
---|
| 689 | [qr/can't read magic file|couldn't find any magic files/ => 'dat'], |
---|
| 690 | [qr/^data\z/ => 'dat'], |
---|
| 691 | |
---|
| 692 | [qr/^ISO-8859.*\btext\b/ => 'txt'], |
---|
| 693 | [qr/^Non-ISO.*ASCII\b.*\btext\b/ => 'txt'], |
---|
| 694 | [qr/^Unicode\b.*\btext\b/i => 'txt'], |
---|
| 695 | [qr/^'diff' output text\b/ => 'txt'], |
---|
| 696 | [qr/^GNU message catalog\b/ => 'mo'], |
---|
| 697 | [qr/^PGP encrypted data\b/ => 'pgp'], |
---|
| 698 | [qr/^PGP armored data( signed)? message\b/ => ['pgp','pgp.asc'] ], |
---|
| 699 | [qr/^PGP armored\b/ => ['pgp','pgp.asc'] ], |
---|
| 700 | |
---|
| 701 | ### 'file' is a bit too trigger happy to claim something is 'mail text' |
---|
| 702 | # [qr/^RFC 822 mail text\b/ => 'mail'], |
---|
| 703 | [qr/^(ASCII|smtp|RFC 822) mail text\b/ => 'txt'], |
---|
| 704 | |
---|
| 705 | [qr/^JPEG image data\b/ =>['image','jpg'] ], |
---|
| 706 | [qr/^GIF image data\b/ =>['image','gif'] ], |
---|
| 707 | [qr/^PNG image data\b/ =>['image','png'] ], |
---|
| 708 | [qr/^TIFF image data\b/ =>['image','tif'] ], |
---|
| 709 | [qr/^PCX\b.*\bimage data\b/ =>['image','pcx'] ], |
---|
| 710 | [qr/^PC bitmap data\b/ =>['image','bmp'] ], |
---|
| 711 | |
---|
| 712 | [qr/^MP2\b/ =>['audio','mpa','mp2'] ], |
---|
| 713 | [qr/^MP3\b/ =>['audio','mpa','mp3'] ], |
---|
| 714 | [qr/^MPEG video stream data\b/ =>['movie','mpv'] ], |
---|
| 715 | [qr/^MPEG system stream data\b/ =>['movie','mpg'] ], |
---|
| 716 | [qr/^MPEG\b/ =>['movie','mpg'] ], |
---|
| 717 | [qr/^Microsoft ASF\b/ =>['movie','wmv'] ], |
---|
| 718 | [qr/^RIFF\b.*\bAVI\b/ =>['movie','avi'] ], |
---|
| 719 | [qr/^RIFF\b.*\bWAVE audio\b/ =>['audio','wav'] ], |
---|
| 720 | |
---|
| 721 | [qr/^Macromedia Flash data\b/ => 'swf'], |
---|
| 722 | [qr/^HTML document text\b/ => 'html'], |
---|
| 723 | [qr/^XML document text\b/ => 'xml'], |
---|
| 724 | [qr/^exported SGML document text\b/ => 'sgml'], |
---|
| 725 | [qr/^PostScript document text\b/ => 'ps'], |
---|
| 726 | [qr/^PDF document\b/ => 'pdf'], |
---|
| 727 | [qr/^Rich Text Format data\b/ => 'rtf'], |
---|
| 728 | [qr/^Microsoft Office Document\b/i => 'doc'], # OLE2: doc, ppt, xls, ... |
---|
| 729 | [qr/^LaTeX\b.*\bdocument text\b/ => 'lat'], |
---|
| 730 | [qr/^TeX DVI file\b/ => 'dvi'], |
---|
| 731 | [qr/\bdocument text\b/ => 'txt'], |
---|
| 732 | [qr/^compiled Java class data\b/ => 'java'], |
---|
| 733 | [qr/^MS Windows 95 Internet shortcut text\b/ => 'url'], |
---|
| 734 | |
---|
| 735 | [qr/^frozen\b/ => 'F'], |
---|
| 736 | [qr/^gzip compressed\b/ => 'gz'], |
---|
| 737 | [qr/^bzip compressed\b/ => 'bz'], |
---|
| 738 | [qr/^bzip2 compressed\b/ => 'bz2'], |
---|
| 739 | [qr/^lzop compressed\b/ => 'lzo'], |
---|
| 740 | [qr/^compress'd/ => 'Z'], |
---|
| 741 | [qr/^Zip archive\b/i => 'zip'], |
---|
| 742 | [qr/^RAR archive\b/i => 'rar'], |
---|
| 743 | [qr/^LHa.*\barchive\b/i => 'lha'], # or .lzh |
---|
| 744 | [qr/^ARC archive\b/i => 'arc'], |
---|
| 745 | [qr/^ARJ archive\b/i => 'arj'], |
---|
| 746 | [qr/^Zoo archive\b/i => 'zoo'], |
---|
| 747 | [qr/^(\S+\s+)?tar archive\b/i => 'tar'], |
---|
| 748 | [qr/^(\S+\s+)?cpio archive\b/i => 'cpio'], |
---|
| 749 | [qr/^Debian binary package\b/i => 'deb'], # standard Unix archive (ar) |
---|
| 750 | [qr/^current ar archive\b/i => 'a'], # standard Unix archive (ar) |
---|
| 751 | [qr/^RPM\b/ => 'rpm'], |
---|
| 752 | [qr/^(Transport Neutral Encapsulation Format|TNEF)\b/i => 'tnef'], |
---|
| 753 | [qr/^Microsoft cabinet file\b/ => 'cab'], |
---|
| 754 | |
---|
| 755 | [qr/^(uuencoded|xxencoded)\b/i => 'uue'], |
---|
| 756 | [qr/^binhex\b/i => 'hqx'], |
---|
| 757 | [qr/^(ASCII|text)\b/i => 'asc'], |
---|
| 758 | [qr/^Emacs.*byte-compiled Lisp data/i => 'asc'], # BinHex with an empty line |
---|
| 759 | [qr/\bscript text executable\b/ => 'txt'], |
---|
| 760 | |
---|
| 761 | [qr/^MS-DOS\b.*\bexecutable\b/ => ['exe','exe-ms'] ], |
---|
| 762 | [qr/^MS Windows\b.*\bexecutable\b/ => ['exe','exe-ms'] ], |
---|
| 763 | [qr/^PA-RISC.*\bexecutable\b/ => ['exe','exe-unix'] ], |
---|
| 764 | [qr/^ELF .*\bexecutable\b/ => ['exe','exe-unix'] ], |
---|
| 765 | [qr/^COFF format .*\bexecutable\b/ => ['exe','exe-unix'] ], |
---|
| 766 | [qr/^executable \(RISC System\b/ => ['exe','exe-unix'] ], |
---|
| 767 | [qr/^VMS\b.*\bexecutable\b/ => ['exe','exe-vms'] ], |
---|
| 768 | |
---|
| 769 | [qr/\bexecutable\b/i => 'exe'], |
---|
| 770 | [qr/^MS Windows\b.*\bDLL\b/ => 'dll'], |
---|
| 771 | [qr/\bshared object, \b/i => 'so'], |
---|
| 772 | [qr/\brelocatable, \b/i => 'o'], |
---|
| 773 | [qr/\btext\b/i => 'asc'], |
---|
| 774 | [qr/.*/ => 'dat'], # catchall |
---|
| 775 | |
---|
| 776 | ); |
---|
| 777 | |
---|
| 778 | # MS Windows PE 32-bit Intel 80386 GUI executable not relocatable |
---|
| 779 | # MS-DOS executable (EXE), OS/2 or MS Windows |
---|
| 780 | # PA-RISC1.1 executable dynamically linked |
---|
| 781 | # PA-RISC1.1 shared executable dynamically linked |
---|
| 782 | # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (FreeBSD), for FreeBSD 5.0.1, dynamically linked (uses shared libs), stripped |
---|
| 783 | # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), stripped |
---|
| 784 | # ELF 64-bit MSB executable, SPARC V9, version 1 (FreeBSD), for FreeBSD 5.0, dynamically linked (uses shared libs), stripped |
---|
| 785 | # ELF 64-bit MSB shared object, SPARC V9, version 1 (FreeBSD), stripped |
---|
| 786 | # ELF 32-bit LSB executable, Intel 80386, version 1, dynamically` |
---|
| 787 | # ELF 32-bit MSB executable, SPARC, version 1, dynamically linke` |
---|
| 788 | # COFF format alpha executable paged stripped - version 3.11-10 |
---|
| 789 | # COFF format alpha executable paged dynamically linked stripped` |
---|
| 790 | # COFF format alpha demand paged executable or object module stripped - version 3.11-10 |
---|
| 791 | # COFF format alpha paged dynamically linked not stripped shared` |
---|
| 792 | # executable (RISC System/6000 V3.1) or obj module |
---|
| 793 | # VMS VAX executable |
---|
| 794 | |
---|
| 795 | # Define aliase names in this module to make it simpler to call |
---|
| 796 | # these routines from amavisd.conf |
---|
| 797 | *read_text = \&Amavis::Util::read_text; |
---|
| 798 | *read_l10n_templates = \&Amavis::Util::read_l10n_templates; |
---|
| 799 | *read_hash = \&Amavis::Util::read_hash; |
---|
| 800 | *ask_daemon = \&Amavis::AV::ask_daemon; |
---|
| 801 | *sophos_savi = \&Amavis::AV::ask_sophos_savi; |
---|
| 802 | *ask_clamav = \&Amavis::AV::ask_clamav; |
---|
| 803 | sub new_RE { Amavis::Lookup::RE->new(@_) } |
---|
| 804 | |
---|
| 805 | sub build_default_maps() { |
---|
| 806 | @local_domains_maps = ( |
---|
| 807 | \%local_domains, \@local_domains_acl, \$local_domains_re); |
---|
| 808 | @mynetworks_maps = (\@mynetworks); |
---|
| 809 | @bypass_virus_checks_maps = ( |
---|
| 810 | \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re); |
---|
| 811 | @bypass_spam_checks_maps = ( |
---|
| 812 | \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re); |
---|
| 813 | @bypass_banned_checks_maps = ( |
---|
| 814 | \%bypass_banned_checks, \@bypass_banned_checks_acl, \$bypass_banned_checks_re); |
---|
| 815 | @bypass_header_checks_maps = ( |
---|
| 816 | \%bypass_header_checks, \@bypass_header_checks_acl, \$bypass_header_checks_re); |
---|
| 817 | @virus_lovers_maps = ( |
---|
| 818 | \%virus_lovers, \@virus_lovers_acl, \$virus_lovers_re); |
---|
| 819 | @spam_lovers_maps = ( |
---|
| 820 | \%spam_lovers, \@spam_lovers_acl, \$spam_lovers_re); |
---|
| 821 | @banned_files_lovers_maps = ( |
---|
| 822 | \%banned_files_lovers, \@banned_files_lovers_acl, \$banned_files_lovers_re); |
---|
| 823 | @bad_header_lovers_maps = ( |
---|
| 824 | \%bad_header_lovers, \@bad_header_lovers_acl, \$bad_header_lovers_re); |
---|
| 825 | @warnvirusrecip_maps = (\$warnvirusrecip); |
---|
| 826 | @warnbannedrecip_maps = (\$warnbannedrecip); |
---|
| 827 | @warnbadhrecip_maps = (\$warnbadhrecip); |
---|
| 828 | @newvirus_admin_maps = (\$newvirus_admin); |
---|
| 829 | @virus_admin_maps = (\%virus_admin, \$virus_admin); |
---|
| 830 | @banned_admin_maps = (\$banned_admin); |
---|
| 831 | @bad_header_admin_maps= (\$bad_header_admin); |
---|
| 832 | @spam_admin_maps = (\%spam_admin, \$spam_admin); |
---|
| 833 | @virus_quarantine_to_maps = (\$virus_quarantine_to); |
---|
| 834 | @banned_quarantine_to_maps = (\$banned_quarantine_to); |
---|
| 835 | @bad_header_quarantine_to_maps = (\$bad_header_quarantine_to); |
---|
| 836 | @spam_quarantine_to_maps = (\$spam_quarantine_to); |
---|
| 837 | @spam_quarantine_bysender_to_maps = (\$spam_quarantine_bysender_to); |
---|
| 838 | @keep_decoded_original_maps = (\$keep_decoded_original_re); |
---|
| 839 | @map_full_type_to_short_type_maps = (\$map_full_type_to_short_type_re); |
---|
| 840 | @banned_filename_maps = (\$banned_filename_re); |
---|
| 841 | @viruses_that_fake_sender_maps = (\$viruses_that_fake_sender_re, 1); |
---|
| 842 | @spam_tag_level_maps = (\$sa_tag_level_deflt); |
---|
| 843 | @spam_tag2_level_maps = (\$sa_tag2_level_deflt); |
---|
| 844 | @spam_kill_level_maps = (\$sa_kill_level_deflt); |
---|
| 845 | @spam_dsn_cutoff_level_maps = (\$sa_dsn_cutoff_level); |
---|
| 846 | @spam_modifies_subj_maps = (\$sa_spam_modifies_subj); |
---|
| 847 | @spam_subject_tag_maps = (\$sa_spam_subject_tag1); # note: inconsistent |
---|
| 848 | @spam_subject_tag2_maps = (\$sa_spam_subject_tag); # note: inconsistent |
---|
| 849 | @whitelist_sender_maps = ( |
---|
| 850 | \%whitelist_sender, \@whitelist_sender_acl, \$whitelist_sender_re); |
---|
| 851 | @blacklist_sender_maps = ( |
---|
| 852 | \%blacklist_sender, \@blacklist_sender_acl, \$blacklist_sender_re); |
---|
| 853 | @score_sender_maps = (); # new variable, no backwards compatibility needed |
---|
| 854 | @message_size_limit_maps = (); # new variable |
---|
| 855 | @addr_extension_virus_maps = (\$addr_extension_virus); |
---|
| 856 | @addr_extension_spam_maps = (\$addr_extension_spam); |
---|
| 857 | @addr_extension_banned_maps = (\$addr_extension_banned); |
---|
| 858 | @addr_extension_bad_header_maps = (\$addr_extension_bad_header); |
---|
| 859 | @debug_sender_maps = (\@debug_sender_acl); |
---|
| 860 | } |
---|
| 861 | |
---|
| 862 | # prepend a lookup table label object for logging purposes |
---|
| 863 | sub label_default_maps() { |
---|
| 864 | for my $varname (qw( |
---|
| 865 | @local_domains_maps @mynetworks_maps |
---|
| 866 | @bypass_virus_checks_maps @bypass_spam_checks_maps |
---|
| 867 | @bypass_banned_checks_maps @bypass_header_checks_maps |
---|
| 868 | @virus_lovers_maps @spam_lovers_maps |
---|
| 869 | @banned_files_lovers_maps @bad_header_lovers_maps |
---|
| 870 | @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps |
---|
| 871 | @newvirus_admin_maps @virus_admin_maps |
---|
| 872 | @banned_admin_maps @bad_header_admin_maps @spam_admin_maps |
---|
| 873 | @virus_quarantine_to_maps |
---|
| 874 | @banned_quarantine_to_maps @bad_header_quarantine_to_maps |
---|
| 875 | @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps |
---|
| 876 | @keep_decoded_original_maps @map_full_type_to_short_type_maps |
---|
| 877 | @banned_filename_maps @viruses_that_fake_sender_maps |
---|
| 878 | @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps |
---|
| 879 | @spam_dsn_cutoff_level_maps @spam_modifies_subj_maps |
---|
| 880 | @spam_subject_tag_maps @spam_subject_tag2_maps |
---|
| 881 | @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps |
---|
| 882 | @message_size_limit_maps |
---|
| 883 | @addr_extension_virus_maps @addr_extension_spam_maps |
---|
| 884 | @addr_extension_banned_maps @addr_extension_bad_header_maps |
---|
| 885 | @debug_sender_maps )) |
---|
| 886 | { |
---|
| 887 | my($g) = $varname; $g =~ s{\@}{Amavis::Conf::}; # qualified variable name |
---|
| 888 | my($label) = $varname; $label=~s/^\@//; $label=~s/_maps$//; |
---|
| 889 | { no strict 'refs'; |
---|
| 890 | unshift(@$g, # NOTE: a symbolic reference |
---|
| 891 | Amavis::Lookup::Label->new($label)) if @$g; # no label if empty |
---|
| 892 | } |
---|
| 893 | } |
---|
| 894 | } |
---|
| 895 | |
---|
| 896 | # read and evaluate configuration files (one or more) |
---|
| 897 | sub read_config(@) { |
---|
| 898 | my(@config_files) = @_; |
---|
| 899 | for my $config_file (@config_files) { |
---|
| 900 | my($msg); |
---|
| 901 | my($errn) = stat($config_file) ? 0 : 0+$!; |
---|
| 902 | if ($errn == ENOENT) { $msg = "does not exist" } |
---|
| 903 | elsif ($errn) { $msg = "is inaccessible: $!" } |
---|
| 904 | elsif (-d _) { $msg = "is a directory" } |
---|
| 905 | elsif (!-f _) { $msg = "is not a regular file" } |
---|
| 906 | elsif ($> && -o _) { $msg = "is owned by EUID $>, should be owned by root"} |
---|
| 907 | elsif ($> && -w _) { $msg = "is writable by EUID $>, EGID $)" } |
---|
| 908 | if (defined $msg) { die "Config file \"$config_file\" $msg," } |
---|
| 909 | $! = undef; my($rv) = do $config_file; |
---|
| 910 | if (!defined($rv)) { |
---|
| 911 | if ($@ ne '') { die "Error in config file \"$config_file\": $@" } |
---|
| 912 | else { die "Error reading config file \"$config_file\": $!" } |
---|
| 913 | } |
---|
| 914 | } |
---|
| 915 | $daemon_chroot_dir = '' if !defined $daemon_chroot_dir; # avoids warnings |
---|
| 916 | # some sensible defaults for essential settings |
---|
| 917 | $TEMPBASE = $MYHOME if !defined $TEMPBASE; |
---|
| 918 | $helpers_home = $MYHOME if !defined $helpers_home; |
---|
| 919 | $db_home = "$MYHOME/db" if !defined $db_home; |
---|
| 920 | $lock_file = "$MYHOME/amavisd.lock" if !defined $lock_file; |
---|
| 921 | $pid_file = "$MYHOME/amavisd.pid" if !defined $pid_file; |
---|
| 922 | |
---|
| 923 | $X_HEADER_TAG = 'X-Virus-Scanned' if !defined $X_HEADER_TAG; |
---|
| 924 | $X_HEADER_LINE= "$myproduct_name at $mydomain" if !defined $X_HEADER_LINE; |
---|
| 925 | #my($pname)= "$pname" if !defined $pname; |
---|
| 926 | # $pname = "Content filter" if !defined $pname; |
---|
| 927 | #my($pname) = "\"Content-filter at $myhostname\"" if !defined $CONTENT; |
---|
| 928 | $hdrfrom_notify_sender = "<postmaster\@$myhostname>" |
---|
| 929 | if !defined $hdrfrom_notify_sender; |
---|
| 930 | $hdrfrom_notify_recip = $mailfrom_notify_recip ne '' |
---|
| 931 | ? "<$mailfrom_notify_recip>" |
---|
| 932 | : $hdrfrom_notify_sender if !defined $hdrfrom_notify_recip; |
---|
| 933 | $hdrfrom_notify_admin = $mailfrom_notify_admin ne '' |
---|
| 934 | ? "<$mailfrom_notify_admin>" |
---|
| 935 | : $hdrfrom_notify_sender if !defined $hdrfrom_notify_admin; |
---|
| 936 | $hdrfrom_notify_spamadmin = $mailfrom_notify_spamadmin ne '' |
---|
| 937 | ? "<$mailfrom_notify_spamadmin>" |
---|
| 938 | : $hdrfrom_notify_sender if !defined $hdrfrom_notify_spamadmin; |
---|
| 939 | |
---|
| 940 | # compatibility with deprecated $warn*sender and old *_destiny values |
---|
| 941 | # map old values <0, =0, >0 into D_REJECT/D_BOUNCE, D_DISCARD, D_PASS |
---|
| 942 | for ($final_virus_destiny, $final_banned_destiny, $final_spam_destiny) { |
---|
| 943 | if ($_ > 0) { $_ = D_PASS } |
---|
| 944 | elsif ($_ < 0 && $_ != D_BOUNCE && $_ != D_REJECT) { # compatibility |
---|
| 945 | # favour Reject with sendmail milter, Bounce with others |
---|
| 946 | $_ = c('forward_method') eq '' ? D_REJECT : D_BOUNCE; |
---|
| 947 | } |
---|
| 948 | } |
---|
| 949 | if ($final_virus_destiny == D_DISCARD && c('warnvirussender') ) |
---|
| 950 | { $final_virus_destiny = D_BOUNCE } |
---|
| 951 | if ($final_spam_destiny == D_DISCARD && c('warnspamsender') ) |
---|
| 952 | { $final_spam_destiny = D_BOUNCE } |
---|
| 953 | if ($final_banned_destiny == D_DISCARD && c('warnbannedsender') ) |
---|
| 954 | { $final_banned_destiny = D_BOUNCE } |
---|
| 955 | if ($final_bad_header_destiny == D_DISCARD && c('warnbadhsender') ) |
---|
| 956 | { $final_bad_header_destiny = D_BOUNCE } |
---|
| 957 | } |
---|
| 958 | |
---|
| 959 | 1; |
---|
| 960 | |
---|
| 961 | # |
---|
| 962 | package Amavis::Lock; |
---|
| 963 | use strict; |
---|
| 964 | use re 'taint'; |
---|
| 965 | |
---|
| 966 | BEGIN { |
---|
| 967 | use Exporter (); |
---|
| 968 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 969 | $VERSION = '2.034'; |
---|
| 970 | @ISA = qw(Exporter); |
---|
| 971 | @EXPORT = qw(&lock &unlock); |
---|
| 972 | } |
---|
| 973 | use Fcntl qw(LOCK_SH LOCK_EX LOCK_UN); |
---|
| 974 | |
---|
| 975 | use subs @EXPORT; |
---|
| 976 | |
---|
| 977 | sub lock($) { |
---|
| 978 | my($file_handle) = @_; |
---|
| 979 | flock($file_handle, LOCK_EX) or die "Can't lock $file_handle: $!"; |
---|
| 980 | # NOTE: a lock is on a file, not on a file handle |
---|
| 981 | } |
---|
| 982 | |
---|
| 983 | sub unlock($) { |
---|
| 984 | my($file_handle) = @_; |
---|
| 985 | flock($file_handle, LOCK_UN) or die "Can't unlock $file_handle: $!"; |
---|
| 986 | } |
---|
| 987 | |
---|
| 988 | 1; |
---|
| 989 | |
---|
| 990 | # |
---|
| 991 | package Amavis::Log; |
---|
| 992 | use strict; |
---|
| 993 | use re 'taint'; |
---|
| 994 | |
---|
| 995 | BEGIN { |
---|
| 996 | use Exporter (); |
---|
| 997 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 998 | $VERSION = '2.034'; |
---|
| 999 | @ISA = qw(Exporter); |
---|
| 1000 | %EXPORT_TAGS = (); |
---|
| 1001 | @EXPORT = (); |
---|
| 1002 | @EXPORT_OK = qw(&init &write_log); |
---|
| 1003 | } |
---|
| 1004 | use subs @EXPORT_OK; |
---|
| 1005 | |
---|
| 1006 | use POSIX qw(locale_h strftime); |
---|
| 1007 | use Unix::Syslog qw(:macros :subs); |
---|
| 1008 | use IO::File (); |
---|
| 1009 | use File::Basename; |
---|
| 1010 | |
---|
| 1011 | BEGIN { |
---|
| 1012 | import Amavis::Conf qw(:platform $myversion $myhostname $daemon_user); |
---|
| 1013 | import Amavis::Lock; |
---|
| 1014 | } |
---|
| 1015 | |
---|
| 1016 | use vars qw($loghandle); # log file handle |
---|
| 1017 | use vars qw($myname); |
---|
| 1018 | use vars qw($syslog_facility $syslog_priority %syslog_priority); |
---|
| 1019 | use vars qw($log_to_stderr $do_syslog $logfile); |
---|
| 1020 | |
---|
| 1021 | sub init($$$$$) { |
---|
| 1022 | my($ident, $syslog_level); |
---|
| 1023 | ($ident, $log_to_stderr, $do_syslog, $syslog_level, $logfile) = @_; |
---|
| 1024 | |
---|
| 1025 | # Avoid taint bug in some versions of Perl (likely in 5.004, 5.005). |
---|
| 1026 | # The 5.6.1 is fine. To test, run this one-liner: |
---|
| 1027 | # perl -Te '"$0 $$"; $r=$$; print eval{kill(0,$$);1}?"OK\n":"BUG\n"' |
---|
| 1028 | $myname = $0; |
---|
| 1029 | # $myname = $1 if basename($0) =~ /^(.*)\z/; |
---|
| 1030 | |
---|
| 1031 | if ($syslog_level =~ /^\s*([a-z0-9]+)\.([a-z0-9]+)\s*\z/i) { |
---|
| 1032 | $syslog_facility = eval("LOG_\U$1"); |
---|
| 1033 | $syslog_priority = eval("LOG_\U$2"); |
---|
| 1034 | } |
---|
| 1035 | $syslog_facility = LOG_DAEMON if $syslog_facility !~ /^\d+\z/; |
---|
| 1036 | $syslog_priority = LOG_WARNING if $syslog_priority !~ /^\d+\z/; |
---|
| 1037 | if ($do_syslog) { |
---|
| 1038 | openlog($ident, LOG_PID, $syslog_facility); |
---|
| 1039 | } elsif ($logfile eq '') { |
---|
| 1040 | die 'No $LOGFILE is specified (and not logging via syslog)'; |
---|
| 1041 | } else { |
---|
| 1042 | $loghandle = IO::File->new($logfile,'>>') |
---|
| 1043 | or die "Failed to open log file $logfile: $!"; |
---|
| 1044 | $loghandle->autoflush(1); |
---|
| 1045 | my($uid) = $daemon_user=~/^(\d+)$/ ? $1 : (getpwnam($daemon_user))[2]; |
---|
| 1046 | if ($> == 0 && $uid) { |
---|
| 1047 | chown($uid,-1,$logfile) |
---|
| 1048 | or die "Can't chown logfile $logfile to $uid: $!"; |
---|
| 1049 | } |
---|
| 1050 | } |
---|
| 1051 | my($msg) = "starting. $myname at $myhostname $myversion"; |
---|
| 1052 | $msg .= ", eol=\"$eol\"" if $eol ne "\n"; |
---|
| 1053 | $msg .= ", Unicode aware" if $unicode_aware; |
---|
| 1054 | $msg .= ", LC_ALL=$ENV{LC_ALL}" if $ENV{LC_ALL} ne ''; |
---|
| 1055 | $msg .= ", LC_TYPE=$ENV{LC_TYPE}" if $ENV{LC_TYPE} ne ''; |
---|
| 1056 | $msg .= ", LC_CTYPE=$ENV{LC_CTYPE}" if $ENV{LC_CTYPE} ne ''; |
---|
| 1057 | $msg .= ", LANG=$ENV{LANG}" if $ENV{LANG} ne ''; |
---|
| 1058 | write_log(0, $msg, undef); |
---|
| 1059 | } |
---|
| 1060 | |
---|
| 1061 | # Log either to syslog or a file |
---|
| 1062 | sub write_log($$$) { |
---|
| 1063 | my($level,$errmsg,$am_id) = @_; |
---|
| 1064 | |
---|
| 1065 | my($old_locale) = setlocale(LC_TIME,"C"); # English dates required! |
---|
| 1066 | my($really_log_to_stderr) = $log_to_stderr || (!$do_syslog && !$loghandle); |
---|
| 1067 | my($prefix) = ''; |
---|
| 1068 | if ($really_log_to_stderr || !$do_syslog) { # create syslog-like prefix |
---|
| 1069 | $prefix = sprintf("%s %s %s[%s]: ", |
---|
| 1070 | strftime("%b %e %H:%M:%S", localtime), $myhostname, $myname, $$); |
---|
| 1071 | } |
---|
| 1072 | $am_id = !defined $am_id ? '' : "($am_id) "; |
---|
| 1073 | $errmsg = Amavis::Util::sanitize_str($errmsg); |
---|
| 1074 | # if (length($errmsg) > 2000) { # crop at some arbitrary limit (< LINE_MAX) |
---|
| 1075 | # $errmsg = substr($errmsg,0,2000) . "..."; |
---|
| 1076 | # } |
---|
| 1077 | if ($really_log_to_stderr) { |
---|
| 1078 | print STDERR $prefix, $am_id, $errmsg, $eol; |
---|
| 1079 | } elsif ($do_syslog) { |
---|
| 1080 | my($prio) = $syslog_priority; # never go below this priority level |
---|
| 1081 | # syslog priorities: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT, EMERG |
---|
| 1082 | if ($level <= -3) { $prio = LOG_CRIT if $prio > LOG_CRIT } |
---|
| 1083 | elsif ($level <= -2) { $prio = LOG_ERR if $prio > LOG_ERR } |
---|
| 1084 | elsif ($level <= -1) { $prio = LOG_WARNING if $prio > LOG_WARNING } |
---|
| 1085 | elsif ($level <= 0) { $prio = LOG_NOTICE if $prio > LOG_NOTICE } |
---|
| 1086 | elsif ($level <= 2) { $prio = LOG_INFO if $prio > LOG_INFO } |
---|
| 1087 | else { $prio = LOG_DEBUG if $prio > LOG_DEBUG } |
---|
| 1088 | my($pre) = ''; |
---|
| 1089 | my($logline_size) = 980; # less than (1023 - prefix) |
---|
| 1090 | while (length($am_id . $pre . $errmsg) > $logline_size) { |
---|
| 1091 | my($avail) = $logline_size - length($am_id . $pre . "..."); |
---|
| 1092 | syslog($prio, "%s", $am_id . $pre . substr($errmsg,0,$avail) . "..."); |
---|
| 1093 | $pre = "..."; |
---|
| 1094 | $errmsg = substr($errmsg, $avail); |
---|
| 1095 | } |
---|
| 1096 | syslog($prio, "%s", $am_id . $pre . $errmsg); |
---|
| 1097 | } else { |
---|
| 1098 | lock($loghandle); |
---|
| 1099 | seek($loghandle,0,2) or die "Can't position log file to its tail: $!"; |
---|
| 1100 | print $loghandle $prefix, $am_id, $errmsg, $eol; |
---|
| 1101 | unlock($loghandle); |
---|
| 1102 | } |
---|
| 1103 | setlocale(LC_TIME, $old_locale); |
---|
| 1104 | } |
---|
| 1105 | |
---|
| 1106 | 1; |
---|
| 1107 | |
---|
| 1108 | # |
---|
| 1109 | package Amavis::Timing; |
---|
| 1110 | use strict; |
---|
| 1111 | use re 'taint'; |
---|
| 1112 | |
---|
| 1113 | BEGIN { |
---|
| 1114 | use Exporter (); |
---|
| 1115 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 1116 | $VERSION = '2.034'; |
---|
| 1117 | @ISA = qw(Exporter); |
---|
| 1118 | %EXPORT_TAGS = (); |
---|
| 1119 | @EXPORT = (); |
---|
| 1120 | @EXPORT_OK = qw(&init §ion_time &report &get_time_so_far); |
---|
| 1121 | } |
---|
| 1122 | use subs @EXPORT_OK; |
---|
| 1123 | |
---|
| 1124 | use Time::HiRes (); |
---|
| 1125 | |
---|
| 1126 | use vars qw(@timing); |
---|
| 1127 | |
---|
| 1128 | # clear array @timing and enter start time |
---|
| 1129 | sub init() { |
---|
| 1130 | @timing = (); section_time('init'); |
---|
| 1131 | } |
---|
| 1132 | |
---|
| 1133 | # enter current time reading into array @timing |
---|
| 1134 | sub section_time($) { |
---|
| 1135 | push(@timing,shift,Time::HiRes::time); |
---|
| 1136 | } |
---|
| 1137 | |
---|
| 1138 | # returns a string - a report of elapsed time by section |
---|
| 1139 | sub report() { |
---|
| 1140 | section_time('rundown'); |
---|
| 1141 | my($notneeded, $t0) = (shift(@timing), shift(@timing)); |
---|
| 1142 | my($total) = $t0 <= 0 ? 0 : $timing[$#timing] - $t0; |
---|
| 1143 | if ($total < 0.0000001) { $total = 0.0000001 } |
---|
| 1144 | my(@sections); |
---|
| 1145 | while (@timing) { |
---|
| 1146 | my($section, $t) = (shift(@timing), shift(@timing)); |
---|
| 1147 | my($dt) = $t-$t0; |
---|
| 1148 | $dt = 0 if $dt < 0; # just in case (clock jumps) |
---|
| 1149 | my($dtp) = $dt > $total ? 100 : $dt*100.0/$total; |
---|
| 1150 | push(@sections, sprintf("%s: %.0f (%.0f%%)", $section, $dt*1000, $dtp)); |
---|
| 1151 | $t0 = $t; |
---|
| 1152 | } |
---|
| 1153 | sprintf("TIMING [total %.0f ms] - %s", $total * 1000, join(", ",@sections)); |
---|
| 1154 | } |
---|
| 1155 | |
---|
| 1156 | # returns value in seconds of elapsed time for processing of this mail so far |
---|
| 1157 | sub get_time_so_far() { |
---|
| 1158 | my($notneeded, $t0) = @timing; |
---|
| 1159 | my($total) = $t0 <= 0 ? 0 : Time::HiRes::time - $t0; |
---|
| 1160 | $total < 0 ? 0 : $total; |
---|
| 1161 | } |
---|
| 1162 | |
---|
| 1163 | use vars qw($t_was_busy $t_busy_cum $t_idle_cum $t0); |
---|
| 1164 | |
---|
| 1165 | sub idle_proc(@) { |
---|
| 1166 | my($t1) = Time::HiRes::time; |
---|
| 1167 | if (defined $t0) { |
---|
| 1168 | ($t_was_busy ? $t_busy_cum : $t_idle_cum) += $t1 - $t0; |
---|
| 1169 | Amavis::Util::ll(5) && Amavis::Util::do_log(5, |
---|
| 1170 | sprintf("idle_proc, @_: was %s, %.1f ms, total idle %.3f s, busy %.3f s", |
---|
| 1171 | $t_was_busy ? "busy" : "idle", 1000 * ($t1 - $t0), |
---|
| 1172 | $t_idle_cum, $t_busy_cum)); |
---|
| 1173 | } |
---|
| 1174 | $t0 = $t1; |
---|
| 1175 | } |
---|
| 1176 | |
---|
| 1177 | sub go_idle(@) { |
---|
| 1178 | if ($t_was_busy) { idle_proc(@_); $t_was_busy = 0 } |
---|
| 1179 | } |
---|
| 1180 | |
---|
| 1181 | sub go_busy(@) { |
---|
| 1182 | if (!$t_was_busy) { idle_proc(@_); $t_was_busy = 1 } |
---|
| 1183 | } |
---|
| 1184 | |
---|
| 1185 | sub report_load() { |
---|
| 1186 | return if $t_busy_cum + $t_idle_cum <= 0; |
---|
| 1187 | Amavis::Util::do_log(3, sprintf( |
---|
| 1188 | "load: %.0f %%, total idle %.3f s, busy %.3f s", |
---|
| 1189 | 100*$t_busy_cum / ($t_busy_cum + $t_idle_cum), $t_idle_cum, $t_busy_cum)); |
---|
| 1190 | } |
---|
| 1191 | |
---|
| 1192 | 1; |
---|
| 1193 | |
---|
| 1194 | # |
---|
| 1195 | package Amavis::Util; |
---|
| 1196 | use strict; |
---|
| 1197 | use re 'taint'; |
---|
| 1198 | |
---|
| 1199 | BEGIN { |
---|
| 1200 | use Exporter (); |
---|
| 1201 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 1202 | $VERSION = '2.034'; |
---|
| 1203 | @ISA = qw(Exporter); |
---|
| 1204 | %EXPORT_TAGS = (); |
---|
| 1205 | @EXPORT = (); |
---|
| 1206 | @EXPORT_OK = qw(&untaint &min &max &safe_encode &q_encode |
---|
| 1207 | &snmp_count &snmp_counters_init &snmp_counters_get |
---|
| 1208 | &am_id &new_am_id &ll &do_log &debug_oneshot |
---|
| 1209 | &retcode &exit_status_str &prolong_timer &sanitize_str |
---|
| 1210 | &strip_tempdir &rmdir_recursively |
---|
| 1211 | &read_text &read_l10n_templates &read_hash |
---|
| 1212 | &run_command &run_command_consumer); |
---|
| 1213 | } |
---|
| 1214 | use subs @EXPORT_OK; |
---|
| 1215 | use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED |
---|
| 1216 | WEXITSTATUS WTERMSIG WSTOPSIG); |
---|
| 1217 | use Errno qw(ENOENT EACCES); |
---|
| 1218 | use Digest::MD5; |
---|
| 1219 | # use Encode; # Perl 5.8 UTF-8 support |
---|
| 1220 | # use Encode::CN; # example: explicitly load Chinese module |
---|
| 1221 | |
---|
| 1222 | BEGIN { |
---|
| 1223 | import Amavis::Conf qw(:platform $DEBUG c cr ca); |
---|
| 1224 | import Amavis::Log qw(write_log); |
---|
| 1225 | import Amavis::Timing qw(section_time); |
---|
| 1226 | } |
---|
| 1227 | |
---|
| 1228 | # Return untainted copy of a string (argument can be a string or a string ref) |
---|
| 1229 | sub untaint($) { |
---|
| 1230 | no re 'taint'; |
---|
| 1231 | my($str); |
---|
| 1232 | local($1); # avoid Perl taint bug: tainted global $1 propagates taintedness |
---|
| 1233 | $str = $1 if (ref($_[0]) ? ${$_[0]} : $_[0]) =~ /^(.*)\z/s; |
---|
| 1234 | $str; |
---|
| 1235 | } |
---|
| 1236 | |
---|
| 1237 | # Returns the smallest number from the list, or undef |
---|
| 1238 | sub min(@) { |
---|
| 1239 | my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref |
---|
| 1240 | my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ < $m) } |
---|
| 1241 | $m; |
---|
| 1242 | } |
---|
| 1243 | |
---|
| 1244 | # Returns the largest number from the list, or undef |
---|
| 1245 | sub max(@) { |
---|
| 1246 | my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref |
---|
| 1247 | my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ > $m) } |
---|
| 1248 | $m; |
---|
| 1249 | } |
---|
| 1250 | |
---|
| 1251 | # A wrapper for Encode::encode, avoiding a bug in Perl 5.8.0 which causes |
---|
| 1252 | # Encode::encode to loop and fill memory when given a tainted string |
---|
| 1253 | sub safe_encode($$;$) { |
---|
| 1254 | if (!$unicode_aware) { $_[1] } # just return the second argument |
---|
| 1255 | else { |
---|
| 1256 | my($encoding,$str,$check) = @_; |
---|
| 1257 | $check = 0 if !defined($check); |
---|
| 1258 | # taintedness of the string, with UTF-8 flag unconditionally off |
---|
| 1259 | my($taint) = Encode::encode('ascii',substr($str,0,0)); |
---|
| 1260 | $taint . Encode::encode($encoding,untaint($str),$check); # preserve taint |
---|
| 1261 | } |
---|
| 1262 | } |
---|
| 1263 | |
---|
| 1264 | # Do the Q-encoding manually, the MIME::Words::encode_mimeword does not |
---|
| 1265 | # encode spaces and does not limit to 75 ch, which violates the RFC 2047 |
---|
| 1266 | sub q_encode($$$) { |
---|
| 1267 | my($octets,$encoding,$charset) = @_; |
---|
| 1268 | my($prefix) = '=?' . $charset . '?' . $encoding . '?'; |
---|
| 1269 | my($suffix) = '?='; local($1,$2,$3); |
---|
| 1270 | # FWS | utext (= NO-WS-CTL|rest of US-ASCII) |
---|
| 1271 | $octets =~ /^ ( [\001-\011\013\014\016-\177]* [ \t] )? (.*?) |
---|
| 1272 | ( [ \t] [\001-\011\013\014\016-\177]* )? \z/sx; |
---|
| 1273 | my($head,$rest,$tail) = ($1,$2,$3); |
---|
| 1274 | # Q-encode $rest according to RFC 2047 |
---|
| 1275 | # more restricted than =?_ so that it may be used in 'phrase' |
---|
| 1276 | $rest =~ s{([^ 0-9a-zA-Z!*/+-])}{sprintf('=%02X',ord($1))}egs; |
---|
| 1277 | $rest =~ tr/ /_/; # turn spaces into _ (rfc2047 allows it) |
---|
| 1278 | my($s) = $head; my($len) = 75 - (length($prefix)+length($suffix)) - 2; |
---|
| 1279 | while ($rest ne '') { |
---|
| 1280 | $s .= ' ' if $s !~ /[ \t]\z/; # encoded words must be separated by FWS |
---|
| 1281 | $rest =~ /^ ( .{0,$len} [^=] (?: [^=] | \z ) ) (.*) \z/sx; |
---|
| 1282 | $s .= $prefix.$1.$suffix; $rest = $2; |
---|
| 1283 | } |
---|
| 1284 | $s.$tail; |
---|
| 1285 | } |
---|
| 1286 | |
---|
| 1287 | # Set or get Amavis internal message id. |
---|
| 1288 | # This message id performs a similar function to queue-id in MTA responses. |
---|
| 1289 | # It may only be used in generating text part of SMTP responses, |
---|
| 1290 | # or in generating log entries. |
---|
| 1291 | use vars qw($amavis_task_id); # internal message id (accessible via &am_id) |
---|
| 1292 | |
---|
| 1293 | sub am_id(;$) { |
---|
| 1294 | if (@_) { # set, if argument present |
---|
| 1295 | $amavis_task_id = shift; |
---|
| 1296 | $0 = "amavisd ($amavis_task_id)"; |
---|
| 1297 | } |
---|
| 1298 | $amavis_task_id; # return current value |
---|
| 1299 | } |
---|
| 1300 | |
---|
| 1301 | sub new_am_id($;$$) { |
---|
| 1302 | my($str, $cnt, $seq) = @_; |
---|
| 1303 | my($id); |
---|
| 1304 | # my($ctx) = Digest::MD5->new; # 128 bits (32 hex digits) |
---|
| 1305 | # $ctx->add($str.$$); |
---|
| 1306 | # $id = substr($ctx->hexdigest, 0, 6); |
---|
| 1307 | $id = defined $str ? $str : sprintf("%05d", $$); |
---|
| 1308 | $id .= sprintf("-%02d", $cnt) if defined $cnt; |
---|
| 1309 | $id .= "-$seq" if $seq > 1; |
---|
| 1310 | am_id($id); |
---|
| 1311 | } |
---|
| 1312 | |
---|
| 1313 | use vars qw(@counter_names); |
---|
| 1314 | # elements may be counter names (increment is 1), or pairs: [name,increment] |
---|
| 1315 | sub snmp_counters_init() { @counter_names = () } |
---|
| 1316 | sub snmp_count(@) { push(@counter_names, @_) } |
---|
| 1317 | sub snmp_counters_get() { \@counter_names } |
---|
| 1318 | |
---|
| 1319 | use vars qw($debug_oneshot); |
---|
| 1320 | sub debug_oneshot(;$$) { |
---|
| 1321 | if (@_) { |
---|
| 1322 | my($new_debug_oneshot) = shift; |
---|
| 1323 | if (($new_debug_oneshot ? 1 : 0) != ($debug_oneshot ? 1 : 0)) { |
---|
| 1324 | do_log(0, "DEBUG_ONESHOT: TURNED ".($new_debug_oneshot ? "ON" : "OFF")); |
---|
| 1325 | do_log(0, shift) if @_; # caller-provided extra log entry, usually |
---|
| 1326 | # the one that caused debug_oneshot call |
---|
| 1327 | } |
---|
| 1328 | $debug_oneshot = $new_debug_oneshot; |
---|
| 1329 | } |
---|
| 1330 | $debug_oneshot; |
---|
| 1331 | } |
---|
| 1332 | |
---|
| 1333 | # is a message log level below the current log level? |
---|
| 1334 | sub ll($) { |
---|
| 1335 | my($level) = @_; |
---|
| 1336 | $level = 0 if $level > 0 && ($DEBUG || $debug_oneshot); |
---|
| 1337 | $level <= c('log_level'); |
---|
| 1338 | } |
---|
| 1339 | |
---|
| 1340 | # write log entry |
---|
| 1341 | sub do_log($$) { |
---|
| 1342 | my($level, $errmsg) = @_; |
---|
| 1343 | if (ll($level)) { |
---|
| 1344 | $level = 0 if $level > 0 && ($DEBUG || $debug_oneshot); |
---|
| 1345 | write_log($level, $errmsg, am_id()); |
---|
| 1346 | } |
---|
| 1347 | } |
---|
| 1348 | |
---|
| 1349 | sub retcode($) { # (this subroutine is being phased out) |
---|
| 1350 | my $code = shift; |
---|
| 1351 | return WEXITSTATUS($code) if WIFEXITED($code); |
---|
| 1352 | return 128 + WTERMSIG($code) if WIFSIGNALED($code); |
---|
| 1353 | return 255; |
---|
| 1354 | } |
---|
| 1355 | |
---|
| 1356 | sub exit_status_str($;$) { |
---|
| 1357 | my($stat,$err) = @_; my($str); |
---|
| 1358 | if (WIFEXITED($stat)) { |
---|
| 1359 | $str = sprintf("exit %d", WEXITSTATUS($stat)); |
---|
| 1360 | } elsif (WIFSTOPPED($stat)) { |
---|
| 1361 | $str = sprintf("stopped, signal %d", WSTOPSIG($stat)); |
---|
| 1362 | } else { |
---|
| 1363 | $str = sprintf("DIED on signal %d (%04x)", WTERMSIG($stat),$stat); |
---|
| 1364 | } |
---|
| 1365 | $str .= ', '.$err if $err ne ''; |
---|
| 1366 | $str; |
---|
| 1367 | } |
---|
| 1368 | |
---|
| 1369 | sub prolong_timer($;$) { |
---|
| 1370 | my($which_section, $child_remaining_time) = @_; |
---|
| 1371 | if (!defined($child_remaining_time)) { |
---|
| 1372 | $child_remaining_time = alarm(0); # check how much time is left |
---|
| 1373 | } |
---|
| 1374 | do_log(4, "prolong_timer after $which_section: " |
---|
| 1375 | . "remaining time = $child_remaining_time s"); |
---|
| 1376 | $child_remaining_time = 60 if $child_remaining_time < 60; |
---|
| 1377 | alarm($child_remaining_time); # restart/prolong the timer |
---|
| 1378 | } |
---|
| 1379 | |
---|
| 1380 | # Mostly for debugging and reporting purposes: |
---|
| 1381 | # Convert nonprintable characters in the argument |
---|
| 1382 | # to \[rnftbe], or \octal code, and '\' to '\\', |
---|
| 1383 | # and Unicode characters to \x{xxxx}, returning the sanitized string. |
---|
| 1384 | sub sanitize_str { |
---|
| 1385 | my($str, $keep_eol) = @_; |
---|
| 1386 | my(%map) = ("\r" => '\\r', "\n" => '\\n', "\f" => '\\f', "\t" => '\\t', |
---|
| 1387 | "\b" => '\\b', "\e" => '\\e', "\\" => '\\\\'); |
---|
| 1388 | if ($keep_eol) { |
---|
| 1389 | $str =~ s/([^\012\040-\133\135-\176])/ # and \240-\376 ? |
---|
| 1390 | exists($map{$1}) ? $map{$1} : |
---|
| 1391 | sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg; |
---|
| 1392 | } else { |
---|
| 1393 | $str =~ s/([^\040-\133\135-\176])/ # and \240-\376 ? |
---|
| 1394 | exists($map{$1}) ? $map{$1} : |
---|
| 1395 | sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg; |
---|
| 1396 | } |
---|
| 1397 | $str; |
---|
| 1398 | } |
---|
| 1399 | |
---|
| 1400 | # Checks tempdir after being cleaned. |
---|
| 1401 | # It may only contain subdirectory 'parts' and file email.txt, nothing else. |
---|
| 1402 | # |
---|
| 1403 | sub check_tempdir($) { |
---|
| 1404 | my($dir) = shift; |
---|
| 1405 | local(*DIR); my($f); |
---|
| 1406 | opendir(DIR,$dir) or die "Can't open directory $dir: $!"; |
---|
| 1407 | while (defined($f = readdir(DIR))) { |
---|
| 1408 | if (!-d ("$dir/$f")) { |
---|
| 1409 | die "Unexpected file $dir/$f" if $f ne 'email.txt'; |
---|
| 1410 | } elsif ($f eq '.' || $f eq '..' || $f eq 'parts') { |
---|
| 1411 | } else { |
---|
| 1412 | die "Unexpected subdirectory $dir/$f"; |
---|
| 1413 | } |
---|
| 1414 | } |
---|
| 1415 | closedir(DIR) or die "Can't close directory $dir: $!"; |
---|
| 1416 | 1; |
---|
| 1417 | } |
---|
| 1418 | |
---|
| 1419 | # Remove all files and subdirectories from the temporary directory, leaving |
---|
| 1420 | # only the directory itself, file email.txt, and empty subdirectory ./parts . |
---|
| 1421 | # Leaving directories for reuse represents an important saving in time, |
---|
| 1422 | # as directory creation + deletion is quite an expensive operation, |
---|
| 1423 | # requiring atomic file system operation, including flushing buffers to disk. |
---|
| 1424 | # |
---|
| 1425 | sub strip_tempdir($) { |
---|
| 1426 | my($dir) = shift; |
---|
| 1427 | do_log(4, "strip_tempdir: $dir"); |
---|
| 1428 | my($errn) = lstat("$dir/parts") ? 0 : 0+$!; |
---|
| 1429 | if ($errn != ENOENT) { |
---|
| 1430 | if ( -l _) { die "strip_tempdir: $dir/parts is a symbolic link" } |
---|
| 1431 | elsif (!-d _) { die "strip_tempdir: $dir/parts is not a directory" } |
---|
| 1432 | rmdir_recursively("$dir/parts", 1); |
---|
| 1433 | } |
---|
| 1434 | # All done. Check for any remains in the top directory just in case |
---|
| 1435 | check_tempdir($dir); |
---|
| 1436 | 1; |
---|
| 1437 | } |
---|
| 1438 | |
---|
| 1439 | # |
---|
| 1440 | # Removes a directory, along with its contents |
---|
| 1441 | sub rmdir_recursively($;$); # prototype |
---|
| 1442 | sub rmdir_recursively($;$) { |
---|
| 1443 | my($dir, $exclude_itself) = @_; |
---|
| 1444 | local(*DIR); my($errn); my($cnt) = 0; my($dir_is_open) = 0; |
---|
| 1445 | do_log(4,"rmdir_recursively: $dir, excl=$exclude_itself"); |
---|
| 1446 | eval { |
---|
| 1447 | $errn = opendir(DIR,$dir) ? 0 : 0+$!; |
---|
| 1448 | if ($errn == ENOENT) { die "Directory $dir does not exist," } |
---|
| 1449 | elsif ($errn == EACCES) { # relax protection on directory, then try again |
---|
| 1450 | do_log(3,"rmdir_recursively: enabling read access to directory $dir"); |
---|
| 1451 | chmod(0750,$dir) or die "Can't change protection-1 on dir $dir: $!"; |
---|
| 1452 | $errn = opendir(DIR,$dir) ? 0 : 0+$!; # try again |
---|
| 1453 | } |
---|
| 1454 | if ($errn) { die "Can't open directory $dir: $!" } |
---|
| 1455 | $dir_is_open = 1; my($f); |
---|
| 1456 | while (defined($f = readdir(DIR))) { |
---|
| 1457 | my($fname) = "$dir/$f"; |
---|
| 1458 | $errn = lstat($fname) ? 0 : 0+$!; |
---|
| 1459 | if ($errn == ENOENT) { die "File \"$fname\" does not exist," } |
---|
| 1460 | elsif ($errn == EACCES) { # relax protection on the directory and retry |
---|
| 1461 | do_log(3,"rmdir_recursively: enabling access to files in dir $dir"); |
---|
| 1462 | chmod(0750,$dir) or die "Can't change protection-2 on dir $dir: $!"; |
---|
| 1463 | $errn = lstat($fname) ? 0 : 0+$!; # try again |
---|
| 1464 | } |
---|
| 1465 | if ($errn) { die "File \"$fname\" inaccessible: $!" } |
---|
| 1466 | next if ($f eq '.' || $f eq '..') && -d _; |
---|
| 1467 | if (-d _) { rmdir_recursively(untaint($fname), 0) } |
---|
| 1468 | else { |
---|
| 1469 | $cnt++; |
---|
| 1470 | if (unlink(untaint($fname))) { # ok |
---|
| 1471 | } else { # relax protection on the directory, then try again |
---|
| 1472 | do_log(3,"rmdir_recursively: enabling write access to dir $dir"); |
---|
| 1473 | my($what) = -l _ ? 'symlink' :-d _ ? 'directory' :'non-regular file'; |
---|
| 1474 | chmod(0750,$dir) or die "Can't change protection-3 on dir $dir: $!"; |
---|
| 1475 | unlink(untaint($fname)) or die "Can't remove $what $fname: $!"; |
---|
| 1476 | } |
---|
| 1477 | } |
---|
| 1478 | } |
---|
| 1479 | }; |
---|
| 1480 | if ($@ eq '') { |
---|
| 1481 | closedir(DIR) or die "Can't close directory $dir: $!" if $dir_is_open; |
---|
| 1482 | } else { |
---|
| 1483 | closedir(DIR) if $dir_is_open; # ignoring status |
---|
| 1484 | die "rmdir_recursively: $@\n"; |
---|
| 1485 | } |
---|
| 1486 | section_time("unlink-$cnt-files"); |
---|
| 1487 | if (!$exclude_itself) { |
---|
| 1488 | rmdir($dir) or die "rmdir_recursively: Can't remove directory $dir: $!"; |
---|
| 1489 | section_time('rmdir'); |
---|
| 1490 | } |
---|
| 1491 | 1; |
---|
| 1492 | } |
---|
| 1493 | |
---|
| 1494 | # read a multiline string from file - may be called from amavisd.conf |
---|
| 1495 | sub read_text($;$) { |
---|
| 1496 | my($filename, $encoding) = @_; |
---|
| 1497 | my($inp) = IO::File->new; |
---|
| 1498 | $inp->open($filename,'<') or die "Can't open file $filename for reading: $!"; |
---|
| 1499 | if ($unicode_aware && $encoding ne '') { |
---|
| 1500 | binmode($inp, ":encoding($encoding)") |
---|
| 1501 | or die "Can't set :encoding($encoding) on file $filename: $!"; |
---|
| 1502 | } |
---|
| 1503 | my($str) = ''; # must not be undef, work around a Perl UTF8 bug |
---|
| 1504 | while (<$inp>) { $str .= $_ } |
---|
| 1505 | $inp->close or die "Can't close $filename: $!"; |
---|
| 1506 | $str; |
---|
| 1507 | } |
---|
| 1508 | |
---|
| 1509 | # attempt to read all user-visible replies from a l10n dir |
---|
| 1510 | # This function auto-fills $notify_sender_templ, $notify_virus_sender_templ, |
---|
| 1511 | # $notify_virus_admin_templ, $notify_virus_recips_templ, |
---|
| 1512 | # $notify_spam_sender_templ and $notify_spam_admin_templ from files named |
---|
| 1513 | # template-dsn.txt, template-virus-sender.txt, template-virus-admin.txt, |
---|
| 1514 | # template-virus-recipient.txt, template-spam-sender.txt, |
---|
| 1515 | # template-spam-admin.txt. If this is available, it uses the charset |
---|
| 1516 | # file to do automatic charset conversion. Used by the Debian distribution. |
---|
| 1517 | sub read_l10n_templates($;$) { |
---|
| 1518 | my($dir) = @_; |
---|
| 1519 | if (@_ > 1) # compatibility with Debian |
---|
| 1520 | { my($l10nlang, $l10nbase) = @_; $dir = "$l10nbase/$l10nlang" } |
---|
| 1521 | my($file_chset) = Amavis::Util::read_text("$dir/charset"); |
---|
| 1522 | if ($file_chset =~ m{^(?:#[^\n]*\n)*([^./\n\s]+)(\s*[#\n].*)?$}s) { |
---|
| 1523 | $file_chset = untaint($1); |
---|
| 1524 | } else { |
---|
| 1525 | die "Invalid charset $file_chset\n"; |
---|
| 1526 | } |
---|
| 1527 | $Amavis::Conf::notify_sender_templ = |
---|
| 1528 | Amavis::Util::read_text("$dir/template-dsn.txt", $file_chset); |
---|
| 1529 | $Amavis::Conf::notify_virus_sender_templ = |
---|
| 1530 | Amavis::Util::read_text("$dir/template-virus-sender.txt", $file_chset); |
---|
| 1531 | $Amavis::Conf::notify_virus_admin_templ = |
---|
| 1532 | Amavis::Util::read_text("$dir/template-virus-admin.txt", $file_chset); |
---|
| 1533 | $Amavis::Conf::notify_virus_recips_templ = |
---|
| 1534 | Amavis::Util::read_text("$dir/template-virus-recipient.txt", $file_chset); |
---|
| 1535 | $Amavis::Conf::notify_spam_sender_templ = |
---|
| 1536 | Amavis::Util::read_text("$dir/template-spam-sender.txt", $file_chset); |
---|
| 1537 | $Amavis::Conf::notify_spam_admin_templ = |
---|
| 1538 | Amavis::Util::read_text("$dir/template-spam-admin.txt", $file_chset); |
---|
| 1539 | } |
---|
| 1540 | |
---|
| 1541 | #use CDB_File; |
---|
| 1542 | #sub tie_hash($$) { |
---|
| 1543 | # my($hashref, $filename) = @_; |
---|
| 1544 | # CDB_File::create(%$hashref, $filename, "$filename.tmp$$") |
---|
| 1545 | # or die "Can't create cdb $filename: $!"; |
---|
| 1546 | # my($cdb) = tie(%$hashref,'CDB_File',$filename) |
---|
| 1547 | # or die "Tie to $filename failed: $!"; |
---|
| 1548 | # $hashref; |
---|
| 1549 | #} |
---|
| 1550 | |
---|
| 1551 | # read a lookup hash from file - may be called from amavisd.conf . |
---|
| 1552 | # |
---|
| 1553 | # Format: one key per line, anything from '#' to the end of line |
---|
| 1554 | # is considered a comment, but '#' within correctly quoted rfc2821 |
---|
| 1555 | # addresses is not treated as a comment (e.g. a hash sign within |
---|
| 1556 | # "strange # \"foo\" address"@example.com is part of the string). |
---|
| 1557 | # Lines may contain a pair: key value, separated by whitespace, or key only, |
---|
| 1558 | # in which case a value 1 is implied. Trailing whitespace is discarded, |
---|
| 1559 | # empty lines (containing only whitespace and comment) are ignored. |
---|
| 1560 | # Addresses (lefthand-side) are converted from rfc2821-quoted form |
---|
| 1561 | # into internal (raw) form and inserted as keys into a given hash. |
---|
| 1562 | # NOTE: the format is partly compatible with Postfix maps (not aliases): |
---|
| 1563 | # no continuation lines are honoured, Postfix maps do not allow |
---|
| 1564 | # rfc2821-quoted addresses containing whitespace, Postfix only allow |
---|
| 1565 | # comments starting at the beginning of the line. |
---|
| 1566 | # |
---|
| 1567 | # The $hashref argument is returned for convenience, so that one can do |
---|
| 1568 | # for example: |
---|
| 1569 | # $per_recip_whitelist_sender_lookup_tables = { |
---|
| 1570 | # '.my1.example.com' => read_hash({},'/var/amavis/my1-example-com.wl'), |
---|
| 1571 | # '.my2.example.com' => read_hash({},'/var/amavis/my2-example-com.wl') } |
---|
| 1572 | # or even simpler: |
---|
| 1573 | # $per_recip_whitelist_sender_lookup_tables = { |
---|
| 1574 | # '.my1.example.com' => read_hash('/var/amavis/my1-example-com.wl'), |
---|
| 1575 | # '.my2.example.com' => read_hash('/var/amavis/my2-example-com.wl') } |
---|
| 1576 | # |
---|
| 1577 | sub read_hash(@) { |
---|
| 1578 | unshift(@_,{}) if !ref $_[0]; # first argument is optional, defaults to {} |
---|
| 1579 | my($hashref, $filename, $keep_case) = @_; |
---|
| 1580 | my($lpcs) = c('localpart_is_case_sensitive'); |
---|
| 1581 | my($inp) = IO::File->new; |
---|
| 1582 | $inp->open($filename,'<') or die "Can't open file $filename for reading: $!"; |
---|
| 1583 | while (<$inp>) { # carefully handle comments, '#' within "" does not count |
---|
| 1584 | chomp; |
---|
| 1585 | my($lhs) = ''; my($rhs) = ''; my($at_rhs) = 0; |
---|
| 1586 | for my $t ( /\G ( " (?: \\. | [^"\\] )* " | |
---|
| 1587 | [^#" \t]+ | [ \t]+ | . )/gcsx) { |
---|
| 1588 | last if $t eq '#'; |
---|
| 1589 | if (!$at_rhs && $t =~ /^[ \t]+\z/) { $at_rhs = 1 } |
---|
| 1590 | else { ($at_rhs ? $rhs : $lhs) .= $t } |
---|
| 1591 | } |
---|
| 1592 | $rhs =~ s/[ \t]+\z//; # trim trailing whitespace |
---|
| 1593 | next if $lhs eq '' && $rhs eq ''; |
---|
| 1594 | my($addr) = Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($lhs); |
---|
| 1595 | my($localpart,$domain) = Amavis::rfc2821_2822_Tools::split_address($addr); |
---|
| 1596 | $localpart = lc($localpart) if !$lpcs; |
---|
| 1597 | $addr = $localpart . lc($domain); |
---|
| 1598 | $hashref->{$addr} = $rhs eq '' ? 1 : $rhs; |
---|
| 1599 | # do_log(5, "read_hash: address: <$addr>: ".$hashref->{$addr}); |
---|
| 1600 | } |
---|
| 1601 | $inp->close or die "Can't close $filename: $!"; |
---|
| 1602 | $hashref; |
---|
| 1603 | } |
---|
| 1604 | |
---|
| 1605 | # Run specified command as a subprocess (like qx operator, but more careful |
---|
| 1606 | # with error reporting and cancels :utf8 mode). Return a file handle open |
---|
| 1607 | # for reading from the subprocess. Use IO::Handle to ensure the subprocess |
---|
| 1608 | # will be automatically reclaimed in case of failure. |
---|
| 1609 | # |
---|
| 1610 | sub run_command($$@) { |
---|
| 1611 | my($stdin_from, $stderr_to, $cmd, @args) = @_; |
---|
| 1612 | my($cmd_text) = join(' ', $cmd, @args); |
---|
| 1613 | $stdin_from = '/dev/null' if $stdin_from eq ''; |
---|
| 1614 | my($msg) = join(' ', $cmd, @args, "<$stdin_from"); |
---|
| 1615 | $msg .= " 2>$stderr_to" if $stderr_to ne ''; |
---|
| 1616 | my($pid); my($proc_fh) = IO::File->new; |
---|
| 1617 | eval { $pid = $proc_fh->open('-|') }; # fork, catching errors |
---|
| 1618 | if ($@ ne '') { chomp($@); die "run_command (open pipe): $@" } |
---|
| 1619 | defined($pid) or die "run_command: can't fork: $!"; |
---|
| 1620 | if (!$pid) { # child |
---|
| 1621 | eval { # must not use die in forked process, or we end up with |
---|
| 1622 | # two running daemons! Close unneeded files. |
---|
| 1623 | # use Devel::Symdump (); |
---|
| 1624 | # my($dumpobj) = Devel::Symdump->rnew; |
---|
| 1625 | # for my $k ($dumpobj->ios) { |
---|
| 1626 | # no strict 'refs'; |
---|
| 1627 | # my($fn) = fileno($k); |
---|
| 1628 | # if ($fn == 1 || $fn == 2) { |
---|
| 1629 | # do_log(2,sprintf("KEEPING %s, fileno=%s", $k, $fn)); |
---|
| 1630 | # } else { |
---|
| 1631 | # $! = undef; close(*{$k}{IO}) and do_log(0, "DID CLOSE $k (fileno=$fn)"); |
---|
| 1632 | # } |
---|
| 1633 | # } |
---|
| 1634 | close(STDIN) or die "Can't close STDIN: $!"; |
---|
| 1635 | close(main::stdin) or die "Can't close main::stdin: $!"; |
---|
| 1636 | open(STDIN, "<$stdin_from") |
---|
| 1637 | or die "Can't reopen STDIN on $stdin_from: $!"; |
---|
| 1638 | fileno(STDIN) == 0 or die ("run_command: STDIN not fd0: ".fileno(STDIN)); |
---|
| 1639 | if ($stderr_to ne '') { |
---|
| 1640 | close(STDERR) or die "Can't close STDERR: $!"; |
---|
| 1641 | open(STDERR, ">$stderr_to") |
---|
| 1642 | or die "Can't open STDERR to $stderr_to: $!"; |
---|
| 1643 | fileno(STDERR) == 2 |
---|
| 1644 | or die ("run_command: STDERR not fd2: ".fileno(STDERR)); |
---|
| 1645 | } |
---|
| 1646 | # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC |
---|
| 1647 | { no warnings; |
---|
| 1648 | exec {$cmd} ($cmd,@args) or die "Failed to exec $cmd_text: $!"; |
---|
| 1649 | } |
---|
| 1650 | }; |
---|
| 1651 | chomp($@); |
---|
| 1652 | do_log(-2,"run_command: child process [$$]: $@\n"); |
---|
| 1653 | { no warnings; |
---|
| 1654 | kill('KILL',$$) # must not exit, we have to avoid DESTROY handlers |
---|
| 1655 | or do_log(-3,"run_command: TROUBLE - Panic1, can't die: $!"); |
---|
| 1656 | # still kicking? die! |
---|
| 1657 | exec('/usr/bin/false'); exec('/bin/false'); exec('false'); exec('true'); |
---|
| 1658 | do_log(-3,"run_command: TROUBLE - Panic2, can't die"); |
---|
| 1659 | exit 1; # better safe than sorry |
---|
| 1660 | # NOTREACHED |
---|
| 1661 | } |
---|
| 1662 | } |
---|
| 1663 | # parent |
---|
| 1664 | do_log(5,"run_command: [$pid] $msg"); |
---|
| 1665 | binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1 |
---|
| 1666 | ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID |
---|
| 1667 | } |
---|
| 1668 | |
---|
| 1669 | # Run specified command as a subprocess. Return a file handle open for |
---|
| 1670 | # WRITING to the subprocess. Use IO::Handle to ensure the subprocess |
---|
| 1671 | # will be automatically reclaimed in case of failure. |
---|
| 1672 | # |
---|
| 1673 | sub run_command_consumer($$@) { |
---|
| 1674 | my($stdout_to, $stderr_to, $cmd, @args) = @_; |
---|
| 1675 | my($cmd_text) = join(' ', $cmd, @args); |
---|
| 1676 | $stdout_to = '/dev/null' if $stdout_to eq ''; |
---|
| 1677 | my($msg) = join(' ', $cmd, @args, ">$stdout_to"); |
---|
| 1678 | $msg .= " 2>$stderr_to" if $stderr_to ne ''; |
---|
| 1679 | my($pid); my($proc_fh) = IO::File->new; |
---|
| 1680 | eval { $pid = $proc_fh->open('|-') }; # fork, catching errors |
---|
| 1681 | if ($@ ne '') { chomp($@); die "run_command_consumer (open pipe): $@" } |
---|
| 1682 | defined($pid) or die "run_command_consumer: can't fork: $!"; |
---|
| 1683 | if (!$pid) { # child |
---|
| 1684 | eval { # must not use die in forked process, or we end up with |
---|
| 1685 | # two running daemons! Close unneeded files. |
---|
| 1686 | close(main::stderr) or die "Can't close main::stderr: $!"; |
---|
| 1687 | close(main::stdout) or die "Can't close main::stdout: $!"; |
---|
| 1688 | close(main::STDOUT) or die "Can't close main::STDOUT: $!"; |
---|
| 1689 | open(STDOUT, ">$stdout_to") |
---|
| 1690 | or die "Can't reopen STDOUT on $stdout_to: $!"; |
---|
| 1691 | fileno(STDOUT) == 1 |
---|
| 1692 | or die ("run_command_consumer: STDOUT not fd1: ".fileno(STDOUT)); |
---|
| 1693 | if ($stderr_to ne '') { |
---|
| 1694 | close(STDERR) or die "Can't close STDERR: $!"; |
---|
| 1695 | open(STDERR, ">$stderr_to") |
---|
| 1696 | or die "Can't open STDERR to $stderr_to: $!"; |
---|
| 1697 | fileno(STDERR) == 2 |
---|
| 1698 | or die ("run_command_consumer: STDERR not fd2: ".fileno(STDERR)); |
---|
| 1699 | } |
---|
| 1700 | # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC |
---|
| 1701 | { no warnings; |
---|
| 1702 | exec {$cmd} ($cmd,@args) or die "Failed to exec $cmd_text: $!"; |
---|
| 1703 | } |
---|
| 1704 | }; |
---|
| 1705 | chomp($@); |
---|
| 1706 | do_log(-2,"run_command_consumer: child process [$$]: $@\n"); |
---|
| 1707 | { no warnings; |
---|
| 1708 | kill('KILL',$$) # must not exit, we have to avoid DESTROY handlers |
---|
| 1709 | or do_log(-3,"run_command_consumer: TROUBLE - Panic1, can't die: $!"); |
---|
| 1710 | # still kicking? die! |
---|
| 1711 | exec('/usr/bin/false'); exec('/bin/false'); exec('false'); exec('true'); |
---|
| 1712 | do_log(-3,"run_command_consumer: TROUBLE - Panic2, can't die"); |
---|
| 1713 | exit 1; # better safe than sorry |
---|
| 1714 | # NOTREACHED |
---|
| 1715 | } |
---|
| 1716 | } |
---|
| 1717 | # parent |
---|
| 1718 | do_log(5,"run_command_consumer: [$pid] $msg"); |
---|
| 1719 | binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1 |
---|
| 1720 | ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID |
---|
| 1721 | } |
---|
| 1722 | |
---|
| 1723 | 1; |
---|
| 1724 | |
---|
| 1725 | # |
---|
| 1726 | package Amavis::rfc2821_2822_Tools; |
---|
| 1727 | use strict; |
---|
| 1728 | use re 'taint'; |
---|
| 1729 | |
---|
| 1730 | BEGIN { |
---|
| 1731 | use Exporter (); |
---|
| 1732 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 1733 | $VERSION = '2.034'; |
---|
| 1734 | @ISA = qw(Exporter); |
---|
| 1735 | %EXPORT_TAGS = (); |
---|
| 1736 | @EXPORT = qw( |
---|
| 1737 | &iso8601_timestamp &iso8601_utc_timestamp &rfc2822_timestamp |
---|
| 1738 | &received_line &parse_received |
---|
| 1739 | &fish_out_ip_from_received &split_address &split_localpart &make_query_keys |
---|
| 1740 | "e_rfc2821_local &qquote_rfc2821_local &unquote_rfc2821_local |
---|
| 1741 | &one_response_for_all |
---|
| 1742 | &EX_OK &EX_NOUSER &EX_UNAVAILABLE &EX_TEMPFAIL &EX_NOPERM); |
---|
| 1743 | } |
---|
| 1744 | |
---|
| 1745 | use subs @EXPORT; |
---|
| 1746 | |
---|
| 1747 | use POSIX qw(locale_h strftime); |
---|
| 1748 | |
---|
| 1749 | BEGIN { |
---|
| 1750 | eval { require 'sysexits.ph' }; # try to use the installed version |
---|
| 1751 | # define the most important constants if undefined |
---|
| 1752 | do { sub EX_OK() {0} } unless defined(&EX_OK); |
---|
| 1753 | do { sub EX_NOUSER() {67} } unless defined(&EX_NOUSER); |
---|
| 1754 | do { sub EX_UNAVAILABLE() {69} } unless defined(&EX_UNAVAILABLE); |
---|
| 1755 | do { sub EX_TEMPFAIL() {75} } unless defined(&EX_TEMPFAIL); |
---|
| 1756 | do { sub EX_NOPERM() {77} } unless defined(&EX_NOPERM); |
---|
| 1757 | } |
---|
| 1758 | |
---|
| 1759 | BEGIN { |
---|
| 1760 | import Amavis::Conf qw(:platform $myhostname c cr ca); |
---|
| 1761 | import Amavis::Util qw(ll do_log); |
---|
| 1762 | } |
---|
| 1763 | |
---|
| 1764 | # Given a Unix time, return the local time zone offset at that time |
---|
| 1765 | # as a string +HHMM or -HHMM, appropriate for the RFC2822 date format. |
---|
| 1766 | # Works also for non-full-hour zone offsets, and on systems where strftime |
---|
| 1767 | # can not return TZ offset as a number; (c) Mark Martinec, GPL |
---|
| 1768 | # |
---|
| 1769 | sub get_zone_offset($) { |
---|
| 1770 | my($t) = @_; |
---|
| 1771 | my($d) = 0; # local zone offset in seconds |
---|
| 1772 | for (1..3) { # match the date (with a safety loop limit just in case) |
---|
| 1773 | my($r) = sprintf("%04d%02d%02d", (localtime($t))[5, 4, 3]) cmp |
---|
| 1774 | sprintf("%04d%02d%02d", (gmtime($t + $d))[5, 4, 3]); |
---|
| 1775 | if ($r == 0) { last } else { $d += $r * 24 * 3600 } |
---|
| 1776 | } |
---|
| 1777 | my($sl,$su) = (0,0); |
---|
| 1778 | for ((localtime($t))[2,1,0]) { $sl = $sl * 60 + $_ } |
---|
| 1779 | for ((gmtime($t + $d))[2,1,0]) { $su = $su * 60 + $_ } |
---|
| 1780 | $d += $sl - $su; # add HMS difference (in seconds) |
---|
| 1781 | my($sign) = $d >= 0 ? '+' : '-'; |
---|
| 1782 | $d = -$d if $d < 0; |
---|
| 1783 | $d = int(($d + 30) / 60.0); # give minutes, rounded |
---|
| 1784 | sprintf("%s%02d%02d", $sign, int($d / 60), $d % 60); |
---|
| 1785 | } |
---|
| 1786 | |
---|
| 1787 | # Given a Unix time (seconds since 1970-01-01T00:00Z), |
---|
| 1788 | # provide date-time timestamp (local time) as specified in ISO 8601 (EN 28601) |
---|
| 1789 | sub iso8601_timestamp($;$) { |
---|
| 1790 | my($t,$suppress_zone) = @_; |
---|
| 1791 | # can't use %z because some systems do not support it (is treated as %Z) |
---|
| 1792 | my($s) = strftime("%Y%m%dT%H%M%S", localtime($t)); |
---|
| 1793 | $s .= get_zone_offset($t) unless $suppress_zone; |
---|
| 1794 | $s; |
---|
| 1795 | } |
---|
| 1796 | |
---|
| 1797 | # Given a Unix time (seconds since 1970-01-01T00:00Z), |
---|
| 1798 | # provide date-time timestamp (UTC) as specified in ISO 8601 (EN 28601) |
---|
| 1799 | sub iso8601_utc_timestamp($;$) { |
---|
| 1800 | my($t,$suppress_zone) = @_; |
---|
| 1801 | my($s) = strftime("%Y%m%dT%H%M%S", gmtime($t)); |
---|
| 1802 | $s .= 'Z' unless $suppress_zone; |
---|
| 1803 | $s; |
---|
| 1804 | } |
---|
| 1805 | |
---|
| 1806 | # Given a Unix time, provide date-time timestamp as specified in RFC 2822 |
---|
| 1807 | # (local time), to be used in headers such as 'Date:' and 'Received:' |
---|
| 1808 | # |
---|
| 1809 | sub rfc2822_timestamp($) { |
---|
| 1810 | my($t) = @_; |
---|
| 1811 | my(@lt) = localtime($t); |
---|
| 1812 | # can't use %z because some systems do not support it (is treated as %Z) |
---|
| 1813 | my($old_locale) = setlocale(LC_TIME,"C"); # English dates required! |
---|
| 1814 | my($zone_name) = strftime("%Z",@lt); |
---|
| 1815 | my($s) = strftime("%a, %e %b %Y %H:%M:%S ", @lt); |
---|
| 1816 | $s .= get_zone_offset($t); |
---|
| 1817 | $s .= " (" . $zone_name . ")" if $zone_name !~ /^\s*\z/; |
---|
| 1818 | setlocale(LC_TIME, $old_locale); # restore the locale |
---|
| 1819 | $s; |
---|
| 1820 | } |
---|
| 1821 | |
---|
| 1822 | sub received_line($$$$) { |
---|
| 1823 | my($conn, $msginfo, $id, $folded) = @_; |
---|
| 1824 | my($smtp_proto, $recips) = ($conn->smtp_proto, $msginfo->recips); |
---|
| 1825 | my($client_ip) = $conn->client_ip; |
---|
| 1826 | if ($client_ip =~ /:/ && $client_ip !~ /^IPv6:/i) { |
---|
| 1827 | $client_ip = 'IPv6:' . $client_ip; |
---|
| 1828 | } |
---|
| 1829 | my($s) = sprintf("from %s%s\n by %s%s (amavisd-new, %s)", |
---|
| 1830 | ($conn->smtp_helo eq '' ? 'unknown' : $conn->smtp_helo), |
---|
| 1831 | ($client_ip eq '' ? '' : " ([$client_ip])"), |
---|
| 1832 | c('localhost_name'), |
---|
| 1833 | ($conn->socket_ip eq '' ? '' |
---|
| 1834 | : sprintf(" (%s [%s])", $myhostname, $conn->socket_ip) ), |
---|
| 1835 | ($conn->socket_port eq '' ? 'unix socket' : "port ".$conn->socket_port) ); |
---|
| 1836 | # must not use proto name QMQPqq in 'with' |
---|
| 1837 | $s .= "\n with $smtp_proto" if $smtp_proto=~/^(ES|S|L)MTPS?A?\z/i; # rfc3848 |
---|
| 1838 | $s .= "\n id $id" if $id ne ''; |
---|
| 1839 | # do not disclose recipients if more than one |
---|
| 1840 | $s .= "\n for " . qquote_rfc2821_local(@$recips) if @$recips == 1; |
---|
| 1841 | $s .= ";\n " . rfc2822_timestamp($msginfo->rx_time); |
---|
| 1842 | $s =~ s/\n//g if !$folded; |
---|
| 1843 | $s; |
---|
| 1844 | } |
---|
| 1845 | |
---|
| 1846 | sub parse_received($) { |
---|
| 1847 | my($received) = @_; |
---|
| 1848 | local($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11); |
---|
| 1849 | $received =~ s/\n([ \t])/$1/g; # unfold |
---|
| 1850 | $received =~ s/[\n\r]//g; # delete remaining newlines if any |
---|
| 1851 | my(%fields); |
---|
| 1852 | while ($received =~ m{\G\s* |
---|
| 1853 | ( \b(from|by) \s+ ( (?: \[ (?: \\. | [^\]\\] )* \] | [^;\s\[] )+ ) |
---|
| 1854 | (?: \s* \( (?: ( [^\s\[]+ ) \s+ )? |
---|
| 1855 | \[ ( (?: \\. | [^\]\\] )* ) \] \s* |
---|
| 1856 | \) )? |
---|
| 1857 | (?: .*? ) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) # junk |
---|
| 1858 | | \b(via|with|id|for) \s+ |
---|
| 1859 | ( (?: " (?: \\. | [^"\\] )* " |
---|
| 1860 | | \[ (?: \\. | [^\]\\] )* \] |
---|
| 1861 | | \\. | . |
---|
| 1862 | )+? (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) ) |
---|
| 1863 | | (;) \s* ( .*? ) \s* \z # time |
---|
| 1864 | | (.*?) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) # junk |
---|
| 1865 | ) ( (?: \s+ | (?: \( (?: \\. | [^)\\] )* \) ) )* ) }xgcsi) |
---|
| 1866 | { |
---|
| 1867 | my($v1, $v2, $v3, $comment) = ('') x 4; |
---|
| 1868 | my($item, $field) = ($1, lc($2 || $6 || $8)); |
---|
| 1869 | if ($field eq 'from' || $field eq 'by') { |
---|
| 1870 | ($v1, $v2, $v3, $comment) = ($3, $4, $5, $11); |
---|
| 1871 | } elsif ($field eq ';') { # time |
---|
| 1872 | ($v1, $comment) = ($9, $11); |
---|
| 1873 | } elsif ($10 eq '') { # via|with|id|for |
---|
| 1874 | ($v1, $comment) = ($7, $11); |
---|
| 1875 | } else { # junk |
---|
| 1876 | ($v1, $comment) = ($10, $11); |
---|
| 1877 | } |
---|
| 1878 | $comment =~ s/^\s+//; |
---|
| 1879 | $comment =~ s/\s+\z//; |
---|
| 1880 | $item =~ s/^\Q$field\E\s*//i; |
---|
| 1881 | if (!exists $fields{$field}) { |
---|
| 1882 | $fields{$field} = [$item, $v1, $v2, $v3, $comment]; |
---|
| 1883 | do_log(5, "parse_received: $field = $item/$v1/$v2/$v3") if $field ne ''; |
---|
| 1884 | } |
---|
| 1885 | } |
---|
| 1886 | \%fields; |
---|
| 1887 | } |
---|
| 1888 | |
---|
| 1889 | sub fish_out_ip_from_received($) { |
---|
| 1890 | my($received) = @_; |
---|
| 1891 | my($ip); |
---|
| 1892 | my($fields_ref) = parse_received($received); |
---|
| 1893 | if (defined $fields_ref && exists $fields_ref->{'from'}) { |
---|
| 1894 | my($item, $v1, $v2, $v3, $comment) = @{$fields_ref->{'from'}}; |
---|
| 1895 | for ($v3, $v2, $v1, $comment, $item) { |
---|
| 1896 | if (/ \[ (\d{1,3} (?: \. \d{1,3}){3}) \] /x) { |
---|
| 1897 | $ip = $1; last; |
---|
| 1898 | } elsif (/ (\d{1,3} (?: \. \d{1,3}){3}) (?!\d) /x) { |
---|
| 1899 | $ip = $1; last; |
---|
| 1900 | } elsif (/ \[ (IPv6:)? ( ([0-9a-zA-Z]* : ){2,} [0-9a-zA-Z:.]* ) \] /xi) { |
---|
| 1901 | $ip = $2; last; |
---|
| 1902 | } |
---|
| 1903 | } |
---|
| 1904 | do_log(5, "fish_out_ip_from_received: $ip, $item"); |
---|
| 1905 | } |
---|
| 1906 | !defined($ip) ? undef : $ip; # undef need not be tainted |
---|
| 1907 | } |
---|
| 1908 | |
---|
| 1909 | # Splits unquoted fully qualified e-mail address, or an address |
---|
| 1910 | # with missing domain part. Returns a pair: (localpart, domain). |
---|
| 1911 | # The domain part (if nonempty) includes the '@' as the first character. |
---|
| 1912 | # If the syntax is badly broken, everything ends up as the localpart. |
---|
| 1913 | # The domain part can be an address literal, as specified by rfc2822. |
---|
| 1914 | # Does not handle explicit route paths. |
---|
| 1915 | # |
---|
| 1916 | sub split_address($) { |
---|
| 1917 | my($mailbox) = @_; |
---|
| 1918 | $mailbox =~ /^ (.*?) ( \@ (?: \[ (?: \\. | [^\]\\] )* \] |
---|
| 1919 | | [^@"<>\[\]\\\s] )* |
---|
| 1920 | ) \z/xs ? ($1, $2) : ($mailbox, ''); |
---|
| 1921 | } |
---|
| 1922 | |
---|
| 1923 | # split_localpart() splits localpart of an e-mail address at the first |
---|
| 1924 | # occurrence of the address extension delimiter character. (based on |
---|
| 1925 | # equivalent routine in Postfix) |
---|
| 1926 | # |
---|
| 1927 | # Reserved addresses are not split: postmaster, mailer-daemon, |
---|
| 1928 | # double-bounce. Addresses that begin with owner-, or addresses |
---|
| 1929 | # that end in -request are not split when the owner_request_special |
---|
| 1930 | # parameter is set. |
---|
| 1931 | |
---|
| 1932 | sub split_localpart($$) { |
---|
| 1933 | my($localpart, $delimiter) = @_; |
---|
| 1934 | my($owner_request_special) = 1; # configurable ??? |
---|
| 1935 | my($extension); |
---|
| 1936 | if ($localpart =~ /^(postmaster|mailer-daemon|double-bounce)\z/i) { |
---|
| 1937 | # do not split these, regardless of what the delimiter is |
---|
| 1938 | } elsif ($delimiter eq '-' && $owner_request_special && |
---|
| 1939 | $localpart =~ /^owner-|-request\z/i) { |
---|
| 1940 | # don't split owner-foo or foo-request |
---|
| 1941 | } elsif ($localpart =~ /^(.+?)\Q$delimiter\E(.*)\z/s) { |
---|
| 1942 | ($localpart, $extension) = ($1, $2); |
---|
| 1943 | # do not split the address if the result would have a null localpart |
---|
| 1944 | } |
---|
| 1945 | ($localpart, $extension); |
---|
| 1946 | } |
---|
| 1947 | |
---|
| 1948 | # For a given email address (e.g. for User+Foo@sub.exAMPLE.CoM) |
---|
| 1949 | # prepare and return a list of lookup keys in the following order: |
---|
| 1950 | # User+Foo@sub.exAMPLE.COM (as-is, no lowercasing) |
---|
| 1951 | # user+foo@sub.example.com |
---|
| 1952 | # user@sub.example.com (only if $recipient_delimiter nonempty) |
---|
| 1953 | # user+foo(@) (only if $include_bare_user) |
---|
| 1954 | # user(@) (only if $include_bare_user and $recipient_delimiter nonempty) |
---|
| 1955 | # (@)sub.example.com |
---|
| 1956 | # (@).sub.example.com |
---|
| 1957 | # (@).example.com |
---|
| 1958 | # (@).com |
---|
| 1959 | # (@). |
---|
| 1960 | # Note about (@): if $at_with_user is true the user-only keys (without domain) |
---|
| 1961 | # get an '@' character appended (e.g. 'user+foo@'). Usual for lookup_hash. |
---|
| 1962 | # If $at_with_user is false the domain-only (no user localpart) keys |
---|
| 1963 | # get a '@' prepended (e.g. '@.example.com'). Usual for lookup_sql. |
---|
| 1964 | # |
---|
| 1965 | # The domain part is lowercased in all but the first resulting item; |
---|
| 1966 | # the localpart is lowercased iff $localpart_is_case_sensitive is true. |
---|
| 1967 | # |
---|
| 1968 | sub make_query_keys($$$) { |
---|
| 1969 | my($addr,$at_with_user,$include_bare_user) = @_; |
---|
| 1970 | my($localpart,$domain) = split_address($addr); $domain = lc($domain); |
---|
| 1971 | my($saved_full_localpart) = $localpart; |
---|
| 1972 | $localpart = lc($localpart) if !c('localpart_is_case_sensitive'); |
---|
| 1973 | # chop off leading @, and trailing dots |
---|
| 1974 | $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s; |
---|
| 1975 | my($extension); my($delim) = c('recipient_delimiter'); |
---|
| 1976 | if ($delim ne '') { |
---|
| 1977 | ($localpart,$extension) = split_localpart($localpart,$delim); |
---|
| 1978 | } |
---|
| 1979 | my($append_to_user,$prepend_to_domain) = $at_with_user ? ('@','') : ('','@'); |
---|
| 1980 | my(@keys); # a list of query keys |
---|
| 1981 | push(@keys, $addr); # as is |
---|
| 1982 | push(@keys, $localpart.$delim.$extension.'@'.$domain) |
---|
| 1983 | if $extension ne ''; # user+foo@example.com |
---|
| 1984 | push(@keys, $localpart.'@'.$domain); # user@example.com |
---|
| 1985 | if ($include_bare_user) { # typically enabled for local users only |
---|
| 1986 | push(@keys, $localpart.$delim.$extension.$append_to_user) |
---|
| 1987 | if $extension ne ''; # user+foo(@) |
---|
| 1988 | push(@keys, $localpart.$append_to_user); # user(@) |
---|
| 1989 | } |
---|
| 1990 | push(@keys, $prepend_to_domain.$domain); # (@)sub.example.com |
---|
| 1991 | if ($domain =~ /\[/) { # don't split address literals |
---|
| 1992 | push(@keys, $prepend_to_domain.'.'); # (@). |
---|
| 1993 | } else { |
---|
| 1994 | my(@dkeys); my($d) = $domain; |
---|
| 1995 | for (;;) { # (@).sub.example.com (@).example.com (@).com (@). |
---|
| 1996 | push(@dkeys, $prepend_to_domain.'.'.$d); |
---|
| 1997 | last if $d eq ''; |
---|
| 1998 | $d = ($d =~ /^([^.]*)\.(.*)\z/s) ? $2 : ''; |
---|
| 1999 | } |
---|
| 2000 | if (@dkeys > 10) { @dkeys = @dkeys[$#dkeys-9 .. $#dkeys] } # sanity limit |
---|
| 2001 | push(@keys,@dkeys); |
---|
| 2002 | } |
---|
| 2003 | my($keys_ref) = []; # remove duplicates |
---|
| 2004 | for my $k (@keys) { push(@$keys_ref,$k) if !grep {$k eq $_} @$keys_ref } |
---|
| 2005 | ll(5) && do_log(5,"query_keys: ".join(', ',@$keys_ref)); |
---|
| 2006 | # the rhs replacement strings are similar to what would be obtained |
---|
| 2007 | # by lookup_re() given the following regular expression: |
---|
| 2008 | # /^( ( ( [^@]*? ) ( \Q$delim\E [^@]* )? ) (?: \@ (.*) ) )$/xs |
---|
| 2009 | my($rhs) = [ # a list of right-hand side replacement strings |
---|
| 2010 | $addr, # $1 = User+Foo@Sub.Example.COM |
---|
| 2011 | $saved_full_localpart, # $2 = User+Foo |
---|
| 2012 | $localpart, # $3 = user |
---|
| 2013 | $delim.$extension, # $4 = +foo |
---|
| 2014 | $domain, # $5 = sub.example.com |
---|
| 2015 | ]; |
---|
| 2016 | ($keys_ref, $rhs); |
---|
| 2017 | } |
---|
| 2018 | |
---|
| 2019 | # quote_rfc2821_local() quotes the local part of a mailbox address |
---|
| 2020 | # (given in internal (unquoted) form), and returns external (quoted) |
---|
| 2021 | # mailbox address, as per rfc2821. |
---|
| 2022 | # |
---|
| 2023 | # Internal (unquoted) form is used internally by amavisd-new and other mail sw, |
---|
| 2024 | # external (quoted) form is used in SMTP commands and message headers. |
---|
| 2025 | # |
---|
| 2026 | # The quote_rfc2821_local() conversion is necessary because addresses |
---|
| 2027 | # we get from certain MTAs are raw, with stripped-off quoting. |
---|
| 2028 | # To re-insert message back via SMTP, the local-part of the address needs |
---|
| 2029 | # to be quoted again if it contains reserved characters or otherwise |
---|
| 2030 | # does not obey the dot-atom syntax, as specified in rfc2821. |
---|
| 2031 | # Failing to do that gets us into trouble: amavis accepts message from MTA, |
---|
| 2032 | # but is unable to hand it back to MTA after checking, receiving |
---|
| 2033 | # '501 Bad address syntax' with every attempt. |
---|
| 2034 | # |
---|
| 2035 | sub quote_rfc2821_local($) { |
---|
| 2036 | my($mailbox) = @_; |
---|
| 2037 | # atext: any character except controls, SP, and specials (rfc2821/rfc2822) |
---|
| 2038 | my($atext) = "a-zA-Z0-9!#\$%&'*/=?^_`{|}~+-"; |
---|
| 2039 | # my($specials) = '()<>\[\]\\\\@:;,."'; |
---|
| 2040 | my($localpart,$domain) = split_address($mailbox); |
---|
| 2041 | if ($localpart !~ /^[$atext]+(\.[$atext]+)*\z/so) { # not dot-atom |
---|
| 2042 | $localpart =~ s/(["\\])/\\$1/g; # quoted-pair |
---|
| 2043 | $localpart = '"' . $localpart . '"'; # make a qcontent out of it |
---|
| 2044 | } |
---|
| 2045 | $domain = '' if $domain eq '@'; # strip off empty domain entirely |
---|
| 2046 | $localpart . $domain; |
---|
| 2047 | } |
---|
| 2048 | |
---|
| 2049 | # wraps the result of quote_rfc2821_local into angle brackets <...> ; |
---|
| 2050 | # If given a list, it returns a list (possibly converted to |
---|
| 2051 | # comma-separated scalar), quoting each element; |
---|
| 2052 | # |
---|
| 2053 | sub qquote_rfc2821_local(@) { |
---|
| 2054 | my(@r) = map { $_ eq '' ? '<>' : ('<' . quote_rfc2821_local($_) . '>') } @_; |
---|
| 2055 | wantarray ? @r : join(', ', @r); |
---|
| 2056 | } |
---|
| 2057 | |
---|
| 2058 | # unquote_rfc2821_local() strips away the quoting from the local part |
---|
| 2059 | # of an external (quoted) mailbox address, and returns internal (unquoted) |
---|
| 2060 | # mailbox address, as per rfc2821. |
---|
| 2061 | # |
---|
| 2062 | # Internal (unquoted) form is used internally by amavisd-new and other mail sw, |
---|
| 2063 | # external (quoted) form is used in SMTP commands and message headers. |
---|
| 2064 | # |
---|
| 2065 | sub unquote_rfc2821_local($) { |
---|
| 2066 | my($mailbox) = @_; |
---|
| 2067 | # the angle-bracket stripping is not really a duty of this subroutine, |
---|
| 2068 | # as it should have been already done elsewhere, but for the time being |
---|
| 2069 | # we do it here: |
---|
| 2070 | $mailbox = $1 if $mailbox =~ /^ \s* < ( .* ) > \s* \z/xs; |
---|
| 2071 | my($localpart,$domain) = split_address($mailbox); |
---|
| 2072 | $localpart =~ s/ " | \\ (.) | \\ \z /$1/xsg; # unquote quoted-pairs |
---|
| 2073 | $localpart . $domain; |
---|
| 2074 | } |
---|
| 2075 | |
---|
| 2076 | # Prepare a single SMTP response and an exit status as per sysexits.h |
---|
| 2077 | # from individual per-recipient response codes, taking into account |
---|
| 2078 | # sendmail milter specifics. Returns a triple: (smtp response, exit status, |
---|
| 2079 | # an indication whether DSN is needed). |
---|
| 2080 | # |
---|
| 2081 | sub one_response_for_all($$$) { |
---|
| 2082 | my($msginfo, $dsn_per_recip_capable, $am_id) = @_; |
---|
| 2083 | my($smtp_resp, $exit_code, $dsn_needed); |
---|
| 2084 | |
---|
| 2085 | my($delivery_method) = $msginfo->delivery_method; |
---|
| 2086 | my($sender) = $msginfo->sender; |
---|
| 2087 | my($per_recip_data) = $msginfo->per_recip_data; |
---|
| 2088 | my($any_not_done) = scalar(grep { !$_->recip_done } @$per_recip_data); |
---|
| 2089 | if ($delivery_method ne '' && $any_not_done) |
---|
| 2090 | { die "Explicit forwarding, but not all recips done" } |
---|
| 2091 | if (!@$per_recip_data) { # no recipients, nothing to do |
---|
| 2092 | $smtp_resp = "250 2.5.0 Ok, id=$am_id"; $exit_code = EX_OK; |
---|
| 2093 | do_log(5, "one_response_for_all <$sender>: no recipients, '$smtp_resp'"); |
---|
| 2094 | } |
---|
| 2095 | if (!defined $smtp_resp) { |
---|
| 2096 | for my $r (@$per_recip_data) { # any 4xx code ? |
---|
| 2097 | if ($r->recip_smtp_response =~ /^4/) # pick the first 4xx code |
---|
| 2098 | { $smtp_resp = $r->recip_smtp_response; last } |
---|
| 2099 | } |
---|
| 2100 | if (!defined $smtp_resp) { |
---|
| 2101 | for my $r (@$per_recip_data) { # any invalid code ? |
---|
| 2102 | if ($r->recip_done && $r->recip_smtp_response !~ /^[245]/) { |
---|
| 2103 | $smtp_resp = '451 4.5.0 Bad SMTP response code??? "' |
---|
| 2104 | . $r->recip_smtp_response . '"'; |
---|
| 2105 | last; # pick the first |
---|
| 2106 | } |
---|
| 2107 | } |
---|
| 2108 | } |
---|
| 2109 | if (defined $smtp_resp) { |
---|
| 2110 | $exit_code = EX_TEMPFAIL; |
---|
| 2111 | do_log(5, "one_response_for_all <$sender>: 4xx found, '$smtp_resp'"); |
---|
| 2112 | } |
---|
| 2113 | } |
---|
| 2114 | # NOTE: a 2xx SMTP response code is set both by internal Discard |
---|
| 2115 | # and by a genuine successful delivery. To distinguish between the two |
---|
| 2116 | # we need to check $r->recip_destiny as well. |
---|
| 2117 | # |
---|
| 2118 | if (!defined $smtp_resp) { |
---|
| 2119 | # if destiny for _all_ recipients is D_DISCARD => Discard |
---|
| 2120 | my($notall); |
---|
| 2121 | for my $r (@$per_recip_data) { |
---|
| 2122 | if ($r->recip_destiny == D_DISCARD) # pick the first DISCARD code |
---|
| 2123 | { $smtp_resp = $r->recip_smtp_response if !defined $smtp_resp } |
---|
| 2124 | else { $notall++; last } # one is not a discard, nogood |
---|
| 2125 | } |
---|
| 2126 | if ($notall) { $smtp_resp = undef } |
---|
| 2127 | if (defined $smtp_resp) { |
---|
| 2128 | # helper program will interpret 99 as discard |
---|
| 2129 | $exit_code = $delivery_method eq '' ? 99 : EX_OK; |
---|
| 2130 | do_log(5, "one_response_for_all <$sender>: all DISCARD, '$smtp_resp'"); |
---|
| 2131 | } |
---|
| 2132 | } |
---|
| 2133 | if (!defined $smtp_resp) { |
---|
| 2134 | # destiny for _all_ recipients is Discard or Reject => 5xx |
---|
| 2135 | # (and there is at least one Reject) |
---|
| 2136 | my($notall, $done_level); |
---|
| 2137 | my($bounce_cnt) = 0; |
---|
| 2138 | for my $r (@$per_recip_data) { |
---|
| 2139 | my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response); |
---|
| 2140 | if ($dest == D_DISCARD) { |
---|
| 2141 | # ok, this one is discard, let's see the rest |
---|
| 2142 | } elsif ($resp =~ /^5/ && $dest != D_BOUNCE) { |
---|
| 2143 | # prefer to report SMTP response code of genuine rejects |
---|
| 2144 | # from MTA, over internal rejects by content filters |
---|
| 2145 | if (!defined $smtp_resp || $r->recip_done > $done_level) |
---|
| 2146 | { $smtp_resp = $resp; $done_level = $r->recip_done } |
---|
| 2147 | } else { $notall++; last } # one is Pass or Bounce, nogood |
---|
| 2148 | } |
---|
| 2149 | if ($notall) { $smtp_resp = undef } |
---|
| 2150 | if (defined $smtp_resp) { |
---|
| 2151 | $exit_code = EX_UNAVAILABLE; |
---|
| 2152 | do_log(5, "one_response_for_all <$sender>: REJECTs, '$smtp_resp'"); |
---|
| 2153 | } |
---|
| 2154 | } |
---|
| 2155 | if (!defined $smtp_resp) { |
---|
| 2156 | # mixed destiny => 2xx, but generate dsn for bounces and rejects |
---|
| 2157 | my($rej_cnt) = 0; my($bounce_cnt) = 0; my($drop_cnt) = 0; |
---|
| 2158 | for my $r (@$per_recip_data) { |
---|
| 2159 | my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response); |
---|
| 2160 | if ($resp =~ /^2/ && $dest == D_PASS) # genuine successful delivery |
---|
| 2161 | { $smtp_resp = $resp if !defined $smtp_resp } |
---|
| 2162 | $drop_cnt++ if $dest == D_DISCARD; |
---|
| 2163 | if ($resp =~ /^5/) |
---|
| 2164 | { if ($dest == D_BOUNCE) { $bounce_cnt++ } else { $rej_cnt++ } } |
---|
| 2165 | } |
---|
| 2166 | $exit_code = EX_OK; |
---|
| 2167 | if (!defined $smtp_resp) { # no genuine Pass/2xx |
---|
| 2168 | # declare success, we'll handle bounce |
---|
| 2169 | $smtp_resp = "250 2.5.0 Ok, id=$am_id"; |
---|
| 2170 | if ($any_not_done) { $smtp_resp .= ", continue delivery" } |
---|
| 2171 | elsif ($delivery_method eq '') { $exit_code = 99 } # milter DISCARD |
---|
| 2172 | } |
---|
| 2173 | if ($rej_cnt + $bounce_cnt + $drop_cnt > 0) { |
---|
| 2174 | $smtp_resp .= ", "; |
---|
| 2175 | $smtp_resp .= "but " if $rej_cnt+$bounce_cnt+$drop_cnt<@$per_recip_data; |
---|
| 2176 | $smtp_resp .= join ", and ", |
---|
| 2177 | map { my($cnt, $nm) = @$_; |
---|
| 2178 | !$cnt ? () : $cnt == @$per_recip_data ? $nm : "$cnt $nm" |
---|
| 2179 | } ([$rej_cnt,'REJECT'], [$bounce_cnt,'BOUNCE'], [$drop_cnt,'DISCARD']); |
---|
| 2180 | } |
---|
| 2181 | $dsn_needed = |
---|
| 2182 | ($bounce_cnt > 0 || ($rej_cnt > 0 && !$dsn_per_recip_capable)) ? 1 : 0; |
---|
| 2183 | ll(5) && do_log(5,"one_response_for_all <$sender>: " |
---|
| 2184 | . ($rej_cnt + $bounce_cnt + $drop_cnt > 0 ? 'mixed' : 'success') |
---|
| 2185 | . ", dsn_needed=$dsn_needed, '$smtp_resp'"); |
---|
| 2186 | } |
---|
| 2187 | ($smtp_resp, $exit_code, $dsn_needed); |
---|
| 2188 | } |
---|
| 2189 | |
---|
| 2190 | 1; |
---|
| 2191 | |
---|
| 2192 | # |
---|
| 2193 | package Amavis::Lookup::RE; |
---|
| 2194 | use strict; |
---|
| 2195 | use re 'taint'; |
---|
| 2196 | |
---|
| 2197 | BEGIN { |
---|
| 2198 | use Exporter (); |
---|
| 2199 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 2200 | $VERSION = '2.034'; |
---|
| 2201 | @ISA = qw(Exporter); |
---|
| 2202 | } |
---|
| 2203 | BEGIN { import Amavis::Util qw(ll do_log) } |
---|
| 2204 | |
---|
| 2205 | # Make an object out of the supplied lookup list |
---|
| 2206 | # to make it distinguishable from simple ACL array |
---|
| 2207 | sub new($$) { my($class) = shift; bless [@_], $class } |
---|
| 2208 | |
---|
| 2209 | # lookup_re() performs a lookup for an e-mail address against |
---|
| 2210 | # a list made up of regular expressions. |
---|
| 2211 | # |
---|
| 2212 | # The full unmodified e-mail address is always used, so splitting to localpart |
---|
| 2213 | # and domain or lowercasing is NOT performed. The regexp is powerful enough |
---|
| 2214 | # that this can be accomplished by its mechanisms. The routine is useful for |
---|
| 2215 | # other RE tests besides the usual e-mail addresses, such as looking for |
---|
| 2216 | # banned file names. |
---|
| 2217 | # |
---|
| 2218 | # Each element of the list can be ref to a pair, or directly a regexp |
---|
| 2219 | # ('Regexp' object created by qr operator, or just a (less efficient) |
---|
| 2220 | # string containing a regular expression). If it is a pair, the first |
---|
| 2221 | # element is treated as a regexp, and the second provides a value in case |
---|
| 2222 | # the regexp matches. If not a pair, the implied result of a match is 1. |
---|
| 2223 | # |
---|
| 2224 | # The regular expression is taken as-is, no implicit anchoring or setting |
---|
| 2225 | # case insensitivity is done, so use qr'(?i)^user@example\.com$', |
---|
| 2226 | # and not a sloppy qr'user@example.com', which can easily backfire. |
---|
| 2227 | # Also, if qr is used with a delimiter other than ', make sure to quote |
---|
| 2228 | # the @ and $ . |
---|
| 2229 | # |
---|
| 2230 | # The pattern allows for capturing of parenthesized substrings, which can |
---|
| 2231 | # then be referenced from the result string using the $1, $2, ... notation, |
---|
| 2232 | # as with the Perl m// operator. The number after the $ may be a multi-digit |
---|
| 2233 | # decimal number. To avoid possible ambiguity the ${n} or $(n) form may be used |
---|
| 2234 | # Substring numbering starts with 1. Nonexistent references evaluate to empty |
---|
| 2235 | # strings. If any substitution is done, the result inherits the taintedness |
---|
| 2236 | # of $addr. Keep in mind that $ and @ characters needs to be backslash-quoted |
---|
| 2237 | # in qq() strings. Example: |
---|
| 2238 | # $virus_quarantine_to = new_RE( |
---|
| 2239 | # [ qr'^(.*)@example\.com$'i => 'virus-${1}@example.com' ], |
---|
| 2240 | # [ qr'^(.*)(@[^@]*)?$'i => 'virus-${1}${2}' ] ); |
---|
| 2241 | # |
---|
| 2242 | # Example (equivalent to the example in lookup_acl): |
---|
| 2243 | # $acl_re = Amavis::Lookup::RE->new( |
---|
| 2244 | # qr'@me\.ac\.uk$'i, [qr'[@.]ac\.uk$'i=>0], qr'\.uk$'i ); |
---|
| 2245 | # ($r,$k) = $acl_re->lookup_re('user@me.ac.uk'); |
---|
| 2246 | # or $r = lookup(0, 'user@me.ac.uk', $acl_re); |
---|
| 2247 | # |
---|
| 2248 | # 'user@me.ac.uk' matches me.ac.uk, returns true and search stops |
---|
| 2249 | # 'user@you.ac.uk' matches .ac.uk, returns false (because of =>0) and search stops |
---|
| 2250 | # 'user@them.co.uk' matches .uk, returns true and search stops |
---|
| 2251 | # 'user@some.com' does not match anything, falls through and returns false (undef) |
---|
| 2252 | |
---|
| 2253 | sub lookup_re($$;$) { |
---|
| 2254 | my($self, $addr,$get_all) = @_; |
---|
| 2255 | local($1,$2,$3,$4); my(@matchingkey,@result); |
---|
| 2256 | for my $e (@$self) { |
---|
| 2257 | my($key,$r); |
---|
| 2258 | if (ref($e) eq 'ARRAY') { # a pair: (regexp,result) |
---|
| 2259 | ($key,$r) = ($e->[0], @$e < 2 ? 1 : $e->[1]); |
---|
| 2260 | } else { # a single regexp (not a pair), implies result 1 |
---|
| 2261 | ($key,$r) = ($e, 1); |
---|
| 2262 | } |
---|
| 2263 | # do_log(5, "lookup_re: key=\"$addr\", matching against RE $key"); |
---|
| 2264 | ""=~/x{0}/; # braindead Perl: serves as explicit deflt for an empty regexp |
---|
| 2265 | my(@rhs) = $addr =~ /$key/; # match, capturing parenthesized subpatterns |
---|
| 2266 | if (@rhs) { # regexp matches |
---|
| 2267 | # do the righthand side replacements if any $n, ${n} or $(n) is specified |
---|
| 2268 | if (!ref($r) && $r=~/\$/) { |
---|
| 2269 | my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) } |
---|
| 2270 | { my($j)=$2+$3+$4; $j<1 ? '' : $rhs[$j-1] }gxse; |
---|
| 2271 | # bring taintedness of input to the result |
---|
| 2272 | $r .= substr($addr,0,0) if $any; |
---|
| 2273 | } |
---|
| 2274 | push(@result,$r); push(@matchingkey,$key); |
---|
| 2275 | last if !$get_all; |
---|
| 2276 | } |
---|
| 2277 | } |
---|
| 2278 | if (!ll(5)) { |
---|
| 2279 | # don't bother preparing log report which will not be printed |
---|
| 2280 | } elsif (!@result) { |
---|
| 2281 | do_log(5,"lookup_re($addr), no matches"); |
---|
| 2282 | } else { # pretty logging |
---|
| 2283 | my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b", |
---|
| 2284 | e => "\e", a => "\a", t => "\t"); |
---|
| 2285 | my(@mk) = @matchingkey; |
---|
| 2286 | for my $mk (@mk) # undo the \-quoting, will be redone by logging routines |
---|
| 2287 | { $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : $1 }egsx } |
---|
| 2288 | if (!$get_all) { # first match wins |
---|
| 2289 | do_log(5,sprintf('lookup_re(%s) matches key "%s", result=%s', |
---|
| 2290 | $addr,$mk[0],$result[0])); |
---|
| 2291 | } else { # want all matches |
---|
| 2292 | do_log(5,"lookup_re($addr) matches keys: ". |
---|
| 2293 | join(', ', map {sprintf('"%s"=>%s',$mk[$_],$result[$_])} |
---|
| 2294 | (0..$#result))); |
---|
| 2295 | } |
---|
| 2296 | } |
---|
| 2297 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
| 2298 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
| 2299 | } |
---|
| 2300 | |
---|
| 2301 | 1; |
---|
| 2302 | |
---|
| 2303 | # |
---|
| 2304 | package Amavis::Lookup::Label; |
---|
| 2305 | use strict; |
---|
| 2306 | use re 'taint'; |
---|
| 2307 | |
---|
| 2308 | # Make an object out of the supplied string, to serve as label |
---|
| 2309 | # in log messages generated by sub lookup |
---|
| 2310 | sub new($$) { my($class) = shift; my($str) = shift; bless \$str, $class } |
---|
| 2311 | sub display($) { my($self) = shift; $$self } |
---|
| 2312 | |
---|
| 2313 | 1; |
---|
| 2314 | |
---|
| 2315 | # |
---|
| 2316 | package Amavis::Lookup; |
---|
| 2317 | use strict; |
---|
| 2318 | use re 'taint'; |
---|
| 2319 | |
---|
| 2320 | BEGIN { |
---|
| 2321 | use Exporter (); |
---|
| 2322 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 2323 | $VERSION = '2.034'; |
---|
| 2324 | @ISA = qw(Exporter); |
---|
| 2325 | %EXPORT_TAGS = (); |
---|
| 2326 | @EXPORT = (); |
---|
| 2327 | @EXPORT_OK = qw(&lookup &lookup_ip_acl); |
---|
| 2328 | } |
---|
| 2329 | use subs @EXPORT_OK; |
---|
| 2330 | |
---|
| 2331 | BEGIN { |
---|
| 2332 | import Amavis::Util qw(ll do_log); |
---|
| 2333 | import Amavis::Conf qw(:platform c cr ca); |
---|
| 2334 | import Amavis::Timing qw(section_time); |
---|
| 2335 | import Amavis::rfc2821_2822_Tools qw(split_address make_query_keys); |
---|
| 2336 | } |
---|
| 2337 | |
---|
| 2338 | # lookup_hash() performs a lookup for an e-mail address against a hash map. |
---|
| 2339 | # If a match is found (a hash key exists in the Perl hash) the function returns |
---|
| 2340 | # whatever the map returns, otherwise undef is returned. First match wins, |
---|
| 2341 | # aborting further search sequence. |
---|
| 2342 | # |
---|
| 2343 | sub lookup_hash($$;$) { |
---|
| 2344 | my($addr, $hash_ref,$get_all) = @_; |
---|
| 2345 | (ref($hash_ref) eq 'HASH') |
---|
| 2346 | or die "lookup_hash: arg2 must be a hash ref: $hash_ref"; |
---|
| 2347 | local($1,$2,$3,$4); my(@matchingkey,@result); |
---|
| 2348 | my($keys_ref,$rhs_ref) = make_query_keys($addr,1,1); |
---|
| 2349 | for my $key (@$keys_ref) { # do the search |
---|
| 2350 | if (defined $$hash_ref{$key}) { # got it |
---|
| 2351 | push(@result,$$hash_ref{$key}); push(@matchingkey,$key); |
---|
| 2352 | last if !$get_all; |
---|
| 2353 | } |
---|
| 2354 | } |
---|
| 2355 | # do the right-hand side replacements if any $n, ${n} or $(n) is specified |
---|
| 2356 | for my $r (@result) { # remember that $r is just an alias to array elements |
---|
| 2357 | if (!ref($r) && $r=~/\$/) { # is a plain string containing a '$' |
---|
| 2358 | my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) } |
---|
| 2359 | { my($j)=$2+$3+$4; $j<1 ? '' : $rhs_ref->[$j-1] }gxse; |
---|
| 2360 | # bring taintedness of input to the result |
---|
| 2361 | $r .= substr($addr,0,0) if $any; |
---|
| 2362 | } |
---|
| 2363 | } |
---|
| 2364 | if (!ll(5)) { |
---|
| 2365 | # only bother with logging when needed |
---|
| 2366 | } elsif (!@result) { |
---|
| 2367 | do_log(5,"lookup_hash($addr), no matches"); |
---|
| 2368 | } elsif (!$get_all) { # first match wins |
---|
| 2369 | do_log(5,sprintf('lookup_hash(%s) matches key "%s", result=%s', |
---|
| 2370 | $addr,$matchingkey[0],$result[0])); |
---|
| 2371 | } else { # want all matches |
---|
| 2372 | do_log(5,"lookup_hash($addr) matches keys: ". |
---|
| 2373 | join(', ', map {sprintf('"%s"=>%s',$matchingkey[$_],$result[$_])} |
---|
| 2374 | (0..$#result))); |
---|
| 2375 | } |
---|
| 2376 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
| 2377 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
| 2378 | } |
---|
| 2379 | |
---|
| 2380 | # lookup_acl() performs a lookup for an e-mail address against |
---|
| 2381 | # access control list. |
---|
| 2382 | # |
---|
| 2383 | # Domain name of the supplied address is compared with each member of the |
---|
| 2384 | # access list in turn, the first match wins (terminates the search), |
---|
| 2385 | # and its value decides whether the result is true (yes, permit, pass) |
---|
| 2386 | # or false (no, deny, drop). Falling through without a match |
---|
| 2387 | # produces false (undef). Search is case-insensitive. |
---|
| 2388 | # |
---|
| 2389 | # If a list member contains a '@', the full e-mail address is compared, |
---|
| 2390 | # otherwise if a list member has a leading dot, the domain name part is |
---|
| 2391 | # matched only, and the domain as well as its subdomains can match. If there |
---|
| 2392 | # is no leading dot, the domain must match exactly (subdomains do not match). |
---|
| 2393 | # |
---|
| 2394 | # The presence of character '!' prepended to the list member decides |
---|
| 2395 | # whether the result will be true (without a '!') or false (with '!') |
---|
| 2396 | # in case this list member matches and terminates the search. |
---|
| 2397 | # |
---|
| 2398 | # Because search stops at the first match, it only makes sense |
---|
| 2399 | # to place more specific patterns before the more general ones. |
---|
| 2400 | # |
---|
| 2401 | # Although not a special case, it is good to remember that '.' always matches, |
---|
| 2402 | # so '.' would stop the search and return true, whereas '!.' would stop the |
---|
| 2403 | # search and return false (0) (which is normally not very useful, |
---|
| 2404 | # as false (undef) is also implied at the end of the list). |
---|
| 2405 | # |
---|
| 2406 | # Examples: |
---|
| 2407 | # |
---|
| 2408 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) |
---|
| 2409 | # 'me.ac.uk' matches me.ac.uk, returns true and search stops |
---|
| 2410 | # |
---|
| 2411 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) |
---|
| 2412 | # 'you.ac.uk' matches .ac.uk, returns false (because of '!') and search stops |
---|
| 2413 | # |
---|
| 2414 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) |
---|
| 2415 | # 'them.co.uk' matches .uk, returns true and search stops |
---|
| 2416 | # |
---|
| 2417 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) |
---|
| 2418 | # 'some.com' does not match anything, falls through and returns false (undef) |
---|
| 2419 | # |
---|
| 2420 | # given: @acl = qw( me.ac.uk !.ac.uk .uk !. ) |
---|
| 2421 | # 'some.com' similar to previous, except it returns 0 instead of undef |
---|
| 2422 | # |
---|
| 2423 | # given: @acl = qw( me.ac.uk !.ac.uk .uk . ) |
---|
| 2424 | # 'some.com' matches catchall ".", and returns true. The ".uk" is redundant |
---|
| 2425 | # |
---|
| 2426 | # more complex example: @acl = qw( |
---|
| 2427 | # !The.Boss@dept1.xxx.com .dept1.xxx.com |
---|
| 2428 | # .dept2.xxx.com .dept3.xxx.com lab.dept4.xxx.com |
---|
| 2429 | # sub.xxx.com !.sub.xxx.com |
---|
| 2430 | # me.d.aaa.com him.d.aaa.com !.d.aaa.com .aaa.com |
---|
| 2431 | # ); |
---|
| 2432 | |
---|
| 2433 | sub lookup_acl($$) { |
---|
| 2434 | my($addr, $acl_ref) = @_; |
---|
| 2435 | (ref($acl_ref) eq 'ARRAY') |
---|
| 2436 | or die "lookup_acl: arg2 must be a list ref: $acl_ref"; |
---|
| 2437 | return undef if !@$acl_ref; # empty list can't match anything |
---|
| 2438 | my($lpcs) = c('localpart_is_case_sensitive'); |
---|
| 2439 | my($localpart,$domain) = split_address($addr); $domain = lc($domain); |
---|
| 2440 | $localpart = lc($localpart) if !$lpcs; |
---|
| 2441 | local($1,$2); |
---|
| 2442 | # chop off leading @ and trailing dots |
---|
| 2443 | $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s; |
---|
| 2444 | my($lcaddr) = $localpart . '@' . $domain; |
---|
| 2445 | my($found, $matchingkey, $result); |
---|
| 2446 | for my $e (@$acl_ref) { |
---|
| 2447 | $result = 1; $matchingkey = $e; my($key) = $e; |
---|
| 2448 | if ($key =~ /^(!+)(.*)\z/s) { # starts with an exclamation mark(s) |
---|
| 2449 | $key = $2; |
---|
| 2450 | $result = 1-$result if (length($1) & 1); # negate if odd |
---|
| 2451 | } |
---|
| 2452 | if ($key =~ /^(.*?)\@([^@]*)\z/s) { # contains '@', check full address |
---|
| 2453 | $found++ if $localpart eq ($lpcs?$1:lc($1)) && $domain eq lc($2); |
---|
| 2454 | } elsif ($key =~ /^\.(.*)\z/s) { # leading dot: domain or subdomain |
---|
| 2455 | my($key_t) = lc($1); |
---|
| 2456 | $found++ if $domain eq $key_t || $domain =~ /(\.|\z)\Q$key_t\E\z/s; |
---|
| 2457 | } else { # match domain (but not its subdomains) |
---|
| 2458 | $found++ if $domain eq lc($key); |
---|
| 2459 | } |
---|
| 2460 | last if $found; |
---|
| 2461 | } |
---|
| 2462 | $matchingkey = $result = undef if !$found; |
---|
| 2463 | do_log(5, "lookup_acl($addr)". |
---|
| 2464 | (!$found?", no match":" matches key \"$matchingkey\", result=$result")); |
---|
| 2465 | !wantarray ? $result : ($result, $matchingkey); |
---|
| 2466 | } |
---|
| 2467 | |
---|
| 2468 | # Perform a lookup for an e-mail address against any number of supplied maps: |
---|
| 2469 | # - SQL map, |
---|
| 2470 | # - LDAP map, |
---|
| 2471 | # - hash map, |
---|
| 2472 | # - (access control) list, |
---|
| 2473 | # - a list of regular expressions, |
---|
| 2474 | # - a (defined) scalar always matches, and returns itself as the 'map' value |
---|
| 2475 | # (useful as a catchall for final 'pass' or 'fail'); |
---|
| 2476 | # (see lookup_hash, lookup_acl, lookup_sql and lookup_ldap for details). |
---|
| 2477 | # |
---|
| 2478 | # when $get_all is 0 (the common usage): |
---|
| 2479 | # If a match is found (a defined value), returns whatever the map returns, |
---|
| 2480 | # otherwise returns undef. FIRST match aborts further search sequence. |
---|
| 2481 | # when $get_all is true: |
---|
| 2482 | # Collects a list of results from ALL matching tables, and within each |
---|
| 2483 | # table from ALL matching key. Returns a ref to the list of results |
---|
| 2484 | # (and a ref to a list of matching keys if returning a pair). |
---|
| 2485 | # The first element of both lists is supposed to be what lookup() would |
---|
| 2486 | # have returned if $get_all were 0. The order of returned elements |
---|
| 2487 | # corresponds to the order of the search. |
---|
| 2488 | # |
---|
| 2489 | sub lookup($$@) { |
---|
| 2490 | my($get_all, $addr, @tables) = @_; |
---|
| 2491 | my($label, @result,@matchingkey); |
---|
| 2492 | for my $tb (@tables) { |
---|
| 2493 | my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection |
---|
| 2494 | if (!ref($t) || ref($t) eq 'SCALAR') { # a scalar always matches |
---|
| 2495 | my($r) = ref($t) ? $$t : $t; # allow direct or indirect reference |
---|
| 2496 | if (defined $r) { |
---|
| 2497 | do_log(5,"lookup: (scalar) matches, result=\"$r\""); |
---|
| 2498 | push(@result,$r); push(@matchingkey,"(constant:$r)"); |
---|
| 2499 | } |
---|
| 2500 | } elsif (ref($t) eq 'HASH') { |
---|
| 2501 | my($r,$mk) = lookup_hash($addr,$t,$get_all); |
---|
| 2502 | if (!defined $r) {} |
---|
| 2503 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
| 2504 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
| 2505 | } elsif (ref($t) eq 'ARRAY') { |
---|
| 2506 | my($r,$mk) = lookup_acl($addr,$t); |
---|
| 2507 | if (defined $r) { push(@result,$r); push(@matchingkey,$mk) } |
---|
| 2508 | } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label |
---|
| 2509 | # just a convenience for logging purposes, not a real lookup method |
---|
| 2510 | $label = $t->display; # grab the name, and proceed with the next table |
---|
| 2511 | } elsif ($t->isa('Amavis::Lookup::RE')) { |
---|
| 2512 | my($r,$mk) = $t->lookup_re($addr,$get_all); |
---|
| 2513 | if (!defined $r) {} |
---|
| 2514 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
| 2515 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
| 2516 | } elsif ($t->isa('Amavis::Lookup::SQL')) { |
---|
| 2517 | my($r,$mk) = $t->lookup_sql($addr,$get_all); |
---|
| 2518 | if (!defined $r) {} |
---|
| 2519 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
| 2520 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
| 2521 | } elsif ($t->isa('Amavis::Lookup::SQLfield')) { |
---|
| 2522 | my($r,$mk) = $t->lookup_sql_field($addr,$get_all); |
---|
| 2523 | if (!defined $r) {} |
---|
| 2524 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
| 2525 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
| 2526 | } elsif ($t->isa('Amavis::Lookup::LDAP')) { |
---|
| 2527 | my($r,$mk) = $t->lookup_ldap($addr,$get_all); |
---|
| 2528 | if (!defined $r) {} |
---|
| 2529 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
| 2530 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
| 2531 | } elsif ($t->isa('Amavis::Lookup::LDAPattr')) { |
---|
| 2532 | my($r,$mk) = $t->lookup_ldap_attr($addr,$get_all); |
---|
| 2533 | if (!defined $r) {} |
---|
| 2534 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
| 2535 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
| 2536 | } else { |
---|
| 2537 | die "TROUBLE: lookup table is an unknown object: " . ref($t); |
---|
| 2538 | } |
---|
| 2539 | last if @result && !$get_all; |
---|
| 2540 | } |
---|
| 2541 | # pretty logging |
---|
| 2542 | if ($label ne '') { $label = " ($label)" } |
---|
| 2543 | if (!ll(4)) { |
---|
| 2544 | # don't bother preparing log report which will not be printed |
---|
| 2545 | } elsif (!@tables) { |
---|
| 2546 | do_log(4,"lookup$label => undef, \"$addr\", no lookup tables"); |
---|
| 2547 | } elsif (!@result) { |
---|
| 2548 | do_log(4,"lookup$label => undef, \"$addr\" does not match"); |
---|
| 2549 | } elsif (!$get_all) { # first match wins |
---|
| 2550 | do_log(4,sprintf( |
---|
| 2551 | 'lookup%s => %-6s "%s" matches, result=%s, matching_key="%s"', |
---|
| 2552 | $label, $result[0]?'true,':'false,', $addr, |
---|
| 2553 | (ref $result[0] ne 'ARRAY' ? '"'.$result[0].'"' |
---|
| 2554 | : '('.join(',',@{$result[0]}).')'), |
---|
| 2555 | $matchingkey[0])); |
---|
| 2556 | } else { # want all matches |
---|
| 2557 | do_log(4,sprintf('lookup%s, %d matches for "%s", results: %s', |
---|
| 2558 | $label, scalar(@result), $addr, |
---|
| 2559 | join(', ', map {sprintf('"%s"=>%s', $matchingkey[$_], |
---|
| 2560 | (ref $result[$_] ne 'ARRAY' |
---|
| 2561 | ? '"'.$result[$_].'"' |
---|
| 2562 | : '('.join(',',@{$result[$_]}).')') )} |
---|
| 2563 | (0..$#result) ))); |
---|
| 2564 | } |
---|
| 2565 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
| 2566 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
| 2567 | } |
---|
| 2568 | |
---|
| 2569 | # ip_to_vec() takes IPv6 or IPv4 IP address with optional prefix length |
---|
| 2570 | # (or IPv4 mask), parses and validates it, and returns it as a 128-bit |
---|
| 2571 | # vector string that can be used as operand to Perl bitwise string operators. |
---|
| 2572 | # Syntax and other errors in the argument throw exception (die). |
---|
| 2573 | # If the second argument $allow_mask is 0, the prefix length or mask |
---|
| 2574 | # specification is not allowed as part of the IP address. |
---|
| 2575 | # |
---|
| 2576 | # The IPv6 syntax parsing and validation adheres to rfc3513. |
---|
| 2577 | # All the following IPv6 address forms are supported: |
---|
| 2578 | # x:x:x:x:x:x:x:x preferred form |
---|
| 2579 | # x:x:x:x:x:x:d.d.d.d alternative form |
---|
| 2580 | # ...::... zero-compressed form |
---|
| 2581 | # addr/prefix-length prefix length may be specified (defaults to 128) |
---|
| 2582 | # Optionally an "IPv6:" prefix may be prepended to the IPv6 address |
---|
| 2583 | # as specified by rfc2821. No brackets are allowed enclosing the address. |
---|
| 2584 | # |
---|
| 2585 | # The following IPv4 forms are allowed: |
---|
| 2586 | # d.d.d.d |
---|
| 2587 | # d.d.d.d/prefix-length CIDR mask length is allowed (defaults to 32) |
---|
| 2588 | # d.d.d.d/m.m.m.m network mask (gets converted to prefix-length) |
---|
| 2589 | # If prefix-length or a mask is specified with an IPv4 address, the address |
---|
| 2590 | # may be shortened to d.d.d/n or d.d/n or d/n. Such truncation is allowed |
---|
| 2591 | # for compatibility with earlier version, but is deprecated and is not |
---|
| 2592 | # allowed for IPv6 addresses. |
---|
| 2593 | # |
---|
| 2594 | # IPv4 addresses and masks are converted to IPv4-mapped IPv6 addresses |
---|
| 2595 | # of the form ::FFFF:d.d.d.d, The CIDR mask length (0..32) is converted |
---|
| 2596 | # to IPv6 prefix-length (96..128). The returned vector strings resulting |
---|
| 2597 | # from IPv4 and IPv6 forms are indistinguishable. |
---|
| 2598 | # |
---|
| 2599 | # NOTE: |
---|
| 2600 | # d.d.d.d is equivalent to ::FFFF:d.d.d.d (IPv4-mapped IPv6 address) |
---|
| 2601 | # which is not the same as ::d.d.d.d (IPv4-compatible IPv6 address) |
---|
| 2602 | # |
---|
| 2603 | # A triple is returned: |
---|
| 2604 | # - IP address represented as a 128-bit vector (a string) |
---|
| 2605 | # - network mask derived from prefix length, a 128-bit vector (string) |
---|
| 2606 | # - prefix length as an integer (0..128) |
---|
| 2607 | # |
---|
| 2608 | sub ip_to_vec($;$) { |
---|
| 2609 | my($ip,$allow_mask) = @_; |
---|
| 2610 | my($ip_len); my(@ip_fields); |
---|
| 2611 | local($1,$2,$3,$4,$5,$6); |
---|
| 2612 | $ip =~ s/^[ \t]+//; $ip =~ s/[ \t\n]+\z//s; # trim |
---|
| 2613 | my($ipa) = $ip; |
---|
| 2614 | ($ipa,$ip_len) = ($1,$2) if $allow_mask && $ip =~ m{^([^/]*)/(.*)\z}s; |
---|
| 2615 | if ($ipa =~ m{^(IPv6:)?(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z}si){ |
---|
| 2616 | # IPv6 alternative form x:x:x:x:x:x:d.d.d.d |
---|
| 2617 | !grep {$_ > 255} ($3,$4,$5,$6) |
---|
| 2618 | or die "Invalid decimal field value in IPv6 address: $ip"; |
---|
| 2619 | $ipa = $2 . sprintf("%02X%02X:%02X%02X", $3,$4,$5,$6); |
---|
| 2620 | } elsif ($ipa =~ m{^\d{1,3}(?:\.\d{1,3}){0,3}\z}) { # IPv4 form |
---|
| 2621 | my(@d) = split(/\./,$ipa,-1); |
---|
| 2622 | !grep {$_ > 255} @d or die "Invalid field value in IPv4 address: $ip"; |
---|
| 2623 | defined($ip_len) || @d==4 |
---|
| 2624 | or die "IPv4 address $ip contains fewer than 4 fields"; |
---|
| 2625 | $ipa = '::FFFF:' . sprintf("%02X%02X:%02X%02X", @d); # IPv4-mapped IPv6 |
---|
| 2626 | if (!defined($ip_len)) { $ip_len = 32; # no length, defaults to /32 |
---|
| 2627 | } elsif ($ip_len =~ /^\d{1,9}\z/) { # /n, IPv4 CIDR notation |
---|
| 2628 | } elsif ($ip_len =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) { |
---|
| 2629 | !grep {$_ > 255} ($1,$2,$3,$4) |
---|
| 2630 | or die "Illegal field value in IPv4 mask: $ip"; |
---|
| 2631 | my($mask1) = pack('C4',$1,$2,$3,$4); # /m.m.m.m |
---|
| 2632 | my($len) = unpack("%b*",$mask1); # count ones |
---|
| 2633 | my($mask2) = pack('B32', '1' x $len); # reconstruct mask from count |
---|
| 2634 | $mask1 eq $mask2 |
---|
| 2635 | or die "IPv4 mask not representing valid CIDR mask: $ip"; |
---|
| 2636 | $ip_len = $len; |
---|
| 2637 | } else { |
---|
| 2638 | die "Invalid IPv4 network mask or CIDR prefix length: $ip"; |
---|
| 2639 | } |
---|
| 2640 | $ip_len<=32 or die "IPv4 network prefix length greater than 32: $ip"; |
---|
| 2641 | $ip_len += 128-32; # convert IPv4 net mask length to IPv6 prefix length |
---|
| 2642 | } |
---|
| 2643 | $ip_len = 128 if !defined($ip_len); |
---|
| 2644 | $ip_len<=128 or die "IPv6 network prefix length greater than 128: $ip"; |
---|
| 2645 | $ipa =~ s/^IPv6://i; |
---|
| 2646 | # now we presumably have an IPv6 preferred form x:x:x:x:x:x:x:x |
---|
| 2647 | if ($ipa !~ /^(.*?)::(.*)\z/s) { # zero-compressing form used? |
---|
| 2648 | @ip_fields = split(/:/,$ipa,-1); # no |
---|
| 2649 | } else { # expand zero-compressing form |
---|
| 2650 | my(@a) = split(/:/,$1,-1); my(@b) = split(/:/,$2,-1); |
---|
| 2651 | my($missing_cnt) = 8-(@a+@b); $missing_cnt = 1 if $missing_cnt<1; |
---|
| 2652 | @ip_fields = (@a, (0) x $missing_cnt, @b); |
---|
| 2653 | } |
---|
| 2654 | !grep { !/^[0-9a-zA-Z]{1,4}\z/ } @ip_fields |
---|
| 2655 | or die "Invalid syntax of IPv6 address: $ip"; |
---|
| 2656 | @ip_fields<8 and die "IPv6 address $ip contains fewer than 8 fields"; |
---|
| 2657 | @ip_fields>8 and die "IPv6 address $ip contains more than 8 fields"; |
---|
| 2658 | my($vec) = pack("n8", map {hex} @ip_fields); |
---|
| 2659 | $ip_len=~/^\d{1,3}\z/ |
---|
| 2660 | or die "Invalid prefix length syntax in IP address: $ip"; |
---|
| 2661 | $ip_len<=128 or die "Invalid prefix length in IPv6 address: $ip"; |
---|
| 2662 | my($mask) = pack('B128', '1' x $ip_len); |
---|
| 2663 | # do_log(5,sprintf("ip_to_vec: %s => %s/%d\n", $ip,unpack("B*",$vec),$ip_len)); |
---|
| 2664 | ($vec,$mask,$ip_len); |
---|
| 2665 | } |
---|
| 2666 | |
---|
| 2667 | # lookup_ip_acl() performs a lookup for an IPv4 or IPv6 address |
---|
| 2668 | # against access control list or a hash of network or host addresses. |
---|
| 2669 | # |
---|
| 2670 | # IP address is compared to each member of the access list in turn, |
---|
| 2671 | # the first match wins (terminates the search), and its value decides |
---|
| 2672 | # whether the result is true (yes, permit, pass) or false (no, deny, drop). |
---|
| 2673 | # Falling through without a match produces false (undef). |
---|
| 2674 | # |
---|
| 2675 | # The presence of character '!' prepended to the list member decides |
---|
| 2676 | # whether the result will be true (without a '!') or false (with '!') |
---|
| 2677 | # in case this list member matches and terminates the search. |
---|
| 2678 | # |
---|
| 2679 | # Because search stops at the first match, it only makes sense |
---|
| 2680 | # to place more specific patterns before the more general ones. |
---|
| 2681 | # |
---|
| 2682 | # For IPv4 a network address can be specified in classless notation |
---|
| 2683 | # n.n.n.n/k, or using a mask n.n.n.n/m.m.m.m . Missing mask implies /32, |
---|
| 2684 | # i.e. a host address. For IPv6 addresses all rfc3513 forms are allowed. |
---|
| 2685 | # See also comments at ip_to_vec(). |
---|
| 2686 | # |
---|
| 2687 | # Although not a special case, it is good to remember that '::/0' |
---|
| 2688 | # always matches any IPv4 or IPv6 address. |
---|
| 2689 | # |
---|
| 2690 | # The '0/0' is equivalent to '::FFFF:0:0/0' and matches any IPv4 address |
---|
| 2691 | # (including IPv4-mapped IPv6 addresses), but not other IPv6 addresses! |
---|
| 2692 | # |
---|
| 2693 | # Example |
---|
| 2694 | # given: @acl = qw( !192.168.1.12 172.16.3.3 !172.16.3.0/255.255.255.0 |
---|
| 2695 | # 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 |
---|
| 2696 | # !0.0.0.0/8 !:: 127.0.0.0/8 ::1 ); |
---|
| 2697 | # matches rfc1918 private address space except host 192.168.1.12 |
---|
| 2698 | # and net 172.16.3/24 (but host 172.16.3.3 within 172.16.3/24 still matches). |
---|
| 2699 | # In addition the 'unspecified' (null) IPv4 and IPv6 addresses return false, |
---|
| 2700 | # and IPv4 and IPv6 loopback addresses match and return true. |
---|
| 2701 | # |
---|
| 2702 | sub lookup_ip_acl($@) { |
---|
| 2703 | my($ip, @nets_ref) = @_; |
---|
| 2704 | my($ip_vec,$ip_mask) = ip_to_vec($ip,0); |
---|
| 2705 | my($label,$found,$fullkey,$result); |
---|
| 2706 | for my $tb (@nets_ref) { |
---|
| 2707 | my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection |
---|
| 2708 | if (!ref($t) || ref($t) eq 'SCALAR') { # a scalar always matches |
---|
| 2709 | my($r) = ref($t) ? $$t : $t; # allow direct or indirect reference |
---|
| 2710 | $result = $r; $fullkey = "(constant:$r)"; |
---|
| 2711 | $found++ if defined $result; |
---|
| 2712 | } elsif (ref($t) eq 'HASH') { |
---|
| 2713 | # match the canonical IP address: dot-quad IPv4, or preferred IPv6 form |
---|
| 2714 | my($ip_c); # IP address in the canonical preferred form: x:x:x:x:x:x:x:x |
---|
| 2715 | my($ip_dq); # IPv4 in a dotted-quad form if IPv4-mapped, or undef |
---|
| 2716 | $ip_c = join(':', map {sprintf('%04x',$_)} unpack('n8',$ip_vec)); |
---|
| 2717 | my($ipv4_vec,$ipv4_mask) = ip_to_vec('::FFFF:0:0/96',1); |
---|
| 2718 | if ( ($ip_vec & $ipv4_mask) eq ($ipv4_vec & $ipv4_mask) ) { |
---|
| 2719 | # is an IPv4-mapped IPv6 address, format it in a dot-quad form |
---|
| 2720 | $ip_dq = join('.', unpack('C4',substr($ip_vec,12,4))); # last 32 bits |
---|
| 2721 | } |
---|
| 2722 | do_log(5, "lookup_ip_acl keys: \"$ip_dq\", \"$ip_c\""); |
---|
| 2723 | if (defined $ip_dq) { # try dot-quad if applicable |
---|
| 2724 | $fullkey = $ip_dq; $result = $t->{$fullkey}; |
---|
| 2725 | $found++ if defined $result; |
---|
| 2726 | } |
---|
| 2727 | if (!$found) { # try the 'preferred IPv6 form' |
---|
| 2728 | $fullkey = $ip_c; $result = $t->{$fullkey}; |
---|
| 2729 | $found++ if defined $result; |
---|
| 2730 | } |
---|
| 2731 | } elsif (ref($t) eq 'ARRAY') { |
---|
| 2732 | for my $net (@$t) { |
---|
| 2733 | $fullkey = $net; my($key) = $fullkey; $result = 1; |
---|
| 2734 | if ($key =~ /^(!+)(.*)\z/s) { # starts with exclamation mark(s) |
---|
| 2735 | $key = $2; |
---|
| 2736 | $result = 1 - $result if (length($1) & 1); # negate if odd |
---|
| 2737 | } |
---|
| 2738 | my($acl_ip_vec, $acl_mask) = ip_to_vec($key,1); |
---|
| 2739 | $found++ if ($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask); |
---|
| 2740 | last if $found; |
---|
| 2741 | } |
---|
| 2742 | } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label |
---|
| 2743 | # just a convenience for logging purposes, not a real lookup method |
---|
| 2744 | $label = $t->display; # grab the name, and proceed with the next table |
---|
| 2745 | } else { |
---|
| 2746 | die "TROUBLE: lookup table is an unknown object: " . ref($t); |
---|
| 2747 | } |
---|
| 2748 | last if $found; |
---|
| 2749 | } |
---|
| 2750 | $fullkey = $result = undef if !$found; |
---|
| 2751 | if ($label ne '') { $label = " ($label)" } |
---|
| 2752 | ll(4) && do_log(4, "lookup_ip_acl$label: key=\"$ip\"" |
---|
| 2753 | . (!$found ? ", no match" : " matches \"$fullkey\", result=$result")); |
---|
| 2754 | !wantarray ? $result : ($result, $fullkey); |
---|
| 2755 | } |
---|
| 2756 | |
---|
| 2757 | 1; |
---|
| 2758 | |
---|
| 2759 | # |
---|
| 2760 | package Amavis::Expand; |
---|
| 2761 | use strict; |
---|
| 2762 | use re 'taint'; |
---|
| 2763 | |
---|
| 2764 | BEGIN { |
---|
| 2765 | use Exporter (); |
---|
| 2766 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 2767 | $VERSION = '2.034'; |
---|
| 2768 | @ISA = qw(Exporter); |
---|
| 2769 | %EXPORT_TAGS = (); |
---|
| 2770 | @EXPORT = (); |
---|
| 2771 | @EXPORT_OK = qw(&expand); |
---|
| 2772 | } |
---|
| 2773 | use subs @EXPORT_OK; |
---|
| 2774 | BEGIN { |
---|
| 2775 | import Amavis::Util qw(ll do_log); |
---|
| 2776 | } |
---|
| 2777 | |
---|
| 2778 | # Given a string reference and a hashref of predefined (builtin) macros, |
---|
| 2779 | # expand() performs a macro expansion and returns a ref to the resulting string |
---|
| 2780 | # |
---|
| 2781 | # This is a simple, yet fully fledged macro processor with proper lexical |
---|
| 2782 | # analysis, call stack, implied quoting levels, user supplied builtin macros, |
---|
| 2783 | # two builtin flow-control macros: selector and iterator, plus a macro #, |
---|
| 2784 | # which discards input tokens until NEWLINE (like 'dnl' in m4). |
---|
| 2785 | # Also recognized are the usual \c and \nnn forms for specifying special |
---|
| 2786 | # characters, where c can be any of: r, n, f, b, e, a, t. Lexical analysis |
---|
| 2787 | # of the input string is preformed only once, macro result values are not |
---|
| 2788 | # in danger of being lexically parsed and are treated as plain characters, |
---|
| 2789 | # loosing any special meaning they might have. No new macros can be defined |
---|
| 2790 | # by processing input string (at least in this version). |
---|
| 2791 | # |
---|
| 2792 | # Simple caller-provided macros have a single character name (usually a letter) |
---|
| 2793 | # and can evaluate to a string (possibly empty or undef), or an array of |
---|
| 2794 | # strings. It can also be a subroutine reference, in which case the subroutine |
---|
| 2795 | # will be called whenever macro value is needed. The subroutine must return |
---|
| 2796 | # a scalar: a string, or an array reference. The result will be treated as if |
---|
| 2797 | # it were specified directly. |
---|
| 2798 | # |
---|
| 2799 | # Two forms of simple macro calls are known: %x and %#x (where x is a single |
---|
| 2800 | # letter macro name, i.e. a key in a user-supplied hash): |
---|
| 2801 | # %x evaluates to the hash value associated with the name x; |
---|
| 2802 | # if the value is an array ref, the result is a single concatenated |
---|
| 2803 | # string of values separated with comma-space pairs; |
---|
| 2804 | # %#x evaluates to a number: if the macro value is a scalar, returns 0 |
---|
| 2805 | # for all-whitespace value, and 1 otherwise. If a value is an array ref, |
---|
| 2806 | # evaluates to the number of elements in the array. |
---|
| 2807 | # A simple macro is evaluated only in nonquoted context, i.e. top-level |
---|
| 2808 | # text or in the first argument of a selector (see below). A literal percent |
---|
| 2809 | # character can be produced by %% or \%. |
---|
| 2810 | # |
---|
| 2811 | # More powerful expansion is provided by two builtin macros, using syntax: |
---|
| 2812 | # [? arg1 | arg2 | ... ] a selector |
---|
| 2813 | # [ arg1 | arg2 | ... ] an iterator |
---|
| 2814 | # where [, [?, | and ] are required tokens. To take away the special meaning |
---|
| 2815 | # of these characters they can be quoted by a backslash, e.g. \[ or \\ . |
---|
| 2816 | # Arguments are arbitrary text, possibly multiline, whitespace counts. |
---|
| 2817 | # Nested macro calls are permitted, proper bracket nesting must be observed. |
---|
| 2818 | # |
---|
| 2819 | # SELECTOR lets its first argument be evaluated immediately, and implicitly |
---|
| 2820 | # protects the remaining arguments. The first argument chooses which of the |
---|
| 2821 | # remaining arguments is selected as a result value. The result is only then |
---|
| 2822 | # evaluated, remaining arguments are discarded without evaluation. The first |
---|
| 2823 | # argument is usually a number (with optional leading and trailing whitespace). |
---|
| 2824 | # If it is a non-numeric string, it is treated as 0 for all-whitespace, and |
---|
| 2825 | # as 1 otherwise. Value 0 selects the very next (second) argument, value 1 |
---|
| 2826 | # selects the one after it, etc. If the value is greater than the number |
---|
| 2827 | # of available arguments, the last one (but never the first) is selected. |
---|
| 2828 | # If there is only one (the first) alternative available but the value is |
---|
| 2829 | # greater than 0, an empty string is returned. |
---|
| 2830 | # Examples: |
---|
| 2831 | # [? 2 | zero | one | two | three ] -> two |
---|
| 2832 | # [? foo | none | any | two | three ] -> any |
---|
| 2833 | # [? 24 | 0 | one | many ] -> many |
---|
| 2834 | # [? 2 |No recipients] -> (empty string) |
---|
| 2835 | # [? %#R |No recipients|One recipient|%#R recipients] |
---|
| 2836 | # [? %q |No quarantine|Quarantined as %q] |
---|
| 2837 | # Note that a selector macro call can be considered a form of if-then-else, |
---|
| 2838 | # except that the 'then' and 'else' parts are swapped! |
---|
| 2839 | # |
---|
| 2840 | # ITERATOR in its full form takes three arguments (and ignores any extra |
---|
| 2841 | # arguments after that): |
---|
| 2842 | # [ %x | body-usually-containing-%x | separator ] |
---|
| 2843 | # All iterator's arguments are implicitly quoted, iterator performs its own |
---|
| 2844 | # substitutions (described below). The result of an iterator call is a body |
---|
| 2845 | # (the second argument) repeated as many times as there are elements in the |
---|
| 2846 | # array denoted by the first argument. In each instance of a body |
---|
| 2847 | # all occurrences of token %x in the body are replaced with each successive |
---|
| 2848 | # element of the array. Resulting body instances are then glued together |
---|
| 2849 | # with a string given as the third argument. The result is finally evaluated |
---|
| 2850 | # as any top-level text for possible further expansion. |
---|
| 2851 | # |
---|
| 2852 | # There are two simplified forms of iterator call: |
---|
| 2853 | # [ body | separator ] |
---|
| 2854 | # or [ body ] |
---|
| 2855 | # where missing separator is considered a null string, and the missing formal |
---|
| 2856 | # argument name is obtained by looking for the first token of the form %x |
---|
| 2857 | # in the body. |
---|
| 2858 | # Examples: |
---|
| 2859 | # [%V| ] a space-separated list of virus names |
---|
| 2860 | # |
---|
| 2861 | # [%V|\n] a newline-separated list of virus names |
---|
| 2862 | # |
---|
| 2863 | # [%V| |
---|
| 2864 | # ] same thing: a newline-separated list of virus names |
---|
| 2865 | # |
---|
| 2866 | # [ |
---|
| 2867 | # %V] a list of virus names, each preceeded by NL and spaces |
---|
| 2868 | # |
---|
| 2869 | # [ %R |%s --> <%R>|, ] a comma-space separated list of sender/recipient |
---|
| 2870 | # name pairs where recipient is iterated over the list |
---|
| 2871 | # of recipients. (Only the (first) token %x in the first |
---|
| 2872 | # argument is significant, other characters are ignored.) |
---|
| 2873 | # |
---|
| 2874 | # [%V|[%R|%R + %V|, ]|; ] produce all combinations of %R + %V elements |
---|
| 2875 | # |
---|
| 2876 | # A combined example: |
---|
| 2877 | # [? %#C |#|Cc: [<%C>|, ]] |
---|
| 2878 | # [? %#C ||Cc: [<%C>|, ]\n]# ... same thing |
---|
| 2879 | # evaluates to an empty string if there are no elements in the %C array, |
---|
| 2880 | # otherwise it evaluates to a line: Cc: <addr1>, <addr2>, ...\n |
---|
| 2881 | # The '#' removes input characters until and including newline after it. |
---|
| 2882 | # It can be used for clarity to allow newlines be placed in the source text |
---|
| 2883 | # but not resulting in empty lines in the expanded text. In the second example |
---|
| 2884 | # above, a backslash at the end of the line would achieve the same result, |
---|
| 2885 | # although the method is different: \NEWLINE is removed during initial lexical |
---|
| 2886 | # analysis, while # is an internal macro which, when called, actively discards |
---|
| 2887 | # tokens following it, until NEWLINE (or end of input) is encountered. |
---|
| 2888 | # Whitespace (including newlines) around the first argument %#C of selector |
---|
| 2889 | # call is ignored and can be used for clarity. |
---|
| 2890 | # |
---|
| 2891 | # These all produce the same result: |
---|
| 2892 | # To: [%T|%T|, ] |
---|
| 2893 | # To: [%T|, ] |
---|
| 2894 | # To: %T |
---|
| 2895 | # |
---|
| 2896 | # See further practical examples in the supplied notification messages; |
---|
| 2897 | # see also README.customize file. |
---|
| 2898 | # |
---|
| 2899 | # Author: Mark Martinec <Mark.Martinec@ijs.si>, 2002 |
---|
| 2900 | # |
---|
| 2901 | sub expand($$) { |
---|
| 2902 | my($str_ref) = shift; # a ref to a source string to be macro expanded; |
---|
| 2903 | my($builtins_href) = shift; # a hashref, mapping builtin macro names (single |
---|
| 2904 | # char) to macro values: strings or array refs |
---|
| 2905 | my($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) = |
---|
| 2906 | \('[', '[?', ']', '|', '#'); # lexical elements to be used as references |
---|
| 2907 | my(%lexmap); # maps string to reference in order to protect lexels |
---|
| 2908 | for (keys(%$builtins_href)) |
---|
| 2909 | { $lexmap{"%$_"} = \"%$_"; $lexmap{"%#$_"} = \"%#$_" } |
---|
| 2910 | for ($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) { $lexmap{$$_} = $_ } |
---|
| 2911 | # parse lexically |
---|
| 2912 | my(@tokens) = $$str_ref =~ /\G \# | \[\?? | [\]|] | % \#? . | \\ [^0-7] | |
---|
| 2913 | \\ [0-7]{1,3} | [^\[\]\\|%\n#]+ | [^\n]+? | \n /gcsx; |
---|
| 2914 | # replace lexical element strings with object references, |
---|
| 2915 | # unquote backslash-quoted characters and %%, and drop backslash-newlines |
---|
| 2916 | my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b", |
---|
| 2917 | e => "\e", a => "\a", t => "\t"); |
---|
| 2918 | for (@tokens) { |
---|
| 2919 | if (exists $lexmap{$_}) { $_ = $lexmap{$_} } # replace with refs |
---|
| 2920 | elsif ($_ eq "\\\n") { $_ = '' } # drop \NEWLINE |
---|
| 2921 | elsif (/^%(%)\z/) { $_ = $1 } # %% -> % |
---|
| 2922 | elsif (/^(%#?.)\z/s) { $_ = \$1 } # unknown builtins |
---|
| 2923 | elsif (/^\\([0-7]{1,3})\z/) { $_ = chr(oct($1)) } # \nnn |
---|
| 2924 | elsif (/^\\(.)\z/s) { $_ = (exists($esc{$1}) ? $esc{$1} : $1) } |
---|
| 2925 | } |
---|
| 2926 | my($level) = 0; my($quote_level) = 0; my(@macro_type, @arg); |
---|
| 2927 | my($output_str) = ''; my($whereto) = \$output_str; |
---|
| 2928 | while (@tokens) { |
---|
| 2929 | my($t) = shift(@tokens); |
---|
| 2930 | if ($t eq '') { # ignore leftovers |
---|
| 2931 | } elsif ($quote_level>0 && ref($t) && ($t == $lex_lbr || $t == $lex_lbrq)){ |
---|
| 2932 | $quote_level++; |
---|
| 2933 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t); |
---|
| 2934 | } elsif (ref($t) && $t == $lex_lbr) { # begin iterator macro call |
---|
| 2935 | $quote_level++; $level++; |
---|
| 2936 | unshift(@arg, [[]]); unshift(@macro_type, ''); $whereto = $arg[0][0]; |
---|
| 2937 | } elsif (ref($t) && $t == $lex_lbrq) { # begin selector macro call |
---|
| 2938 | $level++; unshift(@arg, [[]]); unshift(@macro_type, ''); |
---|
| 2939 | $whereto = $arg[0][0]; $macro_type[0] = 'select'; |
---|
| 2940 | } elsif ($quote_level > 1 && ref($t) && $t == $lex_rbr) { |
---|
| 2941 | $quote_level--; |
---|
| 2942 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t); |
---|
| 2943 | } elsif ($level > 0 && ref($t) && $t == $lex_sep) { # next argument |
---|
| 2944 | if ($quote_level == 0 && $macro_type[0] eq 'select' && @{$arg[0]} == 1) { |
---|
| 2945 | $quote_level++; |
---|
| 2946 | } |
---|
| 2947 | if ($quote_level == 1) { |
---|
| 2948 | unshift(@{$arg[0]}, []); $whereto = $arg[0][0]; # begin next arg |
---|
| 2949 | } else { |
---|
| 2950 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t); |
---|
| 2951 | } |
---|
| 2952 | } elsif ($quote_level > 0 && ref($t) && $t == $lex_rbr) { |
---|
| 2953 | $quote_level--; # quote level just dropped to 0, this is now a call |
---|
| 2954 | $level-- if $level > 0; |
---|
| 2955 | my(@result); |
---|
| 2956 | if ($macro_type[0] eq 'select') { |
---|
| 2957 | my($sel, @alternatives) = reverse @{$arg[0]}; # list of refs |
---|
| 2958 | $sel = !ref($sel) ? '' : join('', @$sel); # turn ref into string |
---|
| 2959 | if ($sel =~ /^\s*\z/) { $sel = 0 } |
---|
| 2960 | elsif ($sel =~ /^\s*(\d+)\s*\z/) { $sel = 0+$1 } # make numeric |
---|
| 2961 | else { $sel = 1 } |
---|
| 2962 | # provide an empty second alternative if we only have one specified |
---|
| 2963 | push(@alternatives, []) if @alternatives < 2 && $sel > 0; |
---|
| 2964 | if ($sel < 0) { $sel = 0 } |
---|
| 2965 | elsif ($sel > $#alternatives) { $sel = $#alternatives } |
---|
| 2966 | @result = @{$alternatives[$sel]}; |
---|
| 2967 | } else { # iterator |
---|
| 2968 | my($cvar_r, $sep_r, $body_r, $cvar); # give meaning to arguments |
---|
| 2969 | if (@{$arg[0]} >= 3) { ($cvar_r,$body_r,$sep_r) = reverse @{$arg[0]} } |
---|
| 2970 | else { ($body_r, $sep_r) = reverse @{$arg[0]}; $cvar_r = $body_r } |
---|
| 2971 | # find the formal argument name (iterator) |
---|
| 2972 | for (@$cvar_r) { |
---|
| 2973 | if (ref && $$_ =~ /^%(.)\z/s) { $cvar = $1; last } |
---|
| 2974 | } |
---|
| 2975 | if (exists($builtins_href->{$cvar})) { |
---|
| 2976 | my($values_r) = $builtins_href->{$cvar}; |
---|
| 2977 | while (ref($values_r) eq 'CODE') { $values_r = &$values_r } |
---|
| 2978 | $values_r = [$values_r] if !ref($values_r); |
---|
| 2979 | my($ind); |
---|
| 2980 | my($re) = qr/^%\Q$cvar\E\z/; |
---|
| 2981 | for my $val (@$values_r) { |
---|
| 2982 | push(@result, @$sep_r) if ++$ind > 1 && ref($sep_r); |
---|
| 2983 | push(@result, map { (ref && $$_ =~ /$re/) ? $val : $_ } @$body_r); |
---|
| 2984 | } |
---|
| 2985 | } |
---|
| 2986 | } |
---|
| 2987 | shift(@macro_type); # pop the call stack |
---|
| 2988 | shift(@arg); |
---|
| 2989 | $whereto = $level > 0 ? $arg[0][0] : \$output_str; |
---|
| 2990 | unshift(@tokens, @result); # active macro call, reevaluate result |
---|
| 2991 | } else { # quoted, plain string, simple macro call, or a misplaced token |
---|
| 2992 | my($s) = ''; |
---|
| 2993 | if ($quote_level > 0 || !ref($t)) { |
---|
| 2994 | $s = $t; # quoted or string |
---|
| 2995 | } elsif ($t == $lex_h) { # discard tokens to (and including) newline |
---|
| 2996 | while (@tokens) { last if shift(@tokens) eq "\n" } |
---|
| 2997 | } elsif ($$t =~ /^%\#(.)\z/s) { # provide number of elements |
---|
| 2998 | if (!exists($builtins_href->{$1})) { $s = 0 } # no such |
---|
| 2999 | else { |
---|
| 3000 | $s = $builtins_href->{$1}; |
---|
| 3001 | while (ref($s) eq 'CODE') { $s = &$s } # subroutine callback |
---|
| 3002 | # for array: number of elements; for scalar: nonwhite=1, other 0 |
---|
| 3003 | $s = ref($s) ? @$s : ($s !~ /^\s*\z/); |
---|
| 3004 | } |
---|
| 3005 | } elsif ($$t =~ /^%(.)\z/s) { # provide values of a builtin macro |
---|
| 3006 | if (!exists($builtins_href->{$1})) { $s = '' } # no such |
---|
| 3007 | else { |
---|
| 3008 | $s = $builtins_href->{$1}; |
---|
| 3009 | while (ref($s) eq 'CODE') { $s = &$s } # subroutine callback |
---|
| 3010 | $s = join(', ', @$s) if ref $s; |
---|
| 3011 | } |
---|
| 3012 | } else { $s = $$t } # misplaced token, e.g. a top level | or ] |
---|
| 3013 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $s) : ($$whereto .= $s); |
---|
| 3014 | } |
---|
| 3015 | } |
---|
| 3016 | \$output_str; |
---|
| 3017 | } |
---|
| 3018 | |
---|
| 3019 | 1; |
---|
| 3020 | |
---|
| 3021 | # |
---|
| 3022 | package Amavis::In::Connection; |
---|
| 3023 | |
---|
| 3024 | # Keeps relevant information about how we received the message: |
---|
| 3025 | # client connection information, SMTP envelope and SMTP parameters |
---|
| 3026 | |
---|
| 3027 | use strict; |
---|
| 3028 | use re 'taint'; |
---|
| 3029 | |
---|
| 3030 | BEGIN { |
---|
| 3031 | use Exporter (); |
---|
| 3032 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 3033 | $VERSION = '2.034'; |
---|
| 3034 | @ISA = qw(Exporter); |
---|
| 3035 | } |
---|
| 3036 | |
---|
| 3037 | sub new |
---|
| 3038 | { my($class) = @_; bless {}, $class } |
---|
| 3039 | sub client_ip # client IP address (immediate SMTP client, i.e. our MTA) |
---|
| 3040 | { my($self)=shift; !@_ ? $self->{client_ip} : ($self->{client_ip}=shift) } |
---|
| 3041 | sub socket_ip # IP address of our interface that received connection |
---|
| 3042 | { my($self)=shift; !@_ ? $self->{socket_ip} : ($self->{socket_ip}=shift) } |
---|
| 3043 | sub socket_port # TCP port of our interface that received connection |
---|
| 3044 | { my($self)=shift; !@_ ? $self->{socket_port}:($self->{socket_port}=shift) } |
---|
| 3045 | sub proto # TCP/UNIX |
---|
| 3046 | { my($self)=shift; !@_ ? $self->{proto} : ($self->{proto}=shift) } |
---|
| 3047 | sub smtp_proto # SMTP/ESMTP(A|S|SA)/LMTP(A|S|SA) # rfc3848, or QMQP/QMQPqq |
---|
| 3048 | { my($self)=shift; !@_ ? $self->{smtp_proto}: ($self->{smtp_proto}=shift) } |
---|
| 3049 | sub smtp_helo # (E)SMTP HELO/EHLO parameter |
---|
| 3050 | { my($self)=shift; !@_ ? $self->{smtp_helo} : ($self->{smtp_helo}=shift) } |
---|
| 3051 | |
---|
| 3052 | 1; |
---|
| 3053 | |
---|
| 3054 | # |
---|
| 3055 | package Amavis::In::Message::PerRecip; |
---|
| 3056 | |
---|
| 3057 | use strict; |
---|
| 3058 | use re 'taint'; |
---|
| 3059 | |
---|
| 3060 | BEGIN { |
---|
| 3061 | use Exporter (); |
---|
| 3062 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 3063 | $VERSION = '2.034'; |
---|
| 3064 | @ISA = qw(Exporter); |
---|
| 3065 | } |
---|
| 3066 | |
---|
| 3067 | # per-recipient data are kept in an array of n-tuples: |
---|
| 3068 | # (recipient-address, destiny, done, smtp-response-text, remote-mta, ...) |
---|
| 3069 | sub new # NOTE: this class is a list, not a hash |
---|
| 3070 | { my($class) = @_; bless [(undef) x 11], $class } |
---|
| 3071 | |
---|
| 3072 | # subs to set or access individual elements of a n-tuple by name |
---|
| 3073 | sub recip_addr # recipient envelope e-mail address |
---|
| 3074 | { my($self)=shift; !@_ ? $$self[0] : ($$self[0]=shift) } |
---|
| 3075 | sub recip_addr_modified |
---|
| 3076 | { my($self)=shift; !@_ ? $$self[1] : ($$self[1]=shift) } |
---|
| 3077 | sub recip_destiny # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS |
---|
| 3078 | { my($self)=shift; !@_ ? $$self[2] : ($$self[2]=shift) } |
---|
| 3079 | sub recip_done # false: not done, true: done (1: faked, 2: truly sent) |
---|
| 3080 | { my($self)=shift; !@_ ? $$self[3] : ($$self[3]=shift) } |
---|
| 3081 | sub recip_smtp_response # rfc2821 response (3-digit + enhanced resp + text) |
---|
| 3082 | { my($self)=shift; !@_ ? $$self[4] : ($$self[4]=shift) } |
---|
| 3083 | sub recip_remote_mta_smtp_response # smtp response as issued by remote MTA |
---|
| 3084 | { my($self)=shift; !@_ ? $$self[5] : ($$self[5]=shift) } |
---|
| 3085 | sub recip_remote_mta # remote MTA that issued the smtp response |
---|
| 3086 | { my($self)=shift; !@_ ? $$self[6] : ($$self[6]=shift) } |
---|
| 3087 | sub recip_mbxname # mailbox name or file when known ('local:' or 'bsmtp:') |
---|
| 3088 | { my($self)=shift; !@_ ? $$self[7] : ($$self[7]=shift) } |
---|
| 3089 | sub recip_whitelisted_sender # recip considers this sender whitelisted (> 0) |
---|
| 3090 | { my($self)=shift; !@_ ? $$self[8] : ($$self[8]=shift) } |
---|
| 3091 | sub recip_blacklisted_sender # recip considers this sender blacklisted |
---|
| 3092 | { my($self)=shift; !@_ ? $$self[9] : ($$self[9]=shift) } |
---|
| 3093 | sub recip_score_boost # recip adds penalty spam points to the final score |
---|
| 3094 | { my($self)=shift; !@_ ? $$self[10] : ($$self[10]=shift) } |
---|
| 3095 | |
---|
| 3096 | sub recip_final_addr { # return recip_addr_modified if set, else recip_addr |
---|
| 3097 | my($self)=shift; |
---|
| 3098 | my($newaddr) = $self->recip_addr_modified; |
---|
| 3099 | defined $newaddr ? $newaddr : $self->recip_addr; |
---|
| 3100 | } |
---|
| 3101 | |
---|
| 3102 | 1; |
---|
| 3103 | |
---|
| 3104 | # |
---|
| 3105 | package Amavis::In::Message; |
---|
| 3106 | # the main purpose of this class is to contain information |
---|
| 3107 | # about the message being processed |
---|
| 3108 | |
---|
| 3109 | use strict; |
---|
| 3110 | use re 'taint'; |
---|
| 3111 | |
---|
| 3112 | BEGIN { |
---|
| 3113 | use Exporter (); |
---|
| 3114 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 3115 | $VERSION = '2.034'; |
---|
| 3116 | @ISA = qw(Exporter); |
---|
| 3117 | } |
---|
| 3118 | |
---|
| 3119 | BEGIN { |
---|
| 3120 | import Amavis::Conf qw( :platform ); |
---|
| 3121 | import Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp); |
---|
| 3122 | import Amavis::In::Message::PerRecip; |
---|
| 3123 | } |
---|
| 3124 | |
---|
| 3125 | sub new |
---|
| 3126 | { my($class) = @_; bless {}, $class } |
---|
| 3127 | sub rx_time # Unix time (s since epoch) of message reception by amavisd |
---|
| 3128 | { my($self)=shift; !@_ ? $self->{rx_time} : ($self->{rx_time}=shift) } |
---|
| 3129 | sub client_addr # original client IP addr, obtained from XFORWARD or milter |
---|
| 3130 | { my($self)=shift; !@_ ? $self->{cli_ip} : ($self->{cli_ip}=shift) } |
---|
| 3131 | sub client_name # orig. client DNS name, obtained from XFORWARD or milter |
---|
| 3132 | { my($self)=shift; !@_ ? $self->{cli_name} : ($self->{cli_name}=shift) } |
---|
| 3133 | sub client_proto # orig. client protocol, obtained from XFORWARD or milter |
---|
| 3134 | { my($self)=shift; !@_ ? $self->{cli_proto} : ($self->{cli_proto}=shift) } |
---|
| 3135 | sub client_helo # orig. client EHLO name, obtained from XFORWARD or milter |
---|
| 3136 | { my($self)=shift; !@_ ? $self->{cli_helo} : ($self->{cli_helo}=shift) } |
---|
| 3137 | sub queue_id # MTA queue ID of message if known (Courier, milter/AM.PDP) |
---|
| 3138 | { my($self)=shift; !@_ ? $self->{queue_id} : ($self->{queue_id}=shift) } |
---|
| 3139 | sub msg_size # ESMTP SIZE value, later corrected by actual message size |
---|
| 3140 | { my($self)=shift; !@_ ? $self->{msg_size} : ($self->{msg_size}=shift) } |
---|
| 3141 | sub auth_user # ESMTP AUTH username |
---|
| 3142 | { my($self)=shift; !@_ ? $self->{auth_user} : ($self->{auth_user}=shift) } |
---|
| 3143 | sub auth_pass # ESMTP AUTH password |
---|
| 3144 | { my($self)=shift; !@_ ? $self->{auth_pass} : ($self->{auth_pass}=shift) } |
---|
| 3145 | sub auth_submitter # ESMTP MAIL command AUTH option value |
---|
| 3146 | { my($self)=shift; !@_ ? $self->{auth_subm} : ($self->{auth_subm}=shift) } |
---|
| 3147 | sub body_type # ESMTP BODY parameter value |
---|
| 3148 | { my($self)=shift; !@_ ? $self->{body_type} : ($self->{body_type}=shift) } |
---|
| 3149 | sub sender # envelope sender |
---|
| 3150 | { my($self)=shift; !@_ ? $self->{sender} : ($self->{sender}=shift) } |
---|
| 3151 | sub sender_contact # unmangled sender address or undef (e.g. believed faked) |
---|
| 3152 | { my($self)=shift; !@_ ? $self->{sender_c} : ($self->{sender_c}=shift) } |
---|
| 3153 | sub sender_source # unmangled sender address or info from the trace |
---|
| 3154 | { my($self)=shift; !@_ ? $self->{sender_src} : ($self->{sender_src}=shift) } |
---|
| 3155 | sub mime_entity # MIME::Parser entity holding the message |
---|
| 3156 | { my($self)=shift; !@_ ? $self->{mime_entity}: ($self->{mime_entity}=shift)} |
---|
| 3157 | sub parts_root # Amavis::Unpackers::Part root object |
---|
| 3158 | { my($self)=shift; !@_ ? $self->{parts_root}: ($self->{parts_root}=shift)} |
---|
| 3159 | sub mail_text # rfc2822 msg: (open) file handle, or MIME::Entity object |
---|
| 3160 | { my($self)=shift; !@_ ? $self->{mail_text} : ($self->{mail_text}=shift) } |
---|
| 3161 | sub mail_text_fn # orig. mail filename or undef, e.g. mail_tempdir/email.txt |
---|
| 3162 | { my($self)=shift; !@_ ? $self->{mail_text_fn} : ($self->{mail_text_fn}=shift) } |
---|
| 3163 | sub mail_tempdir # work directory, either $TEMPBASE or supplied by client |
---|
| 3164 | { my($self)=shift; !@_ ? $self->{mail_tempdir} : ($self->{mail_tempdir}=shift) } |
---|
| 3165 | sub header_edits # Amavis::Out::EditHeader object or undef |
---|
| 3166 | { my($self)=shift; !@_ ? $self->{hdr_edits} : ($self->{hdr_edits}=shift) } |
---|
| 3167 | sub orig_header # original header - an arrayref of lines, with trailing LF |
---|
| 3168 | { my($self)=shift; !@_ ? $self->{orig_header}: ($self->{orig_header}=shift) } |
---|
| 3169 | sub orig_header_size # size of original header |
---|
| 3170 | { my($self)=shift; !@_ ? $self->{orig_hdr_s} : ($self->{orig_hdr_s}=shift) } |
---|
| 3171 | sub orig_body_size # size of original body |
---|
| 3172 | { my($self)=shift; !@_ ? $self->{orig_bdy_s} : ($self->{orig_bdy_s}=shift) } |
---|
| 3173 | sub body_digest # message digest of message body |
---|
| 3174 | { my($self)=shift; !@_ ? $self->{body_digest}: ($self->{body_digest}=shift) } |
---|
| 3175 | sub quarantined_to # list of quarantine mailbox names or addresses if quarantined |
---|
| 3176 | { my($self)=shift; !@_ ? $self->{quarantine} : ($self->{quarantine}=shift) } |
---|
| 3177 | sub dsn_sent # delivery status notification was sent(1) or faked(2) |
---|
| 3178 | { my($self)=shift; !@_ ? $self->{dsn_sent} : ($self->{dsn_sent}=shift) } |
---|
| 3179 | sub delivery_method # delivery method, or empty for implicit delivery (milter) |
---|
| 3180 | { my($self)=shift; !@_ ? $self->{delivery_method} : ($self->{delivery_method}=shift) } |
---|
| 3181 | sub client_delete # don't delete the tempdir, it is a client's reponsibility |
---|
| 3182 | { my($self)=shift; !@_ ? $self->{client_delete} : ($self->{client_delete}=shift) } |
---|
| 3183 | |
---|
| 3184 | # The order of entries in the list is the original order in which |
---|
| 3185 | # recipient addresses (e.g. obtained via 'MAIL TO:') were received. |
---|
| 3186 | # Only the entries that were accepted (via SMTP response code 2xx) |
---|
| 3187 | # are placed in the list. The ORDER MUST BE PRESERVED and no recipients |
---|
| 3188 | # may be added or removed from the list! This is vital in order to be able |
---|
| 3189 | # to produce correct per-recipient responses to a LMTP client! |
---|
| 3190 | # 'destiny' values match the meaning of 'final_*_destiny' |
---|
| 3191 | |
---|
| 3192 | sub per_recip_data { # get or set a listref of envelope recipient n-tuples |
---|
| 3193 | my($self) = shift; |
---|
| 3194 | # store a given listref of n-tuples (originals, not copies!) |
---|
| 3195 | if (@_) { @{$self->{recips}} = @{$_[0]} } |
---|
| 3196 | # return a listref to the original n-tuples, |
---|
| 3197 | # caller may modify the data if he knows what he is doing |
---|
| 3198 | $self->{recips}; |
---|
| 3199 | } |
---|
| 3200 | |
---|
| 3201 | sub recips { # get or set a listref of envelope recipients |
---|
| 3202 | my($self)=shift; |
---|
| 3203 | if (@_) { # store a copy of a given listref of recipient addresses |
---|
| 3204 | # wrap scalars (strings) into n-tuples |
---|
| 3205 | $self->per_recip_data([ map { |
---|
| 3206 | my($per_recip_obj) = Amavis::In::Message::PerRecip->new; |
---|
| 3207 | $per_recip_obj->recip_addr($_); |
---|
| 3208 | $per_recip_obj->recip_destiny(D_PASS); # default is Pass |
---|
| 3209 | $per_recip_obj } @{$_[0]} ]); |
---|
| 3210 | } |
---|
| 3211 | return if !defined wantarray; # don't bother |
---|
| 3212 | # return listref of recipient addresses |
---|
| 3213 | [ map { $_->recip_addr } @{$self->per_recip_data} ]; |
---|
| 3214 | } |
---|
| 3215 | |
---|
| 3216 | 1; |
---|
| 3217 | |
---|
| 3218 | # |
---|
| 3219 | package Amavis::Out::EditHeader; |
---|
| 3220 | |
---|
| 3221 | # Accumulates instructions on what lines need to be added to the message |
---|
| 3222 | # header, deleted, or how to change existing lines, then via a call |
---|
| 3223 | # to write_header() performs these edits on the fly. |
---|
| 3224 | |
---|
| 3225 | use strict; |
---|
| 3226 | use re 'taint'; |
---|
| 3227 | |
---|
| 3228 | BEGIN { |
---|
| 3229 | use Exporter (); |
---|
| 3230 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 3231 | $VERSION = '2.034'; |
---|
| 3232 | @ISA = qw(Exporter); |
---|
| 3233 | @EXPORT_OK = qw(&hdr); |
---|
| 3234 | } |
---|
| 3235 | |
---|
| 3236 | BEGIN { |
---|
| 3237 | import Amavis::Conf qw(:platform c cr ca); |
---|
| 3238 | import Amavis::Timing qw(section_time); |
---|
| 3239 | import Amavis::Util qw(ll do_log safe_encode q_encode); |
---|
| 3240 | } |
---|
| 3241 | use MIME::Words; |
---|
| 3242 | |
---|
| 3243 | sub new { my($class) = @_; bless {}, $class } |
---|
| 3244 | |
---|
| 3245 | sub prepend_header($$$;$) { |
---|
| 3246 | my($self, $field_name, $field_body, $structured) = @_; |
---|
| 3247 | unshift(@{$self->{prepend}}, hdr($field_name, $field_body, $structured)); |
---|
| 3248 | } |
---|
| 3249 | |
---|
| 3250 | sub append_header($$$;$) { |
---|
| 3251 | my($self, $field_name, $field_body, $structured) = @_; |
---|
| 3252 | push(@{$self->{append}}, hdr($field_name, $field_body, $structured)); |
---|
| 3253 | } |
---|
| 3254 | |
---|
| 3255 | sub delete_header($$) { |
---|
| 3256 | my($self, $field_name) = @_; |
---|
| 3257 | $self->{edit}{lc($field_name)} = undef; |
---|
| 3258 | } |
---|
| 3259 | |
---|
| 3260 | sub edit_header($$$;$) { |
---|
| 3261 | my($self, $field_name, $field_edit_sub, $structured) = @_; |
---|
| 3262 | # $field_edit_sub will be called with 2 args: field name and field body; |
---|
| 3263 | # it should return the replacement field body (no field name and colon), |
---|
| 3264 | # with or without the trailing NL |
---|
| 3265 | !defined($field_edit_sub) || ref($field_edit_sub) eq 'CODE' |
---|
| 3266 | or die "edit_header: arg#3 must be undef or a subroutine ref"; |
---|
| 3267 | $self->{edit}{lc($field_name)} = $field_edit_sub; |
---|
| 3268 | } |
---|
| 3269 | |
---|
| 3270 | # copy all header edits from another header-edits object into this one |
---|
| 3271 | sub inherit_header_edits($$) { |
---|
| 3272 | my($self, $other_edits) = @_; |
---|
| 3273 | if (defined $other_edits) { |
---|
| 3274 | unshift(@{$self->{prepend}}, |
---|
| 3275 | @{$other_edits->{prepend}}) if $other_edits->{prepend}; |
---|
| 3276 | unshift(@{$self->{append}}, |
---|
| 3277 | @{$other_edits->{append}}) if $other_edits->{append}; |
---|
| 3278 | if ($other_edits->{edit}) { |
---|
| 3279 | for (keys %{$other_edits->{edit}}) |
---|
| 3280 | { $self->{edit}{$_} = $other_edits->{edit}{$_} } |
---|
| 3281 | } |
---|
| 3282 | } |
---|
| 3283 | } |
---|
| 3284 | |
---|
| 3285 | # Insert space after colon if not present, RFC2047-encode if field body |
---|
| 3286 | # contains non-ASCII characters, fold long lines if needed, |
---|
| 3287 | # prepend space before each NL if missing, append NL if missing; |
---|
| 3288 | # Header fields with only spaces are not allowed. |
---|
| 3289 | # (rfc2822: Each line of characters MUST be no more than 998 characters, |
---|
| 3290 | # and SHOULD be no more than 78 characters, excluding the CRLF. |
---|
| 3291 | # '$structured' indicates that folding is only allowed at positions |
---|
| 3292 | # indicated by \n in the provided header body. |
---|
| 3293 | # |
---|
| 3294 | sub hdr($$;$) { |
---|
| 3295 | my($field_name, $field_body, $structured) = @_; |
---|
| 3296 | if ($field_name =~ /^(X-.*|Subject|Comments)\z/si && |
---|
| 3297 | $field_body =~ /[^\011\012\040-\176]/ #any nonprintable except TAB and LF |
---|
| 3298 | ) { # encode according to RFC 2047 |
---|
| 3299 | $field_body =~ s/\n([ \t])/$1/g; # unfold |
---|
| 3300 | chomp($field_body); |
---|
| 3301 | my($field_body_octets) = safe_encode(c('hdr_encoding'), $field_body); |
---|
| 3302 | my($qb) = c('hdr_encoding_qb'); |
---|
| 3303 | if (uc($qb) eq 'Q') { |
---|
| 3304 | $field_body = q_encode($field_body_octets, $qb, c('hdr_encoding')); |
---|
| 3305 | } else { |
---|
| 3306 | $field_body = MIME::Words::encode_mimeword($field_body_octets, |
---|
| 3307 | $qb, c('hdr_encoding')); |
---|
| 3308 | } |
---|
| 3309 | } else { # supposed to be in plain ASCII, let's make sure it is |
---|
| 3310 | $field_body = safe_encode('ascii', $field_body); |
---|
| 3311 | } |
---|
| 3312 | $field_name = safe_encode('ascii', $field_name); |
---|
| 3313 | my($str) = $field_name . ':'; |
---|
| 3314 | $str .= ' ' if $field_body !~ /^[ \t]/; |
---|
| 3315 | $str .= $field_body; |
---|
| 3316 | $str =~ s/\n([^ \t\n])/\n $1/g; # insert a space at line folds if missing |
---|
| 3317 | $str =~ s/\n([ \t]*\n)+/\n/g; # remove empty lines |
---|
| 3318 | chomp($str); # chop off trailing NL if present |
---|
| 3319 | if ($structured) { |
---|
| 3320 | my(@sublines) = split(/\n/, $str, -1); |
---|
| 3321 | $str = ''; my($s) = ''; my($s_l) = 0; |
---|
| 3322 | for (@sublines) { # join shorter field sections |
---|
| 3323 | if ($s !~ /^\s*\z/ && $s_l + length($_) > 78) { |
---|
| 3324 | $str .= "\n" if $str ne ''; |
---|
| 3325 | $str .= $s; $s = ''; $s_l = 0; |
---|
| 3326 | } |
---|
| 3327 | $s .= $_; $s_l += length($_); |
---|
| 3328 | } |
---|
| 3329 | if ($s !~ /^\s*\z/) { |
---|
| 3330 | $str .= "\n" if $str ne ''; |
---|
| 3331 | $str .= $s; |
---|
| 3332 | } |
---|
| 3333 | } elsif (length($str) > 998) { |
---|
| 3334 | # truncate the damn thing (to be done better) |
---|
| 3335 | $str = substr($str,0,998); |
---|
| 3336 | } |
---|
| 3337 | $str .= "\n"; # append final NL |
---|
| 3338 | do_log(5, "header: $str"); |
---|
| 3339 | $str; |
---|
| 3340 | } |
---|
| 3341 | |
---|
| 3342 | # Copy mail header to the supplied method (line by line) while adding, |
---|
| 3343 | # removing, or changing certain header lines as required, and append |
---|
| 3344 | # an empty line (end-of-header). Returns number of original 'Received:' |
---|
| 3345 | # header fields to make simple loop detection possible (as required |
---|
| 3346 | # by rfc2821 section 6.2). |
---|
| 3347 | # |
---|
| 3348 | # Assumes input file is properly positioned, leaves it positioned |
---|
| 3349 | # at the beginning of the body. |
---|
| 3350 | # |
---|
| 3351 | sub write_header($$$) { |
---|
| 3352 | my($self, $msg, $out_fh) = @_; |
---|
| 3353 | $out_fh = IO::Wrap::wraphandle($out_fh); # assure an IO::Handle-like obj |
---|
| 3354 | my($is_mime) = ref($msg) && $msg->isa('MIME::Entity'); |
---|
| 3355 | my(@header); |
---|
| 3356 | if ($is_mime) { |
---|
| 3357 | @header = map { /^[ \t]*\n?\z/ ? () # remove empty lines, ensure NL |
---|
| 3358 | : (/\n\z/ ? $_ : $_ . "\n") } @{$msg->header}; |
---|
| 3359 | } |
---|
| 3360 | my($received_cnt) = 0; my($str) = ''; |
---|
| 3361 | for (@{$self->{prepend}}) { $str .= $_ } |
---|
| 3362 | if ($str ne '') { $out_fh->print($str) or die "sending mail header1: $!" } |
---|
| 3363 | if (!defined($msg)) { |
---|
| 3364 | # existing header empty |
---|
| 3365 | } elsif (!exists($self->{edit}) || !scalar(%{$self->{edit}})) { |
---|
| 3366 | # no edits needed, do it the fast way |
---|
| 3367 | if ($is_mime) { |
---|
| 3368 | # NOTE: can't use method print_header, as it assumes file glob |
---|
| 3369 | for my $h (@header) |
---|
| 3370 | { $out_fh->print($h) or die "sending mail header2: $!" } |
---|
| 3371 | } else { # assume file handle |
---|
| 3372 | while (<$msg>) { # copy header only, read line by line |
---|
| 3373 | last if $_ eq $eol; # end of header |
---|
| 3374 | $out_fh->print($_) or die "sending mail header3: $!"; |
---|
| 3375 | } |
---|
| 3376 | } |
---|
| 3377 | } else { # header edits are required |
---|
| 3378 | my($curr_head, $next_head); |
---|
| 3379 | push(@header, $eol) if $is_mime; # append empty line as end-of-header |
---|
| 3380 | while (defined($next_head = $is_mime ? shift @header : <$msg>)) { |
---|
| 3381 | if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head } # folded |
---|
| 3382 | else { # new header |
---|
| 3383 | if (!defined($curr_head)) { # no previous complete header field |
---|
| 3384 | } elsif ($curr_head !~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s) { |
---|
| 3385 | # invalid header, but we don't care |
---|
| 3386 | $out_fh->print($curr_head) or die "sending mail header4: $!"; |
---|
| 3387 | } else { # count, edit, or delete |
---|
| 3388 | # obsolete rfc822 syntax allowed whitespace before colon |
---|
| 3389 | my($field_name, $field_body) = ($1, $2); |
---|
| 3390 | my($field_name_lc) = lc($field_name); |
---|
| 3391 | $received_cnt++ if $field_name_lc eq 'received'; |
---|
| 3392 | if (!exists($self->{edit}{$field_name_lc})) { # unchanged |
---|
| 3393 | $out_fh->print($curr_head) or die "sending mail header5: $!"; |
---|
| 3394 | } else { |
---|
| 3395 | my($edit) = $self->{edit}{$field_name_lc}; |
---|
| 3396 | if (defined($edit)) { # edit, not delete |
---|
| 3397 | chomp($field_body); |
---|
| 3398 | ### $field_body =~ s/\n([ \t])/$1/g; # unfold |
---|
| 3399 | $out_fh->print(hdr($field_name, &$edit($field_name,$field_body))) |
---|
| 3400 | or die "sending mail header6: $!"; |
---|
| 3401 | } |
---|
| 3402 | } |
---|
| 3403 | } |
---|
| 3404 | last if $next_head eq $eol; # end-of-header reached |
---|
| 3405 | $curr_head = $next_head; |
---|
| 3406 | } |
---|
| 3407 | } |
---|
| 3408 | } |
---|
| 3409 | $str = ''; |
---|
| 3410 | for (@{$self->{append}}) { $str .= $_ } |
---|
| 3411 | $str .= $eol; # end of header - separator line |
---|
| 3412 | $out_fh->print($str) or die "sending mail header7: $!"; |
---|
| 3413 | section_time('write-header'); |
---|
| 3414 | $received_cnt; |
---|
| 3415 | } |
---|
| 3416 | 1; |
---|
| 3417 | |
---|
| 3418 | # |
---|
| 3419 | package Amavis::Out::Local; |
---|
| 3420 | use strict; |
---|
| 3421 | use re 'taint'; |
---|
| 3422 | |
---|
| 3423 | BEGIN { |
---|
| 3424 | use Exporter (); |
---|
| 3425 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 3426 | $VERSION = '2.034'; |
---|
| 3427 | @ISA = qw(Exporter); |
---|
| 3428 | @EXPORT_OK = qw(&mail_to_local_mailbox); |
---|
| 3429 | } |
---|
| 3430 | |
---|
| 3431 | use Errno qw(ENOENT EACCES); |
---|
| 3432 | use POSIX qw(strftime); |
---|
| 3433 | use IO::File (); |
---|
| 3434 | use IO::Wrap; |
---|
| 3435 | |
---|
| 3436 | BEGIN { |
---|
| 3437 | import Amavis::Conf qw(:platform $gzip $bzip2 c cr ca); |
---|
| 3438 | import Amavis::Lock; |
---|
| 3439 | import Amavis::Util qw(ll do_log am_id exit_status_str run_command_consumer); |
---|
| 3440 | import Amavis::Timing qw(section_time); |
---|
| 3441 | import Amavis::rfc2821_2822_Tools; |
---|
| 3442 | import Amavis::Out::EditHeader; |
---|
| 3443 | } |
---|
| 3444 | |
---|
| 3445 | use subs @EXPORT_OK; |
---|
| 3446 | |
---|
| 3447 | # Deliver to local mailboxes only, ignore the rest: either to directory |
---|
| 3448 | # (maildir style), or file (Unix mbox). (normally used as a quarantine method) |
---|
| 3449 | # |
---|
| 3450 | sub mail_to_local_mailbox(@) { |
---|
| 3451 | my($via, $msginfo, $initial_submission, $filter) = @_; |
---|
| 3452 | $via =~ /^local:(.*)\z/si or die "Bad local method: $via"; |
---|
| 3453 | my($via_arg) = $1; |
---|
| 3454 | my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
| 3455 | @{$msginfo->per_recip_data}; |
---|
| 3456 | return 1 if !@per_recip_data; |
---|
| 3457 | my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object |
---|
| 3458 | if (defined($msg) && !$msg->isa('MIME::Entity')) { |
---|
| 3459 | # at this point, we have no idea what the user gave us... |
---|
| 3460 | # a globref? a FileHandle? |
---|
| 3461 | $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj |
---|
| 3462 | } |
---|
| 3463 | my($sender) = $msginfo->sender; |
---|
| 3464 | for my $r (@per_recip_data) { |
---|
| 3465 | # each one receives its own copy; these are not the original recipients |
---|
| 3466 | my($recip) = $r->recip_final_addr; |
---|
| 3467 | next if $recip eq ''; |
---|
| 3468 | my($localpart,$domain) = split_address($recip); |
---|
| 3469 | my($smtp_response); |
---|
| 3470 | |
---|
| 3471 | # %local_delivery_aliases emulates aliases map - this would otherwise |
---|
| 3472 | # be done by MTA's local delivery agent if we gave the message to MTA. |
---|
| 3473 | # This way we keep interface compatible with other mail delivery |
---|
| 3474 | # methods. The hash value may be a ref to a pair of fixed strings, |
---|
| 3475 | # or a subroutine ref (which must return such pair) to allow delayed |
---|
| 3476 | # (lazy) evaluation when some part of the pair is not yet known |
---|
| 3477 | # at initialization time. |
---|
| 3478 | # If no matching entry is found, the key ($localpart) is treated as |
---|
| 3479 | # a mailbox filename if nonempty, or else quarantining is skipped. |
---|
| 3480 | |
---|
| 3481 | my($mbxname, $suggested_filename); |
---|
| 3482 | { # a block is used as a 'switch' statement - 'last' will exit from it |
---|
| 3483 | my($ldar) = cr('local_delivery_aliases'); # a ref to a hash |
---|
| 3484 | my($alias) = $ldar->{$localpart}; |
---|
| 3485 | if (ref($alias) eq 'ARRAY') { |
---|
| 3486 | ($mbxname, $suggested_filename) = @$alias; |
---|
| 3487 | } elsif (ref($alias) eq 'CODE') { # lazy evaluation |
---|
| 3488 | ($mbxname, $suggested_filename) = &$alias; |
---|
| 3489 | } elsif ($alias ne '') { |
---|
| 3490 | ($mbxname, $suggested_filename) = ($alias, undef); |
---|
| 3491 | } elsif (!exists $ldar->{$localpart}) { |
---|
| 3492 | do_log(0, "no key '$localpart' in \%local_delivery_aliases, skip local delivery"); |
---|
| 3493 | } |
---|
| 3494 | if ($mbxname eq '') { |
---|
| 3495 | my($why) = !exists $ldar->{$localpart} ? 1 : $alias eq '' ? 2 : 3; |
---|
| 3496 | do_log(2, "skip local delivery($why): <$sender> -> <$recip>"); |
---|
| 3497 | $smtp_response = "250 2.6.0 Ok, skip local delivery($why)"; |
---|
| 3498 | last; # exit block, not the loop |
---|
| 3499 | } |
---|
| 3500 | my($ux); # is it a UNIX-style mailbox? |
---|
| 3501 | if (!-d $mbxname) { # assume a filename (need not exist yet) |
---|
| 3502 | $ux = 1; # $mbxname is a UNIX-style mailbox (one file) |
---|
| 3503 | } else { # a directory |
---|
| 3504 | $ux = 0; # $mbxname is a amavis/maildir style mailbox (a directory) |
---|
| 3505 | if ($suggested_filename eq '') |
---|
| 3506 | { $suggested_filename = $via_arg ne '' ? $via_arg : 'msg-%i-%n' } |
---|
| 3507 | $suggested_filename =~ s{%(.)} |
---|
| 3508 | { $1 eq 'b' ? $msginfo->body_digest |
---|
| 3509 | : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time)) |
---|
| 3510 | : $1 eq 'n' ? am_id() |
---|
| 3511 | : $1 eq '%' ? '%' : '%'.$1 }egs; |
---|
| 3512 | # one mail per file, will create specified file |
---|
| 3513 | $mbxname = "$mbxname/$suggested_filename"; |
---|
| 3514 | } |
---|
| 3515 | do_log(1, "local delivery: <$sender> -> <$recip>, mbx=$mbxname"); |
---|
| 3516 | my($mp,$pos,$pipe,$pid); |
---|
| 3517 | my($errn) = stat($mbxname) ? 0 : 0+$!; |
---|
| 3518 | local $SIG{CHLD} = 'DEFAULT'; |
---|
| 3519 | local $SIG{PIPE} = 'IGNORE'; # write to broken pipe would throw a signal |
---|
| 3520 | eval { # try to open the mailbox file for writing |
---|
| 3521 | if (!$ux) { # new file, traditional amavis, or maildir |
---|
| 3522 | if ($errn == ENOENT) { # good, no file, as expected |
---|
| 3523 | } elsif (!$errn && -e _) |
---|
| 3524 | { die "File $mbxname already exists, refuse to overwrite" } |
---|
| 3525 | if ($mbxname =~ /\.gz\z/) { |
---|
| 3526 | ($mp,$pid) = run_command_consumer($mbxname,undef,$gzip); |
---|
| 3527 | $pipe = 1; |
---|
| 3528 | } else { |
---|
| 3529 | $mp = IO::File->new; |
---|
| 3530 | $mp->open($mbxname,'>',0640) |
---|
| 3531 | or die "Can't create file $mbxname: $!"; |
---|
| 3532 | } |
---|
| 3533 | } else { # append to UNIX-style mailbox |
---|
| 3534 | # deliver only to non-executable regular files |
---|
| 3535 | if ($errn == ENOENT) { |
---|
| 3536 | $mp = IO::File->new; |
---|
| 3537 | $mp->open($mbxname,'>',0640) |
---|
| 3538 | or die "Can't create file $mbxname: $!"; |
---|
| 3539 | } elsif (!$errn && !-f _) { |
---|
| 3540 | die "Mailbox $mbxname is not a regular file, refuse to deliver"; |
---|
| 3541 | } elsif (-x _ || -X _) { |
---|
| 3542 | die "Mailbox file $mbxname is executable, refuse to deliver"; |
---|
| 3543 | } else { |
---|
| 3544 | $mp = IO::File->new; |
---|
| 3545 | $mp->open($mbxname,'>>',0640) or die "Can't append to $mbxname: $!"; |
---|
| 3546 | } |
---|
| 3547 | binmode($mp, ":bytes") or die "Can't cancel :utf8 mode: $!" |
---|
| 3548 | if $unicode_aware; |
---|
| 3549 | lock($mp); |
---|
| 3550 | $mp->seek(0,2) or die "Can't position mailbox file to its tail: $!"; |
---|
| 3551 | $pos = $mp->tell; |
---|
| 3552 | } |
---|
| 3553 | if (defined($msg) && !$msg->isa('MIME::Entity')) |
---|
| 3554 | { $msg->seek(0,0) or die "Can't rewind mail file: $!" } |
---|
| 3555 | }; |
---|
| 3556 | if ($@ ne '') { |
---|
| 3557 | chomp($@); |
---|
| 3558 | $smtp_response = $@ eq "timed out" ? "450 4.4.2" : "451 4.5.0"; |
---|
| 3559 | $smtp_response .= " Local delivery(1) to $mbxname failed: $@"; |
---|
| 3560 | last; # exit block, not the loop |
---|
| 3561 | } |
---|
| 3562 | eval { # if things fail from here on, try to restore mailbox state |
---|
| 3563 | if ($ux) { |
---|
| 3564 | $mp->printf("From %s %s$eol", quote_rfc2821_local($sender), |
---|
| 3565 | scalar(localtime($msginfo->rx_time)) ) |
---|
| 3566 | or die "Can't write to $mbxname: $!"; |
---|
| 3567 | } |
---|
| 3568 | my($hdr_edits) = $msginfo->header_edits; |
---|
| 3569 | if (!$hdr_edits) { |
---|
| 3570 | $hdr_edits = Amavis::Out::EditHeader->new; |
---|
| 3571 | $msginfo->header_edits($hdr_edits); |
---|
| 3572 | } |
---|
| 3573 | $hdr_edits->delete_header('Return-Path'); |
---|
| 3574 | $hdr_edits->prepend_header('Delivered-To', |
---|
| 3575 | quote_rfc2821_local($recip)); |
---|
| 3576 | $hdr_edits->prepend_header('Return-Path', |
---|
| 3577 | qquote_rfc2821_local($sender)); |
---|
| 3578 | my($received_cnt) = $hdr_edits->write_header($msg,$mp); |
---|
| 3579 | if ($received_cnt > 110) { |
---|
| 3580 | # loop detection required by rfc2821 section 6.2 |
---|
| 3581 | # Do not modify the signal text, it gets matched elsewhere! |
---|
| 3582 | die "Too many hops: $received_cnt 'Received:' header lines\n"; |
---|
| 3583 | } |
---|
| 3584 | if (!$ux) { # do it in blocks for speed if we can |
---|
| 3585 | while ($msg->read($_,16384) > 0) |
---|
| 3586 | { $mp->print($_) or die "Can't write to $mbxname: $!" } |
---|
| 3587 | } else { # for UNIX-style mailbox delivery: escape 'From ' |
---|
| 3588 | my($blank_line) = 1; |
---|
| 3589 | while (<$msg>) { |
---|
| 3590 | $mp->print('>') or die "Can't write to $mbxname: $!" |
---|
| 3591 | if $blank_line && /^From /; |
---|
| 3592 | $mp->print($_) or die "Can't write to $mbxname: $!"; |
---|
| 3593 | $blank_line = $_ eq $eol; |
---|
| 3594 | } |
---|
| 3595 | } |
---|
| 3596 | # must append an empty line for a Unix mailbox format |
---|
| 3597 | $mp->print($eol) or die "Can't write to $mbxname: $!" if $ux; |
---|
| 3598 | }; |
---|
| 3599 | my($failed) = 0; |
---|
| 3600 | if ($@ ne '') { # trouble |
---|
| 3601 | chomp($@); |
---|
| 3602 | if ($ux && defined($pos) && $can_truncate) { |
---|
| 3603 | # try to restore UNIX-style mailbox to previous size; |
---|
| 3604 | # Produces a fatal error if truncate isn't implemented |
---|
| 3605 | # on your system. |
---|
| 3606 | $mp->truncate($pos) or die "Can't truncate file $mbxname: $!"; |
---|
| 3607 | } |
---|
| 3608 | $failed = 1; |
---|
| 3609 | } |
---|
| 3610 | unlock($mp) if $ux; |
---|
| 3611 | if (!$pipe) { |
---|
| 3612 | $mp->close or die "Can't close $mbxname: $!"; |
---|
| 3613 | } else { |
---|
| 3614 | my($err); $mp->close or $err = $!; |
---|
| 3615 | $?==0 or die ("Closing pipe to $gzip: ".exit_status_str($?,$err)); |
---|
| 3616 | } |
---|
| 3617 | if (!$failed) { |
---|
| 3618 | $smtp_response = "250 2.6.0 Ok, delivered to $mbxname"; |
---|
| 3619 | } elsif ($@ eq "timed out") { |
---|
| 3620 | $smtp_response = "450 4.4.2 Local delivery to $mbxname timed out"; |
---|
| 3621 | } elsif ($@ =~ /too many hops/i) { |
---|
| 3622 | $smtp_response = "550 5.4.6 Rejected delivery to mailbox $mbxname: $@"; |
---|
| 3623 | } else { |
---|
| 3624 | $smtp_response = "451 4.5.0 Local delivery to mailbox $mbxname failed: $@"; |
---|
| 3625 | } |
---|
| 3626 | } # end of block, 'last' within block brings us here |
---|
| 3627 | do_log(-1, $smtp_response) if $smtp_response !~ /^2/; |
---|
| 3628 | $smtp_response .= ", id=" . am_id(); |
---|
| 3629 | $r->recip_smtp_response($smtp_response); $r->recip_done(2); |
---|
| 3630 | $r->recip_mbxname($mbxname) if $mbxname ne '' && $smtp_response =~ /^2/; |
---|
| 3631 | } |
---|
| 3632 | section_time('save-to-local-mailbox'); |
---|
| 3633 | } |
---|
| 3634 | |
---|
| 3635 | 1; |
---|
| 3636 | |
---|
| 3637 | # |
---|
| 3638 | package Amavis::Out; |
---|
| 3639 | use strict; |
---|
| 3640 | use re 'taint'; |
---|
| 3641 | |
---|
| 3642 | BEGIN { |
---|
| 3643 | use Exporter (); |
---|
| 3644 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 3645 | $VERSION = '2.034'; |
---|
| 3646 | @ISA = qw(Exporter); |
---|
| 3647 | %EXPORT_TAGS = (); |
---|
| 3648 | @EXPORT = qw(&mail_dispatch); |
---|
| 3649 | } |
---|
| 3650 | |
---|
| 3651 | use IO::File (); |
---|
| 3652 | use IO::Wrap; |
---|
| 3653 | use Net::Cmd; |
---|
| 3654 | use Net::SMTP 2.24; |
---|
| 3655 | # use Authen::SASL; |
---|
| 3656 | use POSIX qw(strftime |
---|
| 3657 | WIFEXITED WIFSIGNALED WIFSTOPPED |
---|
| 3658 | WEXITSTATUS WTERMSIG WSTOPSIG); |
---|
| 3659 | BEGIN { |
---|
| 3660 | import Amavis::Conf qw(:platform $DEBUG $QUARANTINEDIR |
---|
| 3661 | $relayhost_is_client c cr ca); |
---|
| 3662 | import Amavis::Util qw(untaint min max ll do_log debug_oneshot |
---|
| 3663 | am_id snmp_count retcode exit_status_str |
---|
| 3664 | prolong_timer run_command_consumer); |
---|
| 3665 | import Amavis::Timing qw(section_time); |
---|
| 3666 | import Amavis::rfc2821_2822_Tools; |
---|
| 3667 | import Amavis::Out::Local qw(mail_to_local_mailbox); |
---|
| 3668 | import Amavis::Out::EditHeader; |
---|
| 3669 | } |
---|
| 3670 | |
---|
| 3671 | # modify delivery method string if $relayhost_is_client and mail came in by TCP |
---|
| 3672 | sub dynamic_destination($$) { |
---|
| 3673 | my($method,$conn) = @_; |
---|
| 3674 | my($client_ip) = !defined($conn) ? undef : $conn->client_ip; |
---|
| 3675 | if ($client_ip ne '' && $method =~ /^smtp\b/i) { |
---|
| 3676 | my($new_method); my($relayhost,$relayhost_port,$rest); |
---|
| 3677 | (undef,$relayhost,$relayhost_port,$rest) = split(/:/,$method,4); |
---|
| 3678 | if ($relayhost_is_client) # old style |
---|
| 3679 | { ($relayhost,$relayhost_port,$rest) = ('*','*','') } |
---|
| 3680 | $relayhost = "[$client_ip]" if $relayhost eq '*'; |
---|
| 3681 | $relayhost_port = $conn->socket_port+1 if $relayhost_port eq '*'; |
---|
| 3682 | $rest = ':'.$rest if $rest ne ''; |
---|
| 3683 | $new_method = sprintf("smtp:%s:%s%s", $relayhost,$relayhost_port,$rest); |
---|
| 3684 | if ($new_method ne $method) { |
---|
| 3685 | do_log(3, "dynamic destination override: $method -> $new_method"); |
---|
| 3686 | $method = $new_method; |
---|
| 3687 | } |
---|
| 3688 | } |
---|
| 3689 | $method; |
---|
| 3690 | } |
---|
| 3691 | |
---|
| 3692 | sub mail_dispatch($$$;$) { |
---|
| 3693 | my($conn) = shift; my($msginfo,$initial_submission,$filter) = @_; |
---|
| 3694 | my($via) = $msginfo->delivery_method; |
---|
| 3695 | if ($via =~ /^smtp:/i) { |
---|
| 3696 | mail_via_smtp(dynamic_destination($via,$conn), @_); |
---|
| 3697 | } elsif ($via =~ /^pipe:/i) { |
---|
| 3698 | mail_via_pipe($via, @_); |
---|
| 3699 | } elsif ($via =~ /^bsmtp:/i) { |
---|
| 3700 | mail_via_bsmtp($via, @_); |
---|
| 3701 | } elsif ($via =~ /^local:/i) { |
---|
| 3702 | # 'local:' is used by the quarantine code to relieve it |
---|
| 3703 | # of the need to know which delivery method needs to be used. |
---|
| 3704 | # Deliver first what is local (whatever does not contain '@') |
---|
| 3705 | mail_to_local_mailbox($via, $msginfo, $initial_submission, |
---|
| 3706 | sub { shift->recip_final_addr !~ /\@/ ? 1 : 0 }); |
---|
| 3707 | if (grep { !$_->recip_done } @{$msginfo->per_recip_data}) { |
---|
| 3708 | my($nm) = c('notify_method'); # deliver the rest |
---|
| 3709 | if ($nm =~ /^smtp:/i) { mail_via_smtp(dynamic_destination($nm,$conn),@_)} |
---|
| 3710 | elsif ($nm =~ /^pipe:/i) { mail_via_pipe($nm, @_) } |
---|
| 3711 | elsif ($nm =~ /^bsmtp:/i) { mail_via_bsmtp($nm, @_) } |
---|
| 3712 | } |
---|
| 3713 | } |
---|
| 3714 | } |
---|
| 3715 | |
---|
| 3716 | #sub Net::Cmd::debug_print { |
---|
| 3717 | # my($cmd,$out,$text) = @_; |
---|
| 3718 | # do_log(0, "*** ".$cmd->debug_text($out,$text)) if $out; |
---|
| 3719 | #} |
---|
| 3720 | |
---|
| 3721 | # trivial OO wrapper around Net::SMTP::datasend |
---|
| 3722 | sub new_smtp_data { my($class, $sh) = @_; bless \$sh, $class } |
---|
| 3723 | |
---|
| 3724 | sub print { |
---|
| 3725 | my($self) = shift; |
---|
| 3726 | $$self->datasend(\@_) # datasend may be given an array ref |
---|
| 3727 | or die "datasend timed out while sending header\n"; |
---|
| 3728 | } |
---|
| 3729 | |
---|
| 3730 | # Send mail using SMTP - do multiple transactions if necessary |
---|
| 3731 | # (e.g. due to '452 Too many recipients') |
---|
| 3732 | # |
---|
| 3733 | sub mail_via_smtp(@) { |
---|
| 3734 | my($via, $msginfo, $initial_submission, $filter) = @_; |
---|
| 3735 | my($num_recips_undone) = |
---|
| 3736 | scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
| 3737 | @{$msginfo->per_recip_data}); |
---|
| 3738 | while ($num_recips_undone > 0) { |
---|
| 3739 | mail_via_smtp_single(@_); # send what we can in one transaction |
---|
| 3740 | my($num_recips_undone_after) = |
---|
| 3741 | scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
| 3742 | @{$msginfo->per_recip_data}); |
---|
| 3743 | if ($num_recips_undone_after >= $num_recips_undone) { |
---|
| 3744 | do_log(-2, "TROUBLE: Number of recipients ($num_recips_undone_after) " |
---|
| 3745 | . "not reduced in SMTP transaction, abandon the effort"); |
---|
| 3746 | last; |
---|
| 3747 | } |
---|
| 3748 | if ($num_recips_undone_after > 0) { |
---|
| 3749 | do_log(1, sprintf("Sent to %s recipients via SMTP, %s still to go", |
---|
| 3750 | $num_recips_undone - $num_recips_undone_after, |
---|
| 3751 | $num_recips_undone_after)); |
---|
| 3752 | } |
---|
| 3753 | $num_recips_undone = $num_recips_undone_after; |
---|
| 3754 | } |
---|
| 3755 | 1; |
---|
| 3756 | } |
---|
| 3757 | |
---|
| 3758 | # Send mail using SMTP - single transaction |
---|
| 3759 | # (e.g. forwarding original mail or sending notification) |
---|
| 3760 | # May throw exception (die) if temporary failure (4xx) or other problem |
---|
| 3761 | # |
---|
| 3762 | sub mail_via_smtp_single(@) { |
---|
| 3763 | my($via, $msginfo, $initial_submission, $filter) = @_; |
---|
| 3764 | my($which_section) = 'fwd_init'; |
---|
| 3765 | snmp_count('OutMsgs'); |
---|
| 3766 | local($1,$2,$3); # avoid Perl taint bug, still in 5.8.3 |
---|
| 3767 | $via =~ /^smtp: (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) /six |
---|
| 3768 | or die "Bad fwd method syntax: $via"; |
---|
| 3769 | my($relayhost, $relayhost_port) = ($1.$2, $3); |
---|
| 3770 | my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
| 3771 | @{$msginfo->per_recip_data}; |
---|
| 3772 | my($logmsg) = sprintf("%s via SMTP: [%s]:%s <%s>", |
---|
| 3773 | ($initial_submission ? 'SEND' : 'FWD'), |
---|
| 3774 | $relayhost, $relayhost_port, $msginfo->sender); |
---|
| 3775 | if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 } |
---|
| 3776 | do_log(1, $logmsg . " -> " . |
---|
| 3777 | qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data)); |
---|
| 3778 | my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object |
---|
| 3779 | my($smtp_handle, $smtp_response); my($smtp_code, $smtp_msg, $received_cnt); |
---|
| 3780 | my($any_valid_recips) = 0; my($any_tempfail_recips) = 0; |
---|
| 3781 | my($any_valid_recips_and_data_sent) = 0; my($in_datasend_mode) = 0; |
---|
| 3782 | if (defined($msg) && !$msg->isa('MIME::Entity')) { |
---|
| 3783 | # at this point, we have no idea what the user gave us... |
---|
| 3784 | # a globref? a FileHandle? |
---|
| 3785 | $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj |
---|
| 3786 | $msg->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
| 3787 | } |
---|
| 3788 | # NOTE: Net::SMTP uses alarm to do its own timing. |
---|
| 3789 | # We need to restart our timer when Net::SMTP is done using it !!! |
---|
| 3790 | my($remaining_time) = alarm(0); # check how much time is left, stop timer |
---|
| 3791 | eval { |
---|
| 3792 | $which_section = 'fwd-connect'; |
---|
| 3793 | # Timeout should be more than MTA normally takes to check DNS and RBL, |
---|
| 3794 | # which may take a minute or more in case of unreachable DNS. |
---|
| 3795 | # Specifying shorter timeout will cause alarm to terminate the wait |
---|
| 3796 | # for SMTP status line prematurely, resulting in status code 000. |
---|
| 3797 | # rfc2821 (section 4.5.3.2) requires timeout to be at least 5 minutes |
---|
| 3798 | my($localaddr) = c('local_client_bind_address'); # IP assigned to socket |
---|
| 3799 | my($heloname) = c('localhost_name'); # host name used in HELO/EHLO |
---|
| 3800 | $smtp_handle = Net::SMTP->new($relayhost, Port => $relayhost_port, |
---|
| 3801 | ($localaddr eq '' ? () : (LocalAddr => $localaddr)), |
---|
| 3802 | ($heloname eq '' ? () : (Hello => $heloname)), |
---|
| 3803 | ExactAddresses => 1, |
---|
| 3804 | Timeout => max(60, min(5 * 60, $remaining_time)), # for each operation |
---|
| 3805 | # Timeout => 0, # no timeouts, disable nonblocking mode on socket |
---|
| 3806 | # Debug => debug_oneshot(), |
---|
| 3807 | ); |
---|
| 3808 | defined($smtp_handle) |
---|
| 3809 | or die "Can't connect to $relayhost port $relayhost_port, $!"; |
---|
| 3810 | ll(5) && do_log(5,"Remote host presents itself as: ".$smtp_handle->domain); |
---|
| 3811 | |
---|
| 3812 | section_time($which_section); |
---|
| 3813 | prolong_timer($which_section, $remaining_time); # restart timer |
---|
| 3814 | $remaining_time = undef; |
---|
| 3815 | |
---|
| 3816 | $which_section = 'fwd-xforward'; |
---|
| 3817 | if ($msginfo->client_addr ne '' && $smtp_handle->supports('XFORWARD')) { |
---|
| 3818 | my($cmd) = join(' ', 'XFORWARD', map |
---|
| 3819 | { my($n,$v) = @$_; |
---|
| 3820 | # may encode value as xtext/rfc3461 in future attributes: |
---|
| 3821 | # char between "!" (33) and "~" (126) inclusive, except "+" and "=" |
---|
| 3822 | # $v =~ s/[^\041-\052\054-\074\076-\176]/sprintf("+%02X",ord($&))/eg; |
---|
| 3823 | # Wietse says not to xtext-encode these four attrs, just neuter them |
---|
| 3824 | $v =~ s/[^\041-\176]/?/g; |
---|
| 3825 | $v =~ s/[<>()\\";@]/?/g; # other chars that are special in headers |
---|
| 3826 | # postfix/smtpd/smtpd.c NEUTER_CHARACTERS (but ':' for IPv6) |
---|
| 3827 | $v = substr($v,0,255) if length($v) > 255; # see XFORWARD_README |
---|
| 3828 | $v eq '' ? () : ("$n=$v") } |
---|
| 3829 | ( ['ADDR', $msginfo->client_addr], ['NAME',$msginfo->client_name], |
---|
| 3830 | ['PROTO',$msginfo->client_proto],['HELO',$msginfo->client_helo] )); |
---|
| 3831 | do_log(5, "sending $cmd"); |
---|
| 3832 | $smtp_handle->command($cmd); |
---|
| 3833 | $smtp_handle->response()==2 or die "sending $cmd\n"; |
---|
| 3834 | section_time($which_section); prolong_timer($which_section); |
---|
| 3835 | } |
---|
| 3836 | |
---|
| 3837 | $which_section = 'fwd-auth'; |
---|
| 3838 | my($auth_user) = $msginfo->auth_user; |
---|
| 3839 | my($mechanisms) = $smtp_handle->supports('AUTH'); |
---|
| 3840 | if (!c('auth_required_out')) { |
---|
| 3841 | do_log(3,"AUTH not needed, user='$auth_user', MTA offers '$mechanisms'"); |
---|
| 3842 | } elsif ($mechanisms eq '') { |
---|
| 3843 | do_log(3,"INFO: MTA does not offer AUTH capability, user='$auth_user'"); |
---|
| 3844 | } elsif (!defined $auth_user) { |
---|
| 3845 | do_log(0,"INFO: AUTH needed for submission but AUTH data not available"); |
---|
| 3846 | } else { |
---|
| 3847 | do_log(3,"INFO: authenticating $auth_user, server supports AUTH $mechanisms"); |
---|
| 3848 | my($sasl) = Authen::SASL->new( |
---|
| 3849 | 'callback' => { 'user' => $auth_user, 'authname' => $auth_user, |
---|
| 3850 | 'pass' => $msginfo->auth_pass }); |
---|
| 3851 | $smtp_handle->auth($sasl) or die "sending AUTH, user=$auth_user\n"; |
---|
| 3852 | section_time($which_section); prolong_timer($which_section); |
---|
| 3853 | } |
---|
| 3854 | |
---|
| 3855 | $which_section = 'fwd-mail-from'; |
---|
| 3856 | # how to pass the $msginfo->auth_submitter ???!!! |
---|
| 3857 | $smtp_handle->mail(qquote_rfc2821_local($msginfo->sender)) |
---|
| 3858 | or die "sending MAIL FROM\n"; |
---|
| 3859 | section_time($which_section); prolong_timer($which_section); |
---|
| 3860 | |
---|
| 3861 | $which_section = 'fwd-rcpt-to'; |
---|
| 3862 | my($skipping_resp); |
---|
| 3863 | for my $r (@per_recip_data) { # send recipient addresses |
---|
| 3864 | if (defined $skipping_resp) { |
---|
| 3865 | $r->recip_smtp_response($skipping_resp); $r->recip_done(2); |
---|
| 3866 | next; |
---|
| 3867 | } |
---|
| 3868 | # send a RCPT TO command and get the response |
---|
| 3869 | $smtp_handle->recipient(qquote_rfc2821_local($r->recip_final_addr)); |
---|
| 3870 | $smtp_code = $smtp_handle->code; |
---|
| 3871 | $smtp_msg = $smtp_handle->message; |
---|
| 3872 | chomp($smtp_msg); |
---|
| 3873 | my($rcpt_smtp_resp) = "$smtp_code $smtp_msg"; |
---|
| 3874 | if ($smtp_code =~ /^2/) { |
---|
| 3875 | $any_valid_recips++; |
---|
| 3876 | } else { # not ok |
---|
| 3877 | do_log(3, "response to RCPT TO: \"$rcpt_smtp_resp\""); |
---|
| 3878 | if ($rcpt_smtp_resp =~ /^0/) { |
---|
| 3879 | # timeout, what to do, could cause duplicates |
---|
| 3880 | do_log(-1, "response to RCPT TO not yet available"); |
---|
| 3881 | $rcpt_smtp_resp = "450 4.4.2 ($rcpt_smtp_resp - probably timed out)"; |
---|
| 3882 | } |
---|
| 3883 | $r->recip_remote_mta($relayhost); |
---|
| 3884 | $r->recip_remote_mta_smtp_response($rcpt_smtp_resp); |
---|
| 3885 | if ($rcpt_smtp_resp =~ /^ (\d{3}) \s+ ([245] \. \d{1,3} \. \d{1,3})? |
---|
| 3886 | \s* (.*) \z/xs) |
---|
| 3887 | { |
---|
| 3888 | my($resp_code, $resp_enhcode, $resp_msg) = ($1, $2, $3); |
---|
| 3889 | if ($resp_enhcode eq '' && $resp_code =~ /^([245])/) { |
---|
| 3890 | my($c1) = $1; |
---|
| 3891 | $resp_enhcode = $resp_code eq '452' ? |
---|
| 3892 | "$c1.5.3" : "$c1.1.0"; # insert enhanced code |
---|
| 3893 | $rcpt_smtp_resp = "$resp_code $resp_enhcode $smtp_msg"; |
---|
| 3894 | } |
---|
| 3895 | } |
---|
| 3896 | if ($rcpt_smtp_resp =~ /^452/) { # too many recipients - see rfc2821 |
---|
| 3897 | do_log(-1, sprintf('Only %d recips sent in one go: "%s"', |
---|
| 3898 | $any_valid_recips, $rcpt_smtp_resp)); |
---|
| 3899 | $skipping_resp = $rcpt_smtp_resp; |
---|
| 3900 | } elsif ($rcpt_smtp_resp =~ /^4/) { |
---|
| 3901 | $any_tempfail_recips++; |
---|
| 3902 | $smtp_response = $rcpt_smtp_resp if !defined($smtp_response); |
---|
| 3903 | } |
---|
| 3904 | $r->recip_smtp_response($rcpt_smtp_resp); $r->recip_done(2); |
---|
| 3905 | $smtp_response = $rcpt_smtp_resp |
---|
| 3906 | if $rcpt_smtp_resp =~ /^5/ && $smtp_response !~ /^5/; # keep first 5x |
---|
| 3907 | } |
---|
| 3908 | } |
---|
| 3909 | section_time($which_section); prolong_timer($which_section); |
---|
| 3910 | $smtp_code = $smtp_msg = undef; |
---|
| 3911 | |
---|
| 3912 | # TODO: get the value of $dsn_per_recip_capable argument of check_mail(), |
---|
| 3913 | # or the actual protocol from In::Connection |
---|
| 3914 | my($dsn_per_recip_capable) = 0; |
---|
| 3915 | |
---|
| 3916 | if (!$any_valid_recips) { |
---|
| 3917 | do_log(-1,"mail_via_smtp: DATA skipped, no valid recips, $any_tempfail_recips"); |
---|
| 3918 | } elsif ($any_tempfail_recips && !$dsn_per_recip_capable) { |
---|
| 3919 | # we must not proceede if mail did not came in as LMTP, |
---|
| 3920 | # or we would generate mail duplicates on each delivery attempt |
---|
| 3921 | do_log(-1,"mail_via_smtp: DATA skipped, tempfailed recips: $any_tempfail_recips"); |
---|
| 3922 | } else { # send the message contents (enter DATA phase) |
---|
| 3923 | $which_section = 'fwd-data'; |
---|
| 3924 | $smtp_handle->data or die "sending DATA command\n"; |
---|
| 3925 | $in_datasend_mode = 1; |
---|
| 3926 | |
---|
| 3927 | my($smtp_resp) = $smtp_handle->code . " " . $smtp_handle->message; |
---|
| 3928 | chomp($smtp_resp); |
---|
| 3929 | do_log(5, "response to DATA: \"$smtp_resp\""); |
---|
| 3930 | |
---|
| 3931 | my($smtp_data_fh) = Amavis::Out->new_smtp_data($smtp_handle); |
---|
| 3932 | my($hdr_edits) = $msginfo->header_edits; |
---|
| 3933 | $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits; |
---|
| 3934 | $received_cnt = $hdr_edits->write_header($msg, $smtp_data_fh); |
---|
| 3935 | if ($received_cnt > 100) { |
---|
| 3936 | # loop detection required by rfc2821 6.2 |
---|
| 3937 | # Do not modify the signal text, it gets matched elsewhere! |
---|
| 3938 | die "Too many hops: $received_cnt 'Received:' header lines\n"; |
---|
| 3939 | } |
---|
| 3940 | if (!defined($msg)) { |
---|
| 3941 | # empty mail body |
---|
| 3942 | } elsif ($msg->isa('MIME::Entity')) { |
---|
| 3943 | $msg->print_body($smtp_data_fh); |
---|
| 3944 | } else { |
---|
| 3945 | # Using fixed-size reads instead of line-by-line approach |
---|
| 3946 | # makes feeding mail back to MTA (e.g. Postfix) more than |
---|
| 3947 | # twice as fast for larger mail. |
---|
| 3948 | |
---|
| 3949 | # to reduce the likelyhood of a qmail bare-LF bug (bare LF reported |
---|
| 3950 | # when CR and LF are separated by a TCP packet boundary) one may use |
---|
| 3951 | # this 'while' line, reading line by line, instead of the normal one: |
---|
| 3952 | ### while (defined($_=$msg->getline)) { |
---|
| 3953 | |
---|
| 3954 | while ($msg->read($_, 16384) > 0) { |
---|
| 3955 | $smtp_handle->datasend($_) |
---|
| 3956 | or die "datasend timed out while sending body\n"; |
---|
| 3957 | } |
---|
| 3958 | |
---|
| 3959 | } |
---|
| 3960 | section_time($which_section); prolong_timer($which_section); |
---|
| 3961 | |
---|
| 3962 | $which_section = 'fwd-data-end'; |
---|
| 3963 | # don't check status of dataend here, it may not yet be available |
---|
| 3964 | $smtp_handle->dataend; |
---|
| 3965 | $in_datasend_mode = 0; $any_valid_recips_and_data_sent = 1; |
---|
| 3966 | section_time($which_section); prolong_timer($which_section); |
---|
| 3967 | |
---|
| 3968 | $which_section = 'fwd-rundown-1'; |
---|
| 3969 | # figure out the final SMTP response |
---|
| 3970 | $smtp_code = $smtp_handle->code; |
---|
| 3971 | my(@msgs) = $smtp_handle->message; |
---|
| 3972 | # only the 'command()' resets messages list, so now we have both: |
---|
| 3973 | # 'End data with <CR><LF>.<CR><LF>' and 'Ok: queued as...' in @msgs |
---|
| 3974 | # and only the last SMTP response code in $smtp_handle->code |
---|
| 3975 | my($smtp_msg) = $msgs[$#msgs]; chomp($smtp_msg); # take the last one |
---|
| 3976 | $smtp_response = "$smtp_code $smtp_msg"; |
---|
| 3977 | do_log(5, "response to data end: \"$smtp_response\""); |
---|
| 3978 | for my $r (@per_recip_data) { |
---|
| 3979 | next if $r->recip_done; # skip those that failed at RCPT TO |
---|
| 3980 | $r->recip_remote_mta($relayhost); |
---|
| 3981 | $r->recip_remote_mta_smtp_response($smtp_response); |
---|
| 3982 | } |
---|
| 3983 | if ($smtp_code =~ /^[245]/) { |
---|
| 3984 | my($smtp_status) = substr($smtp_code, 0, 1); |
---|
| 3985 | $smtp_response = sprintf("%s %d.6.0 %s, id=%s, from MTA: %s", |
---|
| 3986 | $smtp_code, $smtp_status, ($smtp_status == 2 ? 'Ok' : 'Failed'), |
---|
| 3987 | am_id(), $smtp_response); |
---|
| 3988 | } |
---|
| 3989 | } |
---|
| 3990 | }; |
---|
| 3991 | my($err) = $@; |
---|
| 3992 | my($saved_section_name) = $which_section; |
---|
| 3993 | if ($err ne '') { chomp($err); $err = ' ' if $err eq '' } # careful chomp |
---|
| 3994 | prolong_timer($which_section, $remaining_time); # restart the timer |
---|
| 3995 | $which_section = 'fwd-rundown'; |
---|
| 3996 | if ($err ne '') { # fetch info about failure |
---|
| 3997 | do_log(3, "mail_via_smtp: session failed: $err"); |
---|
| 3998 | if (!defined($smtp_handle)) { $smtp_code = ''; $smtp_msg = '' } |
---|
| 3999 | else { |
---|
| 4000 | $smtp_code = $smtp_handle->code; $smtp_msg = $smtp_handle->message; |
---|
| 4001 | chomp($smtp_msg); |
---|
| 4002 | } |
---|
| 4003 | } |
---|
| 4004 | # terminate the SMTP session if still alive |
---|
| 4005 | if (!defined $smtp_handle) { |
---|
| 4006 | # nothing |
---|
| 4007 | } elsif ($in_datasend_mode) { |
---|
| 4008 | # We are aborting SMTP session. DATA send mode must NOT be normally |
---|
| 4009 | # terminated with a dataend (dot), otherwise recipient will receive |
---|
| 4010 | # a chopped-off mail (and possibly be receiving it over and over again |
---|
| 4011 | # during each MTA retry. |
---|
| 4012 | do_log(-1, "mail_via_smtp: NOTICE: aborting SMTP session, $err"); |
---|
| 4013 | $smtp_handle->close; # abruptly terminate the SMTP session, ignoring status |
---|
| 4014 | } else { |
---|
| 4015 | $smtp_handle->timeout(15); # don't wait too long for response to a QUIT |
---|
| 4016 | $smtp_handle->quit; # send a QUIT regardless of success so far |
---|
| 4017 | if ($err eq '' && $smtp_handle->status != CMD_OK) { |
---|
| 4018 | do_log(-1,"WARN: sending SMTP QUIT command failed: " |
---|
| 4019 | . $smtp_handle->code . " " . $smtp_handle->message); |
---|
| 4020 | } |
---|
| 4021 | } |
---|
| 4022 | # prepare final smtp response and log abnormal events |
---|
| 4023 | if ($err eq '') { # no errors |
---|
| 4024 | if ($any_valid_recips_and_data_sent && $smtp_response !~ /^[245]/) { |
---|
| 4025 | $smtp_response = |
---|
| 4026 | sprintf("451 4.6.0 Bad SMTP code, id=%s, from MTA: \"%s\"", |
---|
| 4027 | am_id(), $smtp_response); |
---|
| 4028 | } |
---|
| 4029 | } elsif ($err eq "timed out" || $err =~ /: Timeout\z/) { |
---|
| 4030 | my($msg) = ($in_datasend_mode && $smtp_code =~ /^354/) ? |
---|
| 4031 | '' : ", $smtp_code $smtp_msg"; |
---|
| 4032 | $smtp_response = sprintf("450 4.4.2 Timed out during %s%s, id=%s", |
---|
| 4033 | $saved_section_name, $msg, am_id()); |
---|
| 4034 | } elsif ($err =~ /^Can't connect/) { |
---|
| 4035 | $smtp_response = sprintf("450 4.4.1 %s, id=%s", $err, am_id()); |
---|
| 4036 | } elsif ($err =~ /^Too many hops/) { |
---|
| 4037 | $smtp_response = sprintf("550 5.4.6 Rejected: %s, id=%s", $err, am_id()); |
---|
| 4038 | } elsif ($smtp_code =~ /^5/) { # 5xx |
---|
| 4039 | $smtp_response = sprintf("%s 5.5.0 Rejected by MTA: %s %s, id=%s", |
---|
| 4040 | ($smtp_code !~ /^5\d\d\z/ ? "550" : $smtp_code), |
---|
| 4041 | $smtp_code, $smtp_msg, am_id()); |
---|
| 4042 | } elsif ($smtp_code =~ /^0/) { # 000 |
---|
| 4043 | $smtp_response = sprintf("450 4.4.2 No response during %s (%s): id=%s", |
---|
| 4044 | $saved_section_name, $err, am_id()); |
---|
| 4045 | } else { |
---|
| 4046 | $smtp_response = sprintf("%s 4.5.0 from MTA during %s (%s): %s %s, id=%s", |
---|
| 4047 | ($smtp_code !~ /^4\d\d\z/ ? "451" : $smtp_code), |
---|
| 4048 | $saved_section_name, $err, $smtp_code, $smtp_msg, |
---|
| 4049 | am_id()); |
---|
| 4050 | } |
---|
| 4051 | |
---|
| 4052 | do_log( ($smtp_response =~ /^2/ ? 3 : -1), |
---|
| 4053 | "mail_via_smtp: $smtp_response" ) if $smtp_response ne ''; |
---|
| 4054 | if (defined $smtp_response) { |
---|
| 4055 | for my $r (@per_recip_data) { |
---|
| 4056 | if (!$r->recip_done) { # mark it as done |
---|
| 4057 | $r->recip_smtp_response($smtp_response); $r->recip_done(2); |
---|
| 4058 | $r->recip_mbxname($r->recip_final_addr) if $smtp_response =~ /^2/; |
---|
| 4059 | } elsif ($any_valid_recips_and_data_sent |
---|
| 4060 | && $r->recip_smtp_response =~ /^452/) { |
---|
| 4061 | # 'undo' the RCPT TO '452 Too many recipients' situation, |
---|
| 4062 | # needs to be handled in more than one transaction |
---|
| 4063 | $r->recip_smtp_response(undef); $r->recip_done(undef); |
---|
| 4064 | } |
---|
| 4065 | } |
---|
| 4066 | } |
---|
| 4067 | if ( $smtp_response =~ /^2/) { snmp_count('OutMsgsDelivers') } |
---|
| 4068 | elsif ($smtp_response =~ /^4/) { snmp_count('OutAttemptFails') } |
---|
| 4069 | elsif ($smtp_response =~ /^5/) { snmp_count('OutMsgsRejects') } |
---|
| 4070 | section_time($which_section); |
---|
| 4071 | 1; |
---|
| 4072 | } |
---|
| 4073 | |
---|
| 4074 | # Send mail using external mail submission program 'sendmail' (also available |
---|
| 4075 | # with Postfix and Exim) - used for forwarding original mail or sending notif. |
---|
| 4076 | # May throw exception (die) if temporary failure (4xx) or other problem |
---|
| 4077 | # |
---|
| 4078 | sub mail_via_pipe(@) { |
---|
| 4079 | my($via, $msginfo, $initial_submission, $filter) = @_; |
---|
| 4080 | snmp_count('OutMsgs'); |
---|
| 4081 | $via =~ /^pipe:(.*)\z/si or die "Bad fwd method syntax: $via"; |
---|
| 4082 | my($pipe_args) = $1; |
---|
| 4083 | $pipe_args =~ s/^flags=\S*\s*//i; # flags are currently ignored, q implied |
---|
| 4084 | $pipe_args =~ s/^argv=//i; |
---|
| 4085 | my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
| 4086 | @{$msginfo->per_recip_data}; |
---|
| 4087 | my($logmsg) = sprintf("%s via PIPE: <%s>", |
---|
| 4088 | ($initial_submission ? 'SEND' : 'FWD'), $msginfo->sender); |
---|
| 4089 | if (!@per_recip_data) { |
---|
| 4090 | do_log(5, "$logmsg, nothing to do"); |
---|
| 4091 | return 1; |
---|
| 4092 | } |
---|
| 4093 | do_log(1, $logmsg . " -> " . |
---|
| 4094 | qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data)); |
---|
| 4095 | my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object |
---|
| 4096 | if (defined($msg) && !$msg->isa('MIME::Entity')) { |
---|
| 4097 | # at this point, we have no idea what the user gave us... |
---|
| 4098 | # a globref? a FileHandle? |
---|
| 4099 | $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj |
---|
| 4100 | $msg->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
| 4101 | } |
---|
| 4102 | my(@pipe_args) = split(' ', $pipe_args); my(@command) = shift @pipe_args; |
---|
| 4103 | for (@pipe_args) { |
---|
| 4104 | # The sendmail command line expects addresses quoted as per RFC 822. |
---|
| 4105 | # "funny user"@some.domain |
---|
| 4106 | # For compatibility with Sendmail, the Postfix sendmail command line |
---|
| 4107 | # also accepts address formats that are legal in RFC 822 mail headers: |
---|
| 4108 | # Funny Dude <"funny user"@some.domain> |
---|
| 4109 | # Although addresses passed as args to sendmail initial submission |
---|
| 4110 | # should not be <...> bracketed, for some reason original sendmail |
---|
| 4111 | # issues a warning on null reverse-path, but gladly accepty <>. |
---|
| 4112 | # As this is not strictly wrong, we comply to make it happy. |
---|
| 4113 | if (/^\$\{sender\}\z/i) { |
---|
| 4114 | push(@command, |
---|
| 4115 | map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) } |
---|
| 4116 | $msginfo->sender); |
---|
| 4117 | } elsif (/^\$\{recipient\}\z/i) { |
---|
| 4118 | push(@command, |
---|
| 4119 | map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) } |
---|
| 4120 | map { $_->recip_final_addr } @per_recip_data); |
---|
| 4121 | } else { |
---|
| 4122 | push(@command, $_); |
---|
| 4123 | } |
---|
| 4124 | } |
---|
| 4125 | do_log(5, "mail_via_pipe running command: " . join(' ', @command)); |
---|
| 4126 | local $SIG{CHLD} = 'DEFAULT'; |
---|
| 4127 | local $SIG{PIPE} = 'IGNORE'; # write to broken pipe would throw a signal |
---|
| 4128 | my($mp,$pid) = run_command_consumer(undef,undef,@command); |
---|
| 4129 | binmode($mp) or die "Can't set pipe to binmode: $!"; # dflt since Perl 5.8.1 |
---|
| 4130 | my($hdr_edits) = $msginfo->header_edits; |
---|
| 4131 | $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits; |
---|
| 4132 | my($received_cnt) = $hdr_edits->write_header($msg, $mp); |
---|
| 4133 | if ($received_cnt > 100) { # loop detection required by rfc2821 6.2 |
---|
| 4134 | # deal with it later, for now just skip the body |
---|
| 4135 | } elsif (!defined($msg)) { |
---|
| 4136 | # empty mail body |
---|
| 4137 | } elsif ($msg->isa('MIME::Entity')) { |
---|
| 4138 | $msg->print_body($mp); |
---|
| 4139 | } else { |
---|
| 4140 | while ($msg->read($_, 16384) > 0) |
---|
| 4141 | { $mp->print($_) or die "Submitting mail text failed: $!" } |
---|
| 4142 | } |
---|
| 4143 | my($smtp_response); |
---|
| 4144 | if ($received_cnt > 100) { # loop detection required by rfc2821 6.2 |
---|
| 4145 | do_log(-2, "Too many hops: $received_cnt 'Received:' header lines"); |
---|
| 4146 | kill('TERM',$pid); # kill the process running mail submission program |
---|
| 4147 | $mp->close; # and ignore status |
---|
| 4148 | $smtp_response = "550 5.4.6 Rejected: " . |
---|
| 4149 | "Too many hops: $received_cnt 'Received:' header lines"; |
---|
| 4150 | } else { |
---|
| 4151 | my($err); $mp->close or $err=$!; my($child_stat) = $?; |
---|
| 4152 | my($error_str) = exit_status_str($child_stat,$err); |
---|
| 4153 | my($status) = WEXITSTATUS($child_stat); |
---|
| 4154 | # sendmail program (Postfix variant) can return the following exit codes: |
---|
| 4155 | # EX_OK(0), EX_DATAERR, EX_SOFTWARE, EX_TEMPFAIL, EX_NOUSER, EX_UNAVAILABLE |
---|
| 4156 | if ($status == EX_OK) { |
---|
| 4157 | $smtp_response = "250 2.6.0 Ok"; # submitted to MTA |
---|
| 4158 | snmp_count('OutMsgsDelivers'); |
---|
| 4159 | } elsif ($status == EX_TEMPFAIL) { |
---|
| 4160 | $smtp_response = "450 4.5.0 Temporary failure submitting message"; |
---|
| 4161 | snmp_count('OutAttemptFails'); |
---|
| 4162 | } elsif ($status == EX_NOUSER) { |
---|
| 4163 | $smtp_response = "550 5.1.1 Recipient unknown"; |
---|
| 4164 | snmp_count('OutMsgsRejects'); |
---|
| 4165 | } elsif ($status == EX_UNAVAILABLE) { |
---|
| 4166 | $smtp_response = "550 5.5.0 Mail submission service unavailable"; |
---|
| 4167 | snmp_count('OutMsgsRejects'); |
---|
| 4168 | } else { |
---|
| 4169 | $smtp_response = "451 4.5.0 Failed to submit a message: $error_str"; |
---|
| 4170 | snmp_count('OutAttemptFails'); |
---|
| 4171 | } |
---|
| 4172 | } |
---|
| 4173 | $smtp_response .= ", id=" . am_id(); |
---|
| 4174 | for my $r (@per_recip_data) { |
---|
| 4175 | next if $r->recip_done; |
---|
| 4176 | $r->recip_smtp_response($smtp_response); $r->recip_done(2); |
---|
| 4177 | $r->recip_mbxname($r->recip_final_addr) if $smtp_response =~ /^2/; |
---|
| 4178 | } |
---|
| 4179 | section_time('fwd-pipe'); |
---|
| 4180 | 1; |
---|
| 4181 | } |
---|
| 4182 | |
---|
| 4183 | sub mail_via_bsmtp(@) { |
---|
| 4184 | my($via, $msginfo, $initial_submission, $filter) = @_; |
---|
| 4185 | snmp_count('OutMsgs'); local($1); |
---|
| 4186 | $via =~ /^bsmtp:(.*)\z/si or die "Bad fwd method: $via"; |
---|
| 4187 | my($bsmtp_file_final) = $1; my($mbxname); |
---|
| 4188 | $bsmtp_file_final =~ s{%(.)} |
---|
| 4189 | { $1 eq 'b' ? $msginfo->body_digest |
---|
| 4190 | : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time)) |
---|
| 4191 | : $1 eq 'n' ? am_id() |
---|
| 4192 | : $1 eq '%' ? '%' : '%'.$1 }egs; |
---|
| 4193 | # prepend directory if not specified |
---|
| 4194 | $bsmtp_file_final = $QUARANTINEDIR."/".$bsmtp_file_final |
---|
| 4195 | if $QUARANTINEDIR ne '' && $bsmtp_file_final !~ m{^/}; |
---|
| 4196 | my($bsmtp_file_tmp) = $bsmtp_file_final . ".tmp"; |
---|
| 4197 | my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
| 4198 | @{$msginfo->per_recip_data}; |
---|
| 4199 | my($logmsg) = sprintf("%s via BSMTP: %s", |
---|
| 4200 | ($initial_submission ? 'SEND' : 'FWD'), |
---|
| 4201 | qquote_rfc2821_local($msginfo->sender)); |
---|
| 4202 | if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 } |
---|
| 4203 | do_log(1, $logmsg . " -> " . |
---|
| 4204 | qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data) . |
---|
| 4205 | ", file " . $bsmtp_file_final); |
---|
| 4206 | my($msg) = $msginfo->mail_text; # a scalar reference, or a file handle |
---|
| 4207 | if (defined($msg) && !$msg->isa('MIME::Entity')) { |
---|
| 4208 | # at this point, we have no idea what the user gave us... |
---|
| 4209 | # a globref? a FileHandle? |
---|
| 4210 | $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj |
---|
| 4211 | $msg->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
| 4212 | } |
---|
| 4213 | my($mp) = IO::File->new; |
---|
| 4214 | eval { |
---|
| 4215 | $mp->open($bsmtp_file_tmp,'>',0640) |
---|
| 4216 | or die "Can't create BSMTP file $bsmtp_file_tmp: $!"; |
---|
| 4217 | binmode($mp, ":bytes") or die "Can't set :bytes, $!" if $unicode_aware; |
---|
| 4218 | $mp->print("EHLO ", c('localhost_name'), $eol) |
---|
| 4219 | or die "print failed (EHLO): $!"; |
---|
| 4220 | $mp->printf("MAIL FROM:%s BODY=8BITMIME%s", # avoid conversion to 7bit |
---|
| 4221 | qquote_rfc2821_local($msginfo->sender), $eol) |
---|
| 4222 | or die "print failed (MAIL FROM): $!"; |
---|
| 4223 | for my $r (@per_recip_data) { |
---|
| 4224 | $mp->print("RCPT TO:", qquote_rfc2821_local($r->recip_final_addr), $eol) |
---|
| 4225 | or die "print failed (RCPT TO): $!"; |
---|
| 4226 | } |
---|
| 4227 | $mp->print("DATA", $eol) or die "print failed (DATA): $!"; |
---|
| 4228 | my($hdr_edits) = $msginfo->header_edits; |
---|
| 4229 | $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits; |
---|
| 4230 | my($received_cnt) = $hdr_edits->write_header($msg,$mp); |
---|
| 4231 | if ($received_cnt > 100) { # loop detection required by rfc2821 6.2 |
---|
| 4232 | die "Too many hops: $received_cnt 'Received:' header lines"; |
---|
| 4233 | } elsif (!defined($msg)) { # empty mail body |
---|
| 4234 | } elsif ($msg->isa('MIME::Entity')) { |
---|
| 4235 | $msg->print_body($mp); |
---|
| 4236 | } else { |
---|
| 4237 | while (<$msg>) { |
---|
| 4238 | $mp->print(/^\./ ? (".",$_) : $_) or die "print failed-data: $!"; |
---|
| 4239 | } |
---|
| 4240 | } |
---|
| 4241 | $mp->print(".", $eol) or die "print failed (final dot): $!"; |
---|
| 4242 | # $mp->print("QUIT",$eol) or die "print failed (QUIT): $!"; |
---|
| 4243 | $mp->close or die "Can't close BSMTP file $bsmtp_file_tmp: $!"; |
---|
| 4244 | $mp = undef; |
---|
| 4245 | rename($bsmtp_file_tmp, $bsmtp_file_final) |
---|
| 4246 | or die "Can't rename BSMTP file to $bsmtp_file_final: $!"; |
---|
| 4247 | $mbxname = $bsmtp_file_final; |
---|
| 4248 | }; |
---|
| 4249 | my($err) = $@; my($smtp_response); |
---|
| 4250 | if ($err eq '') { |
---|
| 4251 | $smtp_response = "250 2.6.0 Ok, queued as BSMTP $bsmtp_file_final"; |
---|
| 4252 | snmp_count('OutMsgsDelivers'); |
---|
| 4253 | } else { |
---|
| 4254 | chomp($err); |
---|
| 4255 | unlink($bsmtp_file_tmp) |
---|
| 4256 | or do_log(-2,"Can't delete half-finished BSMTP file $bsmtp_file_tmp: $!"); |
---|
| 4257 | $mp->close if defined $mp; # ignore status |
---|
| 4258 | if ($err =~ /too many hops/i) { |
---|
| 4259 | $smtp_response = "550 5.4.6 Rejected: $err"; |
---|
| 4260 | snmp_count('OutMsgsRejects'); |
---|
| 4261 | } else { |
---|
| 4262 | $smtp_response = "451 4.5.0 Writing $bsmtp_file_tmp failed: $err"; |
---|
| 4263 | snmp_count('OutAttemptFails'); |
---|
| 4264 | } |
---|
| 4265 | } |
---|
| 4266 | $smtp_response .= ", id=" . am_id(); |
---|
| 4267 | for my $r (@per_recip_data) { |
---|
| 4268 | next if $r->recip_done; |
---|
| 4269 | $r->recip_smtp_response($smtp_response); $r->recip_done(2); |
---|
| 4270 | $r->recip_mbxname($mbxname) if $mbxname ne '' && $smtp_response =~ /^2/; |
---|
| 4271 | } |
---|
| 4272 | section_time('fwd-bsmtp'); |
---|
| 4273 | 1; |
---|
| 4274 | } |
---|
| 4275 | |
---|
| 4276 | 1; |
---|
| 4277 | |
---|
| 4278 | # |
---|
| 4279 | package Amavis::UnmangleSender; |
---|
| 4280 | use strict; |
---|
| 4281 | use re 'taint'; |
---|
| 4282 | |
---|
| 4283 | BEGIN { |
---|
| 4284 | use Exporter (); |
---|
| 4285 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 4286 | $VERSION = '2.034'; |
---|
| 4287 | @ISA = qw(Exporter); |
---|
| 4288 | %EXPORT_TAGS = (); |
---|
| 4289 | @EXPORT = (); |
---|
| 4290 | @EXPORT_OK = qw(&best_try_originator_ip &best_try_originator |
---|
| 4291 | &first_received_from); |
---|
| 4292 | } |
---|
| 4293 | use subs @EXPORT_OK; |
---|
| 4294 | |
---|
| 4295 | BEGIN { |
---|
| 4296 | import Amavis::Conf qw(:platform @viruses_that_fake_sender_maps); |
---|
| 4297 | import Amavis::Util qw(ll do_log); |
---|
| 4298 | import Amavis::rfc2821_2822_Tools qw( |
---|
| 4299 | split_address parse_received fish_out_ip_from_received); |
---|
| 4300 | import Amavis::Lookup qw(lookup lookup_ip_acl); |
---|
| 4301 | } |
---|
| 4302 | use Mail::Address; |
---|
| 4303 | |
---|
| 4304 | # Returns the envelope sender address, or reconstructs it if there is |
---|
| 4305 | # a good reason to believe the envelope address has been changed or forged, |
---|
| 4306 | # as is common for some varieties of viruses. Returns best guess of the |
---|
| 4307 | # sender address, or undef if it can not be determined. |
---|
| 4308 | # |
---|
| 4309 | sub unmangle_sender($$$) { |
---|
| 4310 | my $sender = shift; # rfc2821 envelope sender address |
---|
| 4311 | my $from = shift; # rfc2822 'From:' header, may include comment |
---|
| 4312 | my $virusname_list = shift; # list ref containing names of detected viruses |
---|
| 4313 | # based on ideas from Furio Ercolessi, Mike Atkinson, Mark Martinec |
---|
| 4314 | |
---|
| 4315 | my($best_try_originator) = $sender; |
---|
| 4316 | my($localpart,$domain) = split_address($sender); |
---|
| 4317 | # extract the RFC2822 'from' address, ignoring phrase and comment |
---|
| 4318 | chomp($from); |
---|
| 4319 | { |
---|
| 4320 | local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted ! |
---|
| 4321 | $from = (Mail::Address->parse($from))[0]; |
---|
| 4322 | } |
---|
| 4323 | $from = $from->address if $from ne ''; |
---|
| 4324 | # NOTE: rfc2822 allows multiple addresses in the From field! |
---|
| 4325 | |
---|
| 4326 | if (grep { /magistr/i } @$virusname_list) { |
---|
| 4327 | for my $j (0..2) { # assemble possible `shifted' candidates |
---|
| 4328 | next if $j >= length($localpart); |
---|
| 4329 | my($try) = $sender; |
---|
| 4330 | substr($try, $j, 1) = chr(ord(substr($try, $j, 1)) - 1); |
---|
| 4331 | if (lc($from) eq lc($try)) { $best_try_originator = $try; last } |
---|
| 4332 | } |
---|
| 4333 | } |
---|
| 4334 | # |
---|
| 4335 | # Virus names are AV-checker vendor specific, but many use same |
---|
| 4336 | # or similar virus names. This requires attention and adjustments |
---|
| 4337 | # from Amavis administrators. |
---|
| 4338 | # |
---|
| 4339 | if (grep { /badtrans/i } @$virusname_list) { |
---|
| 4340 | if ($from =~ /^ # these are fake built-in addresses |
---|
| 4341 | (joanna\@mail\.utexas\.edu | powerpuff\@videotron\.ca | |
---|
| 4342 | (mary\@c-com | support\@cyberramp | admin\@gte | |
---|
| 4343 | administrator\@border) \.net | |
---|
| 4344 | (monika\@telia | jessica\@aol | spiderroll\@hotmail | |
---|
| 4345 | lgonzal\@hotmail | andy\@hweb-media | Gravity49\@aol | |
---|
| 4346 | tina0828\@yahoo | JUJUB271\@AOL | aizzo\@home) \.com |
---|
| 4347 | ) \z/xi ) |
---|
| 4348 | { # discard recipient's address used as a fake 'MAIL FROM:' |
---|
| 4349 | $best_try_originator = undef; |
---|
| 4350 | } else { |
---|
| 4351 | $best_try_originator = $1 if $from=~/^_(.+)\z/s && lc($sender) ne lc($1); |
---|
| 4352 | } |
---|
| 4353 | } |
---|
| 4354 | for my $vn (@$virusname_list) { |
---|
| 4355 | my($result,$matching_key) = lookup(0,$vn,@viruses_that_fake_sender_maps); |
---|
| 4356 | if ($result) { |
---|
| 4357 | do_log(2, "Virus $vn matches $matching_key, sender addr ignored"); |
---|
| 4358 | $best_try_originator = undef; |
---|
| 4359 | last; |
---|
| 4360 | } |
---|
| 4361 | } |
---|
| 4362 | $best_try_originator; |
---|
| 4363 | } |
---|
| 4364 | |
---|
| 4365 | # Given a dotted-quad IPv4 address try reverse DNS resolve, and then |
---|
| 4366 | # forward DNS resolve. If they match, return domain name, |
---|
| 4367 | # otherwise return the IP address in brackets. (resolves IPv4 only) |
---|
| 4368 | # |
---|
| 4369 | sub ip_addr_to_name($) { |
---|
| 4370 | my($addr) = @_; # dotted-quad address string |
---|
| 4371 | local($1,$2,$3,$4); my($result); |
---|
| 4372 | if ($addr !~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) { |
---|
| 4373 | $result = $addr; # not an IPv4 address |
---|
| 4374 | } else { |
---|
| 4375 | my($binaddr) = pack('C4', $1,$2,$3,$4); # to binary string |
---|
| 4376 | do_log(5, "ip_addr_to_name: DNS reverse-resolving: $addr"); |
---|
| 4377 | my(@addr) = gethostbyaddr($binaddr,2); # IP -> name |
---|
| 4378 | $result = '['.$addr.']'; # IP address in brackets if nothing matches |
---|
| 4379 | if (@addr) { |
---|
| 4380 | my($name,$aliases,$addrtype,$length,@addrs) = @addr; |
---|
| 4381 | if ($name =~ /[^.]\.[a-zA-Z]+\z/s) { |
---|
| 4382 | do_log(5, "ip_addr_to_name: DNS forward-resolving: $name"); |
---|
| 4383 | my(@raddr) = gethostbyname($name); # name -> IP |
---|
| 4384 | my($rname,$raliases,$raddrtype,$rlength,@raddrs) = @raddr; |
---|
| 4385 | for my $ra (@raddrs) { |
---|
| 4386 | if (lc($ra) eq lc($binaddr)) { $result = $name; last } |
---|
| 4387 | } |
---|
| 4388 | } |
---|
| 4389 | } |
---|
| 4390 | } |
---|
| 4391 | do_log(3, "ip_addr_to_name: returning: $result"); |
---|
| 4392 | $result; |
---|
| 4393 | } |
---|
| 4394 | |
---|
| 4395 | # Obtain and parse the first entry (chronologically) in the 'Received:' header |
---|
| 4396 | # path trace - to be used as the value of the macro %t in customized messages |
---|
| 4397 | # |
---|
| 4398 | sub first_received_from($) { |
---|
| 4399 | my($entity) = shift; |
---|
| 4400 | my($first_received); |
---|
| 4401 | if (defined($entity)) { |
---|
| 4402 | my($fields) = parse_received($entity->head->get('received', -1)); |
---|
| 4403 | if (exists $fields->{'from'}) { |
---|
| 4404 | my($item, $v1, $v2, $v3, $comment) = @{$fields->{'from'}}; |
---|
| 4405 | $first_received = join(' ', $item, $comment); |
---|
| 4406 | $first_received =~ s/^[ \t\n\r]+//s; # discard leading whitespace |
---|
| 4407 | $first_received =~ s/[ \t\n\r]+\z//s; # discard trailing whitespace |
---|
| 4408 | } |
---|
| 4409 | do_log(5, "first_received_from: $first_received"); |
---|
| 4410 | } |
---|
| 4411 | $first_received; |
---|
| 4412 | } |
---|
| 4413 | |
---|
| 4414 | # Try to extract sender's public IP address from the Received trace |
---|
| 4415 | # |
---|
| 4416 | sub best_try_originator_ip($) { |
---|
| 4417 | my($entity) = @_; |
---|
| 4418 | my($first_received_from_ip); |
---|
| 4419 | if (defined($entity)) { |
---|
| 4420 | my(@publicnetworks) = qw( |
---|
| 4421 | !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 |
---|
| 4422 | !169.254.0.0/16 !192.0.2.0/24 !192.88.99.0/24 !224.0.0.0/4 |
---|
| 4423 | ::FFFF:0:0/96 |
---|
| 4424 | !:: !::1 !FF00::/8 !FE80::/10 !FEC0::/10 |
---|
| 4425 | ::/0 ); # rfc3330, rfc3513 |
---|
| 4426 | my(@received) = reverse $entity->head->get('received'); |
---|
| 4427 | $#received = 5 if $#received > 5; # first six, chronologically |
---|
| 4428 | for my $r (@received) { |
---|
| 4429 | $first_received_from_ip = fish_out_ip_from_received($r); |
---|
| 4430 | last if $first_received_from_ip ne '' && |
---|
| 4431 | eval { lookup_ip_acl($first_received_from_ip,\@publicnetworks) }; |
---|
| 4432 | } |
---|
| 4433 | do_log(5, "best_try_originator_ip: $first_received_from_ip"); |
---|
| 4434 | } |
---|
| 4435 | $first_received_from_ip; |
---|
| 4436 | } |
---|
| 4437 | |
---|
| 4438 | # For the purpose of informing administrators try to obtain true sender |
---|
| 4439 | # address or at least its site, as most viruses and spam have a nasty habit |
---|
| 4440 | # of faking envelope sender address. Return a pair of addresses: |
---|
| 4441 | # - the first (if defined) appears valid and may be used for sender |
---|
| 4442 | # notifications; |
---|
| 4443 | # - the second should only be used in generating customizable notification |
---|
| 4444 | # messages (macro %o), NOT to be used as address for sending notifications, |
---|
| 4445 | # as it can contain invalid address (but can be more informative). |
---|
| 4446 | # |
---|
| 4447 | sub best_try_originator($$$) { |
---|
| 4448 | my($sender, $entity, $virusname_list) = @_; |
---|
| 4449 | return ($sender,$sender) if !defined($entity); # don't bother if no header |
---|
| 4450 | my($originator) = |
---|
| 4451 | unmangle_sender($sender, $entity->head->get('from',0), $virusname_list); |
---|
| 4452 | return ($originator, $originator) if defined $originator; |
---|
| 4453 | my($first_received_from_ip) = best_try_originator_ip($entity); |
---|
| 4454 | $originator = '?@' . ip_addr_to_name($first_received_from_ip) |
---|
| 4455 | if $first_received_from_ip ne ''; |
---|
| 4456 | (undef, $originator); |
---|
| 4457 | } |
---|
| 4458 | |
---|
| 4459 | 1; |
---|
| 4460 | |
---|
| 4461 | # |
---|
| 4462 | package Amavis::Unpackers::NewFilename; |
---|
| 4463 | use strict; |
---|
| 4464 | use re 'taint'; |
---|
| 4465 | |
---|
| 4466 | BEGIN { |
---|
| 4467 | use Exporter (); |
---|
| 4468 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 4469 | $VERSION = '2.034'; |
---|
| 4470 | @ISA = qw(Exporter); |
---|
| 4471 | @EXPORT_OK = qw(&consumed_bytes); |
---|
| 4472 | } |
---|
| 4473 | |
---|
| 4474 | BEGIN { |
---|
| 4475 | import Amavis::Conf qw(c cr ca |
---|
| 4476 | $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR |
---|
| 4477 | $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR); |
---|
| 4478 | import Amavis::Util qw(ll do_log min max); |
---|
| 4479 | } |
---|
| 4480 | |
---|
| 4481 | use vars qw($avail_quota); # available bytes quota for unpacked mail |
---|
| 4482 | use vars qw($rem_quota); # remaining bytes quota for unpacked mail |
---|
| 4483 | |
---|
| 4484 | sub new($;$$) { # create a file name generator object |
---|
| 4485 | my($class, $maxfiles,$mail_size) = @_; |
---|
| 4486 | # calculate and initialize quota |
---|
| 4487 | $avail_quota = $rem_quota = # quota in bytes |
---|
| 4488 | max($MIN_EXPANSION_QUOTA, $mail_size * $MIN_EXPANSION_FACTOR, |
---|
| 4489 | min($MAX_EXPANSION_QUOTA, $mail_size * $MAX_EXPANSION_FACTOR)); |
---|
| 4490 | do_log(4,"Original mail size: $mail_size; quota set to: $avail_quota bytes"); |
---|
| 4491 | # create object |
---|
| 4492 | bless { |
---|
| 4493 | num_of_issued_names => 0, first_issued_ind => 1, last_issued_ind => 0, |
---|
| 4494 | maxfiles => $maxfiles, # undef disables limit |
---|
| 4495 | objlist => [], |
---|
| 4496 | }, $class; |
---|
| 4497 | } |
---|
| 4498 | |
---|
| 4499 | sub parts_list_reset($) { # clear a list of recently issued names |
---|
| 4500 | my($self) = shift; |
---|
| 4501 | $self->{num_of_issued_names} = 0; |
---|
| 4502 | $self->{first_issued_ind} = $self->{last_issued_ind} + 1; |
---|
| 4503 | $self->{objlist} = []; |
---|
| 4504 | } |
---|
| 4505 | |
---|
| 4506 | sub parts_list($) { # returns a ref to a list of recently issued names |
---|
| 4507 | my($self) = shift; |
---|
| 4508 | $self->{objlist}; |
---|
| 4509 | } |
---|
| 4510 | |
---|
| 4511 | sub parts_list_add($$) { # add a parts object to the list of parts |
---|
| 4512 | my($self, $part) = @_; |
---|
| 4513 | push(@{$self->{objlist}}, $part); |
---|
| 4514 | } |
---|
| 4515 | |
---|
| 4516 | sub generate_new_num($) { # make-up a new number for a file and return it |
---|
| 4517 | my($self) = @_; |
---|
| 4518 | if (defined($self->{maxfiles}) && |
---|
| 4519 | $self->{num_of_issued_names} >= $self->{maxfiles}) { |
---|
| 4520 | # do not change the text in die without adjusting decompose_part() |
---|
| 4521 | die "Maximum number of files ($self->{maxfiles}) exceeded"; |
---|
| 4522 | } |
---|
| 4523 | $self->{num_of_issued_names}++; $self->{last_issued_ind}++; |
---|
| 4524 | $self->{last_issued_ind}; |
---|
| 4525 | } |
---|
| 4526 | |
---|
| 4527 | sub consumed_bytes($$;$$) { |
---|
| 4528 | my($bytes, $bywhom, $tentatively, $exquota) = @_; |
---|
| 4529 | my($perc) = !$avail_quota ? '' : sprintf(", (%.0f%%)", |
---|
| 4530 | 100 * ($avail_quota - ($rem_quota - $bytes)) / $avail_quota); |
---|
| 4531 | do_log(4,"Charging $bytes bytes to remaining quota $rem_quota" |
---|
| 4532 | . " (out of $avail_quota$perc) - by $bywhom"); |
---|
| 4533 | if ($bytes > $rem_quota && $rem_quota >= 0) { |
---|
| 4534 | # Do not modify the following signal text, it gets matched elsewhere! |
---|
| 4535 | my($msg) = "Exceeded storage quota $avail_quota bytes by $bywhom; ". |
---|
| 4536 | "last chunk $bytes bytes"; |
---|
| 4537 | do_log(-1, $msg); |
---|
| 4538 | die "$msg\n" if !$exquota; |
---|
| 4539 | } |
---|
| 4540 | $rem_quota -= $bytes unless $tentatively; |
---|
| 4541 | $rem_quota; # return remaining quota |
---|
| 4542 | } |
---|
| 4543 | |
---|
| 4544 | 1; |
---|
| 4545 | |
---|
| 4546 | # |
---|
| 4547 | package Amavis::Unpackers::Part; |
---|
| 4548 | use strict; |
---|
| 4549 | use re 'taint'; |
---|
| 4550 | |
---|
| 4551 | BEGIN { |
---|
| 4552 | use Exporter (); |
---|
| 4553 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 4554 | $VERSION = '2.034'; |
---|
| 4555 | @ISA = qw(Exporter); |
---|
| 4556 | } |
---|
| 4557 | |
---|
| 4558 | BEGIN { |
---|
| 4559 | import Amavis::Util qw(ll do_log); |
---|
| 4560 | } |
---|
| 4561 | |
---|
| 4562 | use vars qw($file_generator_object); |
---|
| 4563 | sub init($) { $file_generator_object = shift } |
---|
| 4564 | |
---|
| 4565 | sub new($;$$) { # create a part descriptor object |
---|
| 4566 | my($class, $dir_name,$parent) = @_; |
---|
| 4567 | my($self) = bless {}, $class; |
---|
| 4568 | if (!defined($dir_name) && !defined($parent)) { |
---|
| 4569 | # just make an empty object, presumably used as a new root |
---|
| 4570 | } else { |
---|
| 4571 | $self->number($file_generator_object->generate_new_num); |
---|
| 4572 | $self->dir_name($dir_name) if defined $dir_name; |
---|
| 4573 | if (defined $parent) { |
---|
| 4574 | $self->parent($parent); |
---|
| 4575 | my($ch_ref) = $parent->children; |
---|
| 4576 | push(@$ch_ref,$self); $parent->children($ch_ref); |
---|
| 4577 | } |
---|
| 4578 | $file_generator_object->parts_list_add($self); # save it |
---|
| 4579 | ll(4) && do_log(4, "Issued a new " . |
---|
| 4580 | (defined $dir_name ? "file name" : "pseudo part") . ": " . |
---|
| 4581 | $self->base_name); |
---|
| 4582 | } |
---|
| 4583 | $self; |
---|
| 4584 | } |
---|
| 4585 | |
---|
| 4586 | sub number |
---|
| 4587 | { my($self)=shift; !@_ ? $self->{number} : ($self->{number}=shift) }; |
---|
| 4588 | sub dir_name |
---|
| 4589 | { my($self)=shift; !@_ ? $self->{dir_name} : ($self->{dir_name}=shift) }; |
---|
| 4590 | sub parent |
---|
| 4591 | { my($self)=shift; !@_ ? $self->{parent} : ($self->{parent}=shift) }; |
---|
| 4592 | sub children |
---|
| 4593 | { my($self)=shift; !@_ ? $self->{children}||[] : ($self->{children}=shift) }; |
---|
| 4594 | sub mime_placement # part location within a MIME tree, e.g. "1/1/3" |
---|
| 4595 | { my($self)=shift; !@_ ? $self->{place} : ($self->{place}=shift) }; |
---|
| 4596 | sub type_short # string or a ref to a list of strings |
---|
| 4597 | { my($self)=shift; !@_ ? $self->{ty_short} : ($self->{ty_short}=shift) }; |
---|
| 4598 | sub type_long |
---|
| 4599 | { my($self)=shift; !@_ ? $self->{ty_long} : ($self->{ty_long}=shift) }; |
---|
| 4600 | sub type_declared |
---|
| 4601 | { my($self)=shift; !@_ ? $self->{ty_decl} : ($self->{ty_decl}=shift) }; |
---|
| 4602 | sub name_declared # string or a ref to a list of strings |
---|
| 4603 | { my($self)=shift; !@_ ? $self->{nm_decl} : ($self->{nm_decl}=shift) }; |
---|
| 4604 | sub size |
---|
| 4605 | { my($self)=shift; !@_ ? $self->{size} : ($self->{size}=shift) }; |
---|
| 4606 | sub exists |
---|
| 4607 | { my($self)=shift; !@_ ? $self->{exists} : ($self->{exists}=shift) }; |
---|
| 4608 | sub attributes # listref of characters representing attributes |
---|
| 4609 | { my($self)=shift; !@_ ? $self->{attr} : ($self->{attr}=shift) }; |
---|
| 4610 | sub attributes_add { # U=undecodable, C=crypted, D=directory,S=special,L=link |
---|
| 4611 | my($self)=shift; my($a) = $self->{attr} || []; |
---|
| 4612 | for my $arg (@_) { push(@$a,$arg) if $arg ne '' && !grep {$_ eq $arg} @$a } |
---|
| 4613 | $self->{attr} = $a; |
---|
| 4614 | }; |
---|
| 4615 | |
---|
| 4616 | sub base_name { my($self)=shift; sprintf("p%03d",$self->number) } |
---|
| 4617 | |
---|
| 4618 | sub full_name { |
---|
| 4619 | my($self)=shift; my($d) = $self->dir_name; |
---|
| 4620 | !defined($d) ? undef : $d.'/'.$self->base_name; |
---|
| 4621 | } |
---|
| 4622 | |
---|
| 4623 | # returns a ref to a list of part ancestors, starting with the root object, |
---|
| 4624 | # and including the part object itself |
---|
| 4625 | sub path { |
---|
| 4626 | my($self)=shift; |
---|
| 4627 | my(@path); |
---|
| 4628 | for (my($p)=$self; defined($p); $p=$p->parent) { unshift(@path,$p) } |
---|
| 4629 | \@path; |
---|
| 4630 | }; |
---|
| 4631 | |
---|
| 4632 | 1; |
---|
| 4633 | |
---|
| 4634 | # |
---|
| 4635 | package Amavis::Unpackers::OurFiler; |
---|
| 4636 | use strict; |
---|
| 4637 | use re 'taint'; |
---|
| 4638 | |
---|
| 4639 | BEGIN { |
---|
| 4640 | use Exporter (); |
---|
| 4641 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 4642 | $VERSION = '2.034'; |
---|
| 4643 | @ISA = qw(Exporter MIME::Parser::Filer); # subclass of MIME::Parser::Filer |
---|
| 4644 | %EXPORT_TAGS = (); |
---|
| 4645 | @EXPORT = (); |
---|
| 4646 | @EXPORT_OK = (); |
---|
| 4647 | } |
---|
| 4648 | # This package will be used by mime_decode(). |
---|
| 4649 | # |
---|
| 4650 | # We don't want no heavy MIME::Parser machinery for file name extension |
---|
| 4651 | # guessing, decoding charsets in filenames (and listening to complaints |
---|
| 4652 | # about it), checking for evil filenames, checking for filename contention, ... |
---|
| 4653 | # (which can not be turned off completely by ignore_filename(1) !!!) |
---|
| 4654 | # Just enforce our file name! And while at it, collect generated filenames. |
---|
| 4655 | # |
---|
| 4656 | sub new($$$) { |
---|
| 4657 | my($class, $dir, $parent_obj) = @_; |
---|
| 4658 | $dir =~ s{/+\z}{}; # chop off trailing slashes from directory name |
---|
| 4659 | bless {parent => $parent_obj, directory => $dir}, $class; |
---|
| 4660 | } |
---|
| 4661 | |
---|
| 4662 | # provide a generated file name |
---|
| 4663 | sub output_path($@) { |
---|
| 4664 | my($self, $head) = @_; |
---|
| 4665 | my($newpart_obj) = |
---|
| 4666 | Amavis::Unpackers::Part->new($self->{directory}, $self->{parent}); |
---|
| 4667 | get_amavisd_part($head, $newpart_obj); # store object into head |
---|
| 4668 | $newpart_obj->full_name; |
---|
| 4669 | } |
---|
| 4670 | |
---|
| 4671 | sub get_amavisd_part($;$) { |
---|
| 4672 | my($head) = shift; |
---|
| 4673 | !@_ ? $head->{amavisd_parts_obj} : ($head->{amavisd_parts_obj} = shift); |
---|
| 4674 | } |
---|
| 4675 | |
---|
| 4676 | 1; |
---|
| 4677 | |
---|
| 4678 | # |
---|
| 4679 | package Amavis::Unpackers::Validity; |
---|
| 4680 | use strict; |
---|
| 4681 | use re 'taint'; |
---|
| 4682 | |
---|
| 4683 | BEGIN { |
---|
| 4684 | use Exporter (); |
---|
| 4685 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 4686 | $VERSION = '2.034'; |
---|
| 4687 | @ISA = qw(Exporter); |
---|
| 4688 | %EXPORT_TAGS = (); |
---|
| 4689 | @EXPORT = (); |
---|
| 4690 | @EXPORT_OK = qw(&check_header_validity &check_for_banned_names); |
---|
| 4691 | } |
---|
| 4692 | |
---|
| 4693 | BEGIN { |
---|
| 4694 | import Amavis::Util qw(ll do_log sanitize_str); |
---|
| 4695 | import Amavis::Conf qw(:platform c cr ca); |
---|
| 4696 | import Amavis::Lookup qw(lookup); |
---|
| 4697 | } |
---|
| 4698 | |
---|
| 4699 | use subs @EXPORT_OK; |
---|
| 4700 | |
---|
| 4701 | sub check_header_validity($$) { |
---|
| 4702 | my($conn, $msginfo) = @_; |
---|
| 4703 | my(@bad); |
---|
| 4704 | my($curr_head); |
---|
| 4705 | for my $next_head (@{$msginfo->orig_header}, "\n") { |
---|
| 4706 | if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head } # folded |
---|
| 4707 | else { # new header |
---|
| 4708 | if (!defined($curr_head)) { # no previous complete header |
---|
| 4709 | } else { |
---|
| 4710 | # obsolete rfc822 syntax allowed whitespace before colon |
---|
| 4711 | my($field_name, $field_body) = |
---|
| 4712 | $curr_head =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s |
---|
| 4713 | ? ($1, $2) : (undef, $curr_head); |
---|
| 4714 | my($msg1,$msg2); |
---|
| 4715 | if (!defined($field_name) && $curr_head=~/^()()(.*)\z/s) { |
---|
| 4716 | $msg1 = "Invalid header field head"; |
---|
| 4717 | } elsif ($curr_head =~ /^(.*?)([\000\015])(.*)\z/s) { |
---|
| 4718 | $msg1 = "Improper use of control character"; |
---|
| 4719 | } elsif ($curr_head =~ /^(.*?)([\200-\377])(.*)\z/s) { |
---|
| 4720 | $msg1 = "Non-encoded 8-bit data"; |
---|
| 4721 | } elsif ($curr_head =~ /^(.*?)([^\000-\377])(.*)\z/s) { |
---|
| 4722 | $msg1 = "Non-encoded Unicode character"; |
---|
| 4723 | } elsif ($curr_head =~ /^()()([ \t]+)$/m) { |
---|
| 4724 | $msg1 ="Improper folded header field made up entirely of whitespace"; |
---|
| 4725 | } |
---|
| 4726 | if (defined $msg1) { |
---|
| 4727 | my($pre, $ch, $post) = ($1, $2, $3); |
---|
| 4728 | if (length($post) > 20) { $post = substr($post,0,15) . "..." } |
---|
| 4729 | if (length($pre)-length($field_name)-2 > 50-length($post)) { |
---|
| 4730 | $pre = "$field_name: ..." |
---|
| 4731 | . substr($pre, length($pre) - (45-length($post))); |
---|
| 4732 | } |
---|
| 4733 | $msg1 .= sprintf(" (char %02X hex)", ord($ch)) if length($ch)==1; |
---|
| 4734 | $msg1 .= " in message header '$field_name'" if $field_name ne ''; |
---|
| 4735 | $msg2 = sanitize_str($pre); my($msg2_pre_l) = length($msg2); |
---|
| 4736 | $msg2 .= sanitize_str($ch . $post); |
---|
| 4737 | # push(@bad, "$msg1\n $msg2\n " . (' ' x $msg2_pre_l) . '^'); |
---|
| 4738 | push(@bad, "$msg1: $msg2"); |
---|
| 4739 | } |
---|
| 4740 | } |
---|
| 4741 | last if $next_head eq $eol; # end-of-header reached |
---|
| 4742 | $curr_head = $next_head; |
---|
| 4743 | } |
---|
| 4744 | } |
---|
| 4745 | @bad; |
---|
| 4746 | } |
---|
| 4747 | |
---|
| 4748 | sub check_for_banned_names($) { |
---|
| 4749 | my($parts_root) = @_; |
---|
| 4750 | do_log(3, "Checking for banned types and filenames"); |
---|
| 4751 | my(@banned_part_descr,@banned_matching_keys,@banned_rhs); my($part); |
---|
| 4752 | my($bfnmr) = ca('banned_filename_maps'); # a ref to a list |
---|
| 4753 | for (my(@unvisited)=($parts_root); |
---|
| 4754 | @unvisited and $part=shift(@unvisited); |
---|
| 4755 | push(@unvisited,@{$part->children})) |
---|
| 4756 | { # traverse decomposed parts tree breadth-first |
---|
| 4757 | my(@path) = @{$part->path}; |
---|
| 4758 | next if @path <= 1; |
---|
| 4759 | # ll(5) && do_log(5, "part path: ".join(", ", map {$_->base_name} @path)); |
---|
| 4760 | shift(@path); # ignore place-holder root node |
---|
| 4761 | next if @{$part->children}; # ignore non-leaf nodes |
---|
| 4762 | my(@descr); my($found,$key_val,$key_what,$result,$matchingkey); |
---|
| 4763 | for my $p (@path) { |
---|
| 4764 | my(@k,$n); |
---|
| 4765 | $n = $p->base_name; |
---|
| 4766 | if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"P=$n") } |
---|
| 4767 | $n = $p->mime_placement; |
---|
| 4768 | if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"L=$n") } |
---|
| 4769 | $n = $p->type_declared; |
---|
| 4770 | $n = [$n] if !ref($n); |
---|
| 4771 | for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"M=$m")} } |
---|
| 4772 | $n = $p->type_short; |
---|
| 4773 | $n = [$n] if !ref($n); |
---|
| 4774 | for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"T=$m")} } |
---|
| 4775 | $n = $p->name_declared; |
---|
| 4776 | $n = [$n] if !ref($n); |
---|
| 4777 | for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"N=$m")} } |
---|
| 4778 | $n = $p->attributes; |
---|
| 4779 | $n = [$n] if !ref($n); |
---|
| 4780 | for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"A=$m")} } |
---|
| 4781 | push(@descr, join("\t",@k)); |
---|
| 4782 | if (!$found && @$bfnmr) { # still searching? (old style) |
---|
| 4783 | for my $k (@k) { |
---|
| 4784 | $k =~ /^([a-zA-Z0-9])=(.*)\z/s; |
---|
| 4785 | ($key_what,$key_val) = ($1,$2); |
---|
| 4786 | next unless $key_what =~ /^[TMNA]\z/; |
---|
| 4787 | if ($key_what eq 'T') { |
---|
| 4788 | $key_val = '.' . $key_val; # prepend a dot for compatibility |
---|
| 4789 | } elsif ($key_what eq 'A') { |
---|
| 4790 | if ($key_val eq 'U') { $key_val = 'UNDECIPHERABLE' } else { next } |
---|
| 4791 | } |
---|
| 4792 | do_log(4, sprintf("check_for_banned (%s) %s=%s", |
---|
| 4793 | $p->base_name, $key_what, $key_val)); |
---|
| 4794 | ($result,$matchingkey) = lookup(0,$key_val,@$bfnmr); |
---|
| 4795 | $found++ if defined $result; |
---|
| 4796 | last if $found; |
---|
| 4797 | } |
---|
| 4798 | } |
---|
| 4799 | } |
---|
| 4800 | my($key_val_str) = join(' | ',@descr); $key_val_str =~ s/\t/,/g; |
---|
| 4801 | if (!$found) { # try new style |
---|
| 4802 | ($result,$matchingkey) = |
---|
| 4803 | lookup(0,join("\n",@descr), |
---|
| 4804 | Amavis::Lookup::Label->new('banned_namepath_re'), |
---|
| 4805 | cr('banned_namepath_re')); |
---|
| 4806 | $found++ if defined $result; |
---|
| 4807 | } |
---|
| 4808 | my($ll) = $result ? 1 : 3; |
---|
| 4809 | if (ll($ll)) { # only bother with logging when needed |
---|
| 4810 | my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b", |
---|
| 4811 | e => "\e", a => "\a", t => "\t"); |
---|
| 4812 | my($mk) = $matchingkey; # pretty-print |
---|
| 4813 | $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : '\\'.$1 }egsx; |
---|
| 4814 | do_log($ll, sprintf('p.path%s: "%s"%s', |
---|
| 4815 | !$result?'':" BANNED:$result", $key_val_str, |
---|
| 4816 | !defined $result ? '' : ", matching_key=\"$mk\"")); |
---|
| 4817 | } |
---|
| 4818 | if ($result) { |
---|
| 4819 | push(@banned_part_descr, $key_val_str); |
---|
| 4820 | push(@banned_matching_keys, $matchingkey); |
---|
| 4821 | push(@banned_rhs, $result); |
---|
| 4822 | } |
---|
| 4823 | } |
---|
| 4824 | # return listrefs of parts descriptors, matching keys and lookup results |
---|
| 4825 | (\@banned_part_descr, \@banned_matching_keys, \@banned_rhs); |
---|
| 4826 | } |
---|
| 4827 | |
---|
| 4828 | 1; |
---|
| 4829 | |
---|
| 4830 | # |
---|
| 4831 | package Amavis::Unpackers::MIME; |
---|
| 4832 | use strict; |
---|
| 4833 | use re 'taint'; |
---|
| 4834 | |
---|
| 4835 | BEGIN { |
---|
| 4836 | use Exporter (); |
---|
| 4837 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 4838 | $VERSION = '2.034'; |
---|
| 4839 | @ISA = qw(Exporter); |
---|
| 4840 | %EXPORT_TAGS = (); |
---|
| 4841 | @EXPORT = (); |
---|
| 4842 | @EXPORT_OK = qw(&mime_decode); |
---|
| 4843 | } |
---|
| 4844 | use Errno qw(ENOENT EACCES); |
---|
| 4845 | use MIME::Parser; |
---|
| 4846 | use MIME::Words; |
---|
| 4847 | |
---|
| 4848 | BEGIN { |
---|
| 4849 | import Amavis::Conf qw(:platform c cr ca); |
---|
| 4850 | import Amavis::Timing qw(section_time); |
---|
| 4851 | import Amavis::Util qw(snmp_count ll do_log); |
---|
| 4852 | import Amavis::Unpackers::NewFilename qw(consumed_bytes); |
---|
| 4853 | } |
---|
| 4854 | |
---|
| 4855 | use subs @EXPORT_OK; |
---|
| 4856 | |
---|
| 4857 | # save MIME preamble and epilogue (if nontrivial) as extra (pseudo)parts |
---|
| 4858 | sub mime_decode_pre_epi($$$$$) { |
---|
| 4859 | my($pe_name, $pe_lines, $tempdir, $parent_obj, $placement) = @_; |
---|
| 4860 | if (defined $pe_lines && @$pe_lines) { |
---|
| 4861 | do_log(5, "mime_decode_$pe_name: " . scalar(@$pe_lines) . " lines"); |
---|
| 4862 | if (@$pe_lines > 5 || "@$pe_lines" !~ m{^[a-zA-Z0-9/\@:;,. \t\n_-]*\z}s) { |
---|
| 4863 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts", |
---|
| 4864 | $parent_obj); |
---|
| 4865 | $newpart_obj->mime_placement($placement); |
---|
| 4866 | $newpart_obj->name_declared($pe_name); |
---|
| 4867 | my($newpart) = $newpart_obj->full_name; |
---|
| 4868 | my($outpart) = IO::File->new; |
---|
| 4869 | $outpart->open($newpart,'>') |
---|
| 4870 | or die "Can't create $pe_name file $newpart: $!"; |
---|
| 4871 | binmode($outpart, ":bytes") or die "Can't cancel :utf8 mode: $!" |
---|
| 4872 | if $unicode_aware; |
---|
| 4873 | my($len); |
---|
| 4874 | for (@$pe_lines) { |
---|
| 4875 | $outpart->print($_) or die "Can't write $pe_name to $newpart: $!"; |
---|
| 4876 | $len += length($_); |
---|
| 4877 | } |
---|
| 4878 | $outpart->close or die "Can't close $pe_name $newpart: $!"; |
---|
| 4879 | $newpart_obj->size($len); |
---|
| 4880 | consumed_bytes($len, "mime_decode_$pe_name", 0, 1); |
---|
| 4881 | } |
---|
| 4882 | } |
---|
| 4883 | } |
---|
| 4884 | |
---|
| 4885 | # traverse MIME::Entity object depth-first, |
---|
| 4886 | # extracting preambles and epilogues as extra (pseudo)parts, and |
---|
| 4887 | # filling-in additional information into Amavis::Unpackers::Part objects |
---|
| 4888 | sub mime_traverse($$$$$); # prototype |
---|
| 4889 | sub mime_traverse($$$$$) { |
---|
| 4890 | my($entity, $tempdir, $parent_obj, $depth, $placement) = @_; |
---|
| 4891 | mime_decode_pre_epi('preamble', $entity->preamble, |
---|
| 4892 | $tempdir, $parent_obj, $placement); |
---|
| 4893 | my($mt, $et) = ($entity->mime_type, $entity->effective_type); |
---|
| 4894 | my($part); my($head) = $entity->head; my($body) = $entity->bodyhandle; |
---|
| 4895 | if (!defined($body)) { # a MIME container only contains parts, no bodypart |
---|
| 4896 | # create pseudo-part objects for MIME containers (e.g. multipart/* ) |
---|
| 4897 | $part = Amavis::Unpackers::Part->new(undef,$parent_obj); |
---|
| 4898 | # $part->type_short('no-file'); |
---|
| 4899 | do_log(2, $part->base_name." $placement Content-Type: $mt"); |
---|
| 4900 | } else { # does have a body part (i.e. not a MIME container) |
---|
| 4901 | my($fn) = $body->path; my($size); |
---|
| 4902 | if (!defined($fn)) { $size = length($body->as_string) } |
---|
| 4903 | else { |
---|
| 4904 | my($msg); my($errn) = lstat($fn) ? 0 : 0+$!; |
---|
| 4905 | if ($errn == ENOENT) { $msg = "does not exist" } |
---|
| 4906 | elsif ($errn) { $msg = "is inaccessible: $!" } |
---|
| 4907 | elsif (!-r _) { $msg = "is not readable" } |
---|
| 4908 | elsif (!-f _) { $msg = "is not a regular file" } |
---|
| 4909 | else { |
---|
| 4910 | $size = -s _; |
---|
| 4911 | do_log(4,"mime_traverse: file $fn is empty") if !$size; |
---|
| 4912 | } |
---|
| 4913 | do_log(-1,"WARN: mime_traverse: file $fn $msg") if defined $msg; |
---|
| 4914 | } |
---|
| 4915 | consumed_bytes($size, 'mime_decode', 0, 1); |
---|
| 4916 | # retrieve Amavis::Unpackers::Part object (if any), stashed into head obj |
---|
| 4917 | $part = Amavis::Unpackers::OurFiler::get_amavisd_part($head); |
---|
| 4918 | if (defined $part) { |
---|
| 4919 | $part->size($size); |
---|
| 4920 | if ($size==0) { $part->type_short('empty'); $part->type_long('empty') } |
---|
| 4921 | do_log(2, $part->base_name." $placement Content-Type: $mt" . |
---|
| 4922 | ", size: $size B, name: ".$entity->head->recommended_filename); |
---|
| 4923 | my($old_parent_obj) = $part->parent; |
---|
| 4924 | if ($parent_obj ne $old_parent_obj) { # reparent if necessary |
---|
| 4925 | ll(5) && do_log(5,sprintf("reparenting %s from %s to %s", |
---|
| 4926 | $part->base_name, |
---|
| 4927 | $old_parent_obj->base_name, $parent_obj->base_name)); |
---|
| 4928 | my($ch_ref) = $old_parent_obj->children; |
---|
| 4929 | $old_parent_obj->children([grep {$_ ne $part} @$ch_ref]); |
---|
| 4930 | $ch_ref = $parent_obj->children; |
---|
| 4931 | push(@$ch_ref,$part); $parent_obj->children($ch_ref); |
---|
| 4932 | $part->parent($parent_obj); |
---|
| 4933 | } |
---|
| 4934 | } |
---|
| 4935 | } |
---|
| 4936 | if (defined $part) { |
---|
| 4937 | $part->mime_placement($placement); |
---|
| 4938 | $part->type_declared($mt eq $et ? $mt : [$mt, $et]); |
---|
| 4939 | my(@rn); # recommended file names, both raw and RFC 2047 decoded |
---|
| 4940 | my($val, $val_decoded); |
---|
| 4941 | $val = $head->mime_attr('content-disposition.filename'); |
---|
| 4942 | if ($val ne '') { |
---|
| 4943 | push(@rn, $val); |
---|
| 4944 | $val_decoded = MIME::Words::decode_mimewords($val); |
---|
| 4945 | push(@rn, $val_decoded) if $val_decoded ne $val; |
---|
| 4946 | } |
---|
| 4947 | $val = $head->mime_attr('content-type.name'); |
---|
| 4948 | if ($val ne '') { |
---|
| 4949 | $val_decoded = MIME::Words::decode_mimewords($val); |
---|
| 4950 | push(@rn, $val_decoded) if !grep { $_ eq $val_decoded } @rn; |
---|
| 4951 | push(@rn, $val) if !grep { $_ eq $val } @rn; |
---|
| 4952 | } |
---|
| 4953 | $part->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn; |
---|
| 4954 | } |
---|
| 4955 | mime_decode_pre_epi('epilogue', $entity->epilogue, |
---|
| 4956 | $tempdir, $parent_obj, $placement); |
---|
| 4957 | my($item_num) = 0; |
---|
| 4958 | for my $e ($entity->parts) { # recursive descent |
---|
| 4959 | $item_num++; |
---|
| 4960 | mime_traverse($e,$tempdir,$part,$depth+1,"$placement/$item_num"); |
---|
| 4961 | } |
---|
| 4962 | } |
---|
| 4963 | |
---|
| 4964 | # Break up mime parts, return MIME::Entity object |
---|
| 4965 | sub mime_decode($$$) { |
---|
| 4966 | my($fileh, $tempdir, $parent_obj) = @_; |
---|
| 4967 | # $fileh may be an open file handle, or a file name |
---|
| 4968 | |
---|
| 4969 | my($parser) = MIME::Parser->new; |
---|
| 4970 | $parser->filer(Amavis::Unpackers::OurFiler->new("$tempdir/parts", |
---|
| 4971 | $parent_obj)); |
---|
| 4972 | $parser->ignore_errors(1); # also is the default |
---|
| 4973 | # $parser->extract_nested_messages(0); |
---|
| 4974 | $parser->extract_nested_messages("NEST"); # parse embedded message/rfc822 |
---|
| 4975 | $parser->extract_uuencode(1); |
---|
| 4976 | my($entity); |
---|
| 4977 | snmp_count('OpsDecByMimeParser'); |
---|
| 4978 | if (ref($fileh)) { # assume open file handle |
---|
| 4979 | do_log(4, "Extracting mime components"); |
---|
| 4980 | $fileh->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
| 4981 | local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted ! |
---|
| 4982 | $entity = $parser->parse($fileh); |
---|
| 4983 | } else { # assume $fileh is a file name |
---|
| 4984 | do_log(4, "Extracting mime components from $fileh"); |
---|
| 4985 | local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted ! |
---|
| 4986 | $entity = $parser->parse_open("$tempdir/parts/$fileh"); |
---|
| 4987 | } |
---|
| 4988 | # my($mime_err) = $parser->last_error; # deprecated |
---|
| 4989 | my($mime_err) = $parser->results->errors; |
---|
| 4990 | $mime_err=~s/\s+\z//; $mime_err=~s/[ \t\r]*\n+/; /g; $mime_err=~s/\s+/ /g; |
---|
| 4991 | $mime_err = substr($mime_err,0,250) . '...' if length($mime_err) > 250; |
---|
| 4992 | do_log(1, "WARN: MIME::Parser $mime_err") if $mime_err ne ''; |
---|
| 4993 | mime_traverse($entity, $tempdir, $parent_obj, 0, '1'); |
---|
| 4994 | section_time('mime_decode'); |
---|
| 4995 | ($entity, $mime_err); |
---|
| 4996 | } |
---|
| 4997 | |
---|
| 4998 | 1; |
---|
| 4999 | |
---|
| 5000 | # |
---|
| 5001 | package Amavis::Notify; |
---|
| 5002 | use strict; |
---|
| 5003 | use re 'taint'; |
---|
| 5004 | |
---|
| 5005 | BEGIN { |
---|
| 5006 | use Exporter (); |
---|
| 5007 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 5008 | $VERSION = '2.034'; |
---|
| 5009 | @ISA = qw(Exporter); |
---|
| 5010 | %EXPORT_TAGS = (); |
---|
| 5011 | @EXPORT = (); |
---|
| 5012 | @EXPORT_OK = qw(&delivery_status_notification &delivery_short_report |
---|
| 5013 | &string_to_mime_entity &defanged_mime_entity); |
---|
| 5014 | } |
---|
| 5015 | |
---|
| 5016 | BEGIN { |
---|
| 5017 | import Amavis::Util qw(ll do_log am_id safe_encode q_encode); |
---|
| 5018 | import Amavis::Timing qw(section_time); |
---|
| 5019 | import Amavis::Conf qw(:platform $myhostname c cr ca); |
---|
| 5020 | import Amavis::Lookup qw(lookup); |
---|
| 5021 | import Amavis::Expand qw(expand); |
---|
| 5022 | import Amavis::rfc2821_2822_Tools; |
---|
| 5023 | } |
---|
| 5024 | # use Encode; # Perl 5.8 UTF-8 support |
---|
| 5025 | use MIME::Entity; |
---|
| 5026 | |
---|
| 5027 | use subs @EXPORT_OK; |
---|
| 5028 | |
---|
| 5029 | # Convert mail (that was obtained by macro-expanding notification templates) |
---|
| 5030 | # into proper MIME::Entity object. Some ad-hoc solutions are used |
---|
| 5031 | # for compatibility with previous version. |
---|
| 5032 | # |
---|
| 5033 | sub string_to_mime_entity($) { |
---|
| 5034 | my($mail_as_string_ref) = @_; |
---|
| 5035 | local($1,$2,$3); my($entity); my($m_hdr,$m_body); |
---|
| 5036 | ($m_hdr, $m_body) = ($1, $3) |
---|
| 5037 | if $$mail_as_string_ref =~ /^(.*?\r?\n)(\r?\n|\z)(.*)\z/s; |
---|
| 5038 | $m_body = safe_encode(c('bdy_encoding'), $m_body); |
---|
| 5039 | # make sure _our_ source line number is reported in case of failure |
---|
| 5040 | my($nxmh) = c('notify_xmailer_header'); |
---|
| 5041 | eval {$entity = MIME::Entity->build( |
---|
| 5042 | Type => 'text/plain', Encoding => '-SUGGEST', Charset=> c('bdy_encoding'), |
---|
| 5043 | (defined $nxmh && $nxmh eq '' ? () # leave the MIME::Entity default |
---|
| 5044 | : ('X-Mailer' => $nxmh) ), # X-Mailer hdr or undef |
---|
| 5045 | Data => $m_body); 1} or do {chomp($@); die $@}; |
---|
| 5046 | my($head) = $entity->head; |
---|
| 5047 | # insert header fields from template into MIME::Head entity |
---|
| 5048 | $m_hdr =~ s/\r?\n([ \t])/$1/g; # unfold template header |
---|
| 5049 | for my $hdr_line (split(/\r?\n/, $m_hdr)) { |
---|
| 5050 | if ($hdr_line =~ /^([^:]*):\s*(.*)\z/s) { |
---|
| 5051 | my($fhead, $fbody) = ($1, $2); |
---|
| 5052 | # encode according to RFC 2047 if necessary |
---|
| 5053 | $fhead = safe_encode('ascii', $fhead); |
---|
| 5054 | if ($fhead =~ /^(X-.*|Subject|Comments)\z/si && |
---|
| 5055 | $fbody =~ /[^\011\012\040-\176]/) # nonprint. except TAB and LF? |
---|
| 5056 | { # encode according to RFC 2047 |
---|
| 5057 | # TODO: shouldn't we unfold first?! |
---|
| 5058 | my($fbody_octets); |
---|
| 5059 | if (!$unicode_aware) { $fbody_octets = $fbody } |
---|
| 5060 | else { |
---|
| 5061 | $fbody_octets = safe_encode(c('hdr_encoding'), $fbody); |
---|
| 5062 | do_log(5, "string_to_mime_entity UTF-8 body: $fbody"); |
---|
| 5063 | do_log(5, "string_to_mime_entity body octets: $fbody_octets"); |
---|
| 5064 | } |
---|
| 5065 | my($qb) = c('hdr_encoding_qb'); |
---|
| 5066 | if (uc($qb) eq 'Q') { |
---|
| 5067 | $fbody = q_encode($fbody_octets, $qb, c('hdr_encoding')); |
---|
| 5068 | } else { |
---|
| 5069 | $fbody = MIME::Words::encode_mimeword($fbody_octets, |
---|
| 5070 | $qb, c('hdr_encoding')); |
---|
| 5071 | } |
---|
| 5072 | } else { # supposed to be in plain ASCII, let's make sure it is |
---|
| 5073 | $fbody = safe_encode('ascii', $fbody); |
---|
| 5074 | } |
---|
| 5075 | do_log(5, sprintf("string_to_mime_entity %s: %s", $fhead, $fbody)); |
---|
| 5076 | # make sure _our_ source line number is reported in case of failure |
---|
| 5077 | if (!eval { $head->replace($fhead, $fbody); 1 }) { |
---|
| 5078 | chomp($@); |
---|
| 5079 | die sprintf("%s header field '%s: %s'", |
---|
| 5080 | ($@ eq '' ? "invalid" : "$@, "), $fhead, $fbody); |
---|
| 5081 | } |
---|
| 5082 | } |
---|
| 5083 | } |
---|
| 5084 | $entity; # return the built MIME::Entity |
---|
| 5085 | } |
---|
| 5086 | |
---|
| 5087 | # Generate delivery status notification according to |
---|
| 5088 | # rfc1892 (now rfc3462) and rfc1894 (now rfc3464). |
---|
| 5089 | # Return dsn message object if dsn is needed, or undef otherwise. |
---|
| 5090 | # |
---|
| 5091 | sub delivery_status_notification($$$$$) { |
---|
| 5092 | my($conn,$msginfo,$report_success_dsn_also,$builtins_ref,$template_ref) = @_; |
---|
| 5093 | my($dsn_time) = time; # time of dsn creation - now |
---|
| 5094 | my($notification); |
---|
| 5095 | if ($msginfo->sender eq '') { # must not respond to null reverse path |
---|
| 5096 | do_log(4, "Not sending DSN to empty return path"); |
---|
| 5097 | } else { |
---|
| 5098 | my($from_mta, $client_ip) = ($conn->smtp_helo, $conn->client_ip); |
---|
| 5099 | my($msg) = ''; # constructed dsn text according to rfc3464 |
---|
| 5100 | $msg .= "Reporting-MTA: dns; $myhostname\n"; |
---|
| 5101 | $msg .= "Received-From-MTA: smtp; $from_mta ([$client_ip])\n" |
---|
| 5102 | if $from_mta ne ''; |
---|
| 5103 | $msg .= "Arrival-Date: " . rfc2822_timestamp($msginfo->rx_time) . "\n"; |
---|
| 5104 | |
---|
| 5105 | my($any); # any recipients with failed delivery? |
---|
| 5106 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 5107 | my($remote_mta) = $r->recip_remote_mta; |
---|
| 5108 | my($smtp_resp) = $r->recip_smtp_response; |
---|
| 5109 | if (!$r->recip_done) { |
---|
| 5110 | if ($msginfo->delivery_method eq '') { # e.g. milter |
---|
| 5111 | # as far as we are concerned all is ok, delivery will be performed |
---|
| 5112 | # by a helper program or MTA |
---|
| 5113 | $smtp_resp = "250 2.5.0 Ok, continue delivery"; |
---|
| 5114 | } else { |
---|
| 5115 | do_log(-2,"TROUBLE: recipient not done: <" |
---|
| 5116 | . $r->recip_addr . "> " . $smtp_resp); |
---|
| 5117 | } |
---|
| 5118 | } |
---|
| 5119 | my($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg); |
---|
| 5120 | if ($smtp_resp =~ /^ (\d{3}) \s+ ([245] \. \d{1,3} \. \d{1,3})? |
---|
| 5121 | \s* (.*) \z/xs) { |
---|
| 5122 | ($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg) = ($1,$2,$3); |
---|
| 5123 | } else { |
---|
| 5124 | $smtp_resp_msg = $smtp_resp; |
---|
| 5125 | } |
---|
| 5126 | my($smtp_resp_class) = $smtp_resp_code =~ /^(\d)/ ? $1 : '0'; |
---|
| 5127 | if ($smtp_resp_enhcode eq '' && $smtp_resp_class =~ /^([245])\z/) { |
---|
| 5128 | $smtp_resp_enhcode = "$1.0.0"; |
---|
| 5129 | } |
---|
| 5130 | # skip success notifications |
---|
| 5131 | next unless $smtp_resp_class ne '2' || $report_success_dsn_also; |
---|
| 5132 | $any++; |
---|
| 5133 | $msg .= "\n"; # empty line between groups of per-recipient fields |
---|
| 5134 | if ($remote_mta ne '' && $r->recip_final_addr ne $r->recip_addr) { |
---|
| 5135 | $msg .= "X-NextToLast-Final-Recipient: rfc822; " |
---|
| 5136 | . quote_rfc2821_local($r->recip_addr) . "\n"; |
---|
| 5137 | $msg .= "Final-Recipient: rfc822; " |
---|
| 5138 | . quote_rfc2821_local($r->recip_final_addr) . "\n"; |
---|
| 5139 | } else { |
---|
| 5140 | $msg .= "Final-Recipient: rfc822; " |
---|
| 5141 | . quote_rfc2821_local($r->recip_addr) . "\n"; |
---|
| 5142 | } |
---|
| 5143 | $msg .= "Action: ".($smtp_resp_class eq '2' ? 'delivered':'failed')."\n"; |
---|
| 5144 | $msg .= "Status: $smtp_resp_enhcode\n"; |
---|
| 5145 | my($rem_smtp_resp) = $r->recip_remote_mta_smtp_response; |
---|
| 5146 | if ($remote_mta eq '' || $rem_smtp_resp eq '') { |
---|
| 5147 | $msg .= "Diagnostic-Code: smtp; $smtp_resp\n"; |
---|
| 5148 | } else { |
---|
| 5149 | $msg .= "Remote-MTA: dns; $remote_mta\n"; |
---|
| 5150 | $msg .= "Diagnostic-Code: smtp; $rem_smtp_resp\n"; |
---|
| 5151 | } |
---|
| 5152 | $msg .= "Last-Attempt-Date: " . rfc2822_timestamp($dsn_time) . "\n"; |
---|
| 5153 | } |
---|
| 5154 | return $notification if !$any; # don't bother, we won't be sending DSN |
---|
| 5155 | |
---|
| 5156 | my($to_hdr) = qquote_rfc2821_local($msginfo->sender_contact); |
---|
| 5157 | |
---|
| 5158 | # use the provided template text |
---|
| 5159 | my(%mybuiltins) = %$builtins_ref; # make a local copy |
---|
| 5160 | $mybuiltins{'f'} = c('hdrfrom_notify_sender'); |
---|
| 5161 | $mybuiltins{'T'} = $to_hdr; |
---|
| 5162 | $mybuiltins{'d'} = rfc2822_timestamp($dsn_time); |
---|
| 5163 | my($dsn) = expand($template_ref, \%mybuiltins); |
---|
| 5164 | |
---|
| 5165 | my($dsn_entity) = string_to_mime_entity($dsn); |
---|
| 5166 | $dsn_entity->make_multipart; |
---|
| 5167 | my($head) = $dsn_entity->head; |
---|
| 5168 | |
---|
| 5169 | # rfc3464: The From field of the message header of the DSN SHOULD contain |
---|
| 5170 | # the address of a human who is responsible for maintaining the mail system |
---|
| 5171 | # at the Reporting MTA site (e.g. Postmaster), so that a reply to the |
---|
| 5172 | # DSN will reach that person. |
---|
| 5173 | eval { $head->replace('From', c('hdrfrom_notify_sender')); 1 } |
---|
| 5174 | or do { chomp($@); die $@ }; |
---|
| 5175 | eval { $head->replace('To', $to_hdr); 1 } or do { chomp($@); die $@ }; |
---|
| 5176 | eval { $head->replace('Date', rfc2822_timestamp($dsn_time)); 1 } |
---|
| 5177 | or do { chomp($@); die $@ }; |
---|
| 5178 | |
---|
| 5179 | my($field) = Mail::Field->new('Content_type'); # underline, not hyphen! |
---|
| 5180 | $field->type("multipart/report; report-type=delivery-status"); |
---|
| 5181 | $field->boundary(MIME::Entity::make_boundary()); |
---|
| 5182 | $head->replace('Content-type', $field->stringify); |
---|
| 5183 | $head = undef; |
---|
| 5184 | |
---|
| 5185 | # make sure _our_ source line number is reported in case of failure |
---|
| 5186 | eval {$dsn_entity->attach( |
---|
| 5187 | Type => 'message/delivery-status', Encoding => '7bit', |
---|
| 5188 | Description => 'Delivery error report', |
---|
| 5189 | Data => $msg); 1} or do {chomp($@); die $@}; |
---|
| 5190 | eval {$dsn_entity->attach( |
---|
| 5191 | Type => 'text/rfc822-headers', Encoding => '-SUGGEST', |
---|
| 5192 | Description => 'Undelivered-message headers', |
---|
| 5193 | Data => $msginfo->orig_header); 1} or do {chomp($@); die $@}; |
---|
| 5194 | $notification = Amavis::In::Message->new; |
---|
| 5195 | $notification->rx_time($dsn_time); |
---|
| 5196 | $notification->delivery_method(c('notify_method')); |
---|
| 5197 | $notification->sender(c('mailfrom_notify_sender')); # should be empty! |
---|
| 5198 | $notification->auth_submitter('<>'); |
---|
| 5199 | $notification->auth_user(c('amavis_auth_user')); |
---|
| 5200 | $notification->auth_pass(c('amavis_auth_pass')); |
---|
| 5201 | $notification->recips([$msginfo->sender_contact]); |
---|
| 5202 | $notification->mail_text($dsn_entity); |
---|
| 5203 | } |
---|
| 5204 | $notification; |
---|
| 5205 | } |
---|
| 5206 | |
---|
| 5207 | # Return a pair of arrayrefs of quoted recipient addresses (the first lists |
---|
| 5208 | # recipients with successful delivery status, the second all the rest), |
---|
| 5209 | # plus a list of short per-recipient delivery reports for failed deliveries, |
---|
| 5210 | # that can be used in the first MIME part (the free text format) of delivery |
---|
| 5211 | # status notifications. |
---|
| 5212 | # |
---|
| 5213 | sub delivery_short_report($) { |
---|
| 5214 | my($msginfo) = @_; |
---|
| 5215 | my(@succ_recips, @failed_recips, @failed_recips_full); |
---|
| 5216 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 5217 | my($remote_mta) = $r->recip_remote_mta; |
---|
| 5218 | my($smtp_resp) = $r->recip_smtp_response; |
---|
| 5219 | my($qrecip_addr) = scalar(qquote_rfc2821_local($r->recip_addr)); |
---|
| 5220 | if ($r->recip_destiny == D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)) { |
---|
| 5221 | push(@succ_recips, $qrecip_addr); |
---|
| 5222 | } else { |
---|
| 5223 | push(@failed_recips, $qrecip_addr); |
---|
| 5224 | push(@failed_recips_full, |
---|
| 5225 | sprintf("%s:%s\n %s", $qrecip_addr, |
---|
| 5226 | ($remote_mta eq ''?'':" $remote_mta said:"), $smtp_resp)); |
---|
| 5227 | } |
---|
| 5228 | } |
---|
| 5229 | (\@succ_recips, \@failed_recips, \@failed_recips_full); |
---|
| 5230 | } |
---|
| 5231 | |
---|
| 5232 | # Build a new MIME::Entity object based on the original mail, but hopefully |
---|
| 5233 | # safer to mail readers: conventional mail header fields are retained, |
---|
| 5234 | # original mail becomes an attachment of type 'message/rfc822'. |
---|
| 5235 | # Text in $first_part becomes the first MIME part of type 'text/plain'. |
---|
| 5236 | # |
---|
| 5237 | sub defanged_mime_entity($$$) { |
---|
| 5238 | my($conn,$msginfo,$first_part) = @_; |
---|
| 5239 | my($new_entity); |
---|
| 5240 | my($resent_time) = $msginfo->rx_time; |
---|
| 5241 | $first_part = safe_encode(c('bdy_encoding'), $first_part); |
---|
| 5242 | # make sure _our_ source line number is reported in case of failure |
---|
| 5243 | my($nxmh) = c('notify_xmailer_header'); |
---|
| 5244 | eval {$new_entity = MIME::Entity->build( |
---|
| 5245 | Type => 'multipart/mixed', |
---|
| 5246 | (defined $nxmh && $nxmh eq '' ? () # leave the MIME::Entity default |
---|
| 5247 | : ('X-Mailer' => $nxmh) ), # X-Mailer hdr or undef |
---|
| 5248 | ); 1} or do {chomp($@); die $@}; |
---|
| 5249 | my($head) = $new_entity->head; |
---|
| 5250 | my($orig_head) = $msginfo->mime_entity->head; |
---|
| 5251 | # TODO: we should retain the ordering of Resent-* with their Received fields |
---|
| 5252 | for my $field_head ( # copy some of the original header fields |
---|
| 5253 | qw(Received From Sender To Cc Reply-To Date Message-ID |
---|
| 5254 | Resent-From Resent-Sender Resent-To Resent-Cc |
---|
| 5255 | Resent-Date Resent-Message-ID |
---|
| 5256 | In-Reply-To References Subject |
---|
| 5257 | Comments Keywords Organization X-Mailer) ) { |
---|
| 5258 | for my $value ($orig_head->get_all($field_head)) { |
---|
| 5259 | do_log(4, "copying-over the header field: $field_head"); |
---|
| 5260 | eval { $head->add($field_head, $value); 1 } or do {chomp($@); die $@}; |
---|
| 5261 | } |
---|
| 5262 | } |
---|
| 5263 | $head = undef; # object not needed any longer |
---|
| 5264 | eval {$new_entity->attach( |
---|
| 5265 | Type => 'text/plain', Encoding => '-SUGGEST', Charset => c('bdy_encoding'), |
---|
| 5266 | Data => $first_part); 1} or do {chomp($@); die $@}; |
---|
| 5267 | eval {$new_entity->attach( # rfc2046 |
---|
| 5268 | Type => 'message/rfc822; x-spam-type=original', |
---|
| 5269 | Encoding => '8bit', Path => $msginfo->mail_text_fn, |
---|
| 5270 | Description => 'Original message', |
---|
| 5271 | Filename => 'message.txt', Disposition => 'attachment'); 1} |
---|
| 5272 | or do {chomp($@); die $@}; |
---|
| 5273 | $new_entity; |
---|
| 5274 | } |
---|
| 5275 | |
---|
| 5276 | 1; |
---|
| 5277 | |
---|
| 5278 | # |
---|
| 5279 | package Amavis::Cache; |
---|
| 5280 | # offer an 'IPC::Cache'-compatible simple interface |
---|
| 5281 | # to a local (per-process) memory-based cache; |
---|
| 5282 | use strict; |
---|
| 5283 | use re 'taint'; |
---|
| 5284 | |
---|
| 5285 | BEGIN { |
---|
| 5286 | import Amavis::Util qw(ll do_log); |
---|
| 5287 | } |
---|
| 5288 | BEGIN { |
---|
| 5289 | use Exporter (); |
---|
| 5290 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 5291 | $VERSION = '2.0341'; |
---|
| 5292 | @ISA = qw(Exporter); |
---|
| 5293 | } |
---|
| 5294 | |
---|
| 5295 | # simple local memory-based cache |
---|
| 5296 | sub new { # called by each child process |
---|
| 5297 | my($class) = @_; |
---|
| 5298 | do_log(5,"BerkeleyDB not available, using memory-based local cache"); |
---|
| 5299 | bless {}, $class; |
---|
| 5300 | } |
---|
| 5301 | sub get { my($self,$key) = @_; thaw($self->{$key}) } |
---|
| 5302 | sub set { my($self,$key,$obj) = @_; $self->{$key} = freeze($obj) } |
---|
| 5303 | |
---|
| 5304 | # protect % and ~, as well as NUL and \200 for good measure |
---|
| 5305 | sub encode($) { |
---|
| 5306 | my($str) = @_; $str =~ s/[%~\000\200]/sprintf("%%%02X",ord($&))/egs; $str; |
---|
| 5307 | } |
---|
| 5308 | |
---|
| 5309 | # simple Storable::freeze lookalike |
---|
| 5310 | sub freeze($); # prototype |
---|
| 5311 | sub freeze($) { |
---|
| 5312 | my($obj) = @_; my($ty) = ref($obj); |
---|
| 5313 | if (!defined($obj)) { 'U' } |
---|
| 5314 | elsif (!$ty) { join('~', '', encode($obj)) } # string |
---|
| 5315 | elsif ($ty eq 'SCALAR') { join('~', 'S', encode(freeze($$obj))) } |
---|
| 5316 | elsif ($ty eq 'REF') { join('~', 'R', encode(freeze($$obj))) } |
---|
| 5317 | elsif ($ty eq 'ARRAY') { join('~', 'A', map {encode(freeze($_))} @$obj) } |
---|
| 5318 | elsif ($ty eq 'HASH') { |
---|
| 5319 | join('~','H',map {(encode($_),encode(freeze($obj->{$_})))} sort keys %$obj) |
---|
| 5320 | } else { die "Can't freeze object type $ty" } |
---|
| 5321 | } |
---|
| 5322 | |
---|
| 5323 | # simple Storable::thaw lookalike |
---|
| 5324 | sub thaw($); # prototype |
---|
| 5325 | sub thaw($) { |
---|
| 5326 | my($str) = @_; |
---|
| 5327 | return undef if !defined $str; |
---|
| 5328 | my($ty,@val) = split(/~/,$str,-1); |
---|
| 5329 | for (@val) { s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg } |
---|
| 5330 | if ($ty eq 'U') { undef } |
---|
| 5331 | elsif ($ty eq '') { $val[0] } |
---|
| 5332 | elsif ($ty eq 'S') { my($obj)=thaw($val[0]); \$obj } |
---|
| 5333 | elsif ($ty eq 'R') { my($obj)=thaw($val[0]); \$obj } |
---|
| 5334 | elsif ($ty eq 'A') { [map {thaw($_)} @val] } |
---|
| 5335 | elsif ($ty eq 'H') { |
---|
| 5336 | my($hr) = {}; |
---|
| 5337 | while (@val) { my($k) = shift @val; $hr->{$k} = thaw(shift @val) } |
---|
| 5338 | $hr; |
---|
| 5339 | } else { die "Can't thaw object type $ty" } |
---|
| 5340 | } |
---|
| 5341 | |
---|
| 5342 | 1; |
---|
| 5343 | |
---|
| 5344 | # |
---|
| 5345 | package Amavis; |
---|
| 5346 | require 5.005; # need qr operator and \z in regexps |
---|
| 5347 | use strict; |
---|
| 5348 | use re 'taint'; |
---|
| 5349 | |
---|
| 5350 | use POSIX qw(strftime); |
---|
| 5351 | use Errno qw(ENOENT EACCES); |
---|
| 5352 | use IO::File (); |
---|
| 5353 | # body digest for caching, either SHA1 or MD5 |
---|
| 5354 | #use Digest::SHA1; |
---|
| 5355 | use Digest::MD5; |
---|
| 5356 | use Net::Server 0.83; |
---|
| 5357 | use Net::Server::PreForkSimple; |
---|
| 5358 | |
---|
| 5359 | BEGIN { |
---|
| 5360 | import Amavis::Conf qw(:platform :sa :confvars c cr ca); |
---|
| 5361 | import Amavis::Util qw(untaint min max ll do_log sanitize_str debug_oneshot |
---|
| 5362 | am_id snmp_counters_init snmp_count prolong_timer); |
---|
| 5363 | import Amavis::Log; |
---|
| 5364 | import Amavis::Timing qw(section_time get_time_so_far); |
---|
| 5365 | import Amavis::rfc2821_2822_Tools; |
---|
| 5366 | import Amavis::Lookup qw(lookup lookup_ip_acl); |
---|
| 5367 | import Amavis::Out; |
---|
| 5368 | import Amavis::Out::EditHeader; |
---|
| 5369 | import Amavis::UnmangleSender qw(best_try_originator_ip best_try_originator |
---|
| 5370 | first_received_from); |
---|
| 5371 | import Amavis::Unpackers::Validity qw( |
---|
| 5372 | check_header_validity check_for_banned_names); |
---|
| 5373 | import Amavis::Unpackers::MIME qw(mime_decode); |
---|
| 5374 | import Amavis::Expand qw(expand); |
---|
| 5375 | import Amavis::Notify qw(delivery_status_notification delivery_short_report |
---|
| 5376 | string_to_mime_entity defanged_mime_entity); |
---|
| 5377 | import Amavis::In::Connection; |
---|
| 5378 | import Amavis::In::Message; |
---|
| 5379 | } |
---|
| 5380 | |
---|
| 5381 | # Make it a subclass of Net::Server::PreForkSimple |
---|
| 5382 | # to override method &process_request (and others if desired) |
---|
| 5383 | use vars qw(@ISA); |
---|
| 5384 | # @ISA = qw(Net::Server); |
---|
| 5385 | @ISA = qw(Net::Server::PreForkSimple); |
---|
| 5386 | |
---|
| 5387 | delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; |
---|
| 5388 | |
---|
| 5389 | use vars qw( |
---|
| 5390 | $extra_code_db $extra_code_cache |
---|
| 5391 | $extra_code_sql $extra_code_ldap |
---|
| 5392 | $extra_code_in_amcl $extra_code_in_smtp $extra_code_in_qmqpqq |
---|
| 5393 | $extra_code_antivirus $extra_code_antispam $extra_code_unpackers); |
---|
| 5394 | |
---|
| 5395 | use vars qw(%modules_basic); |
---|
| 5396 | use vars qw($spam_level $spam_status $spam_report); |
---|
| 5397 | use vars qw($user_id_sql $wb_listed_sql $implicit_maps_inserted); |
---|
| 5398 | use vars qw($db_env $snmp_db); |
---|
| 5399 | use vars qw($body_digest $body_digest_cache); |
---|
| 5400 | use vars qw(%builtins); # customizable notification messages |
---|
| 5401 | use vars qw($child_invocation_count $child_task_count); |
---|
| 5402 | # $child_invocation_count # counts child re-use from 1 to max_requests |
---|
| 5403 | # $child_task_count # counts check_mail() calls - this normally runs |
---|
| 5404 | # in sync with $child_invocation_count, but with |
---|
| 5405 | # SMTP or LMTP input there may be more than one |
---|
| 5406 | # message passed during a single SMTP session |
---|
| 5407 | use vars qw(@config_files); |
---|
| 5408 | use vars qw($VIRUSFILE $CONN $MSGINFO); |
---|
| 5409 | use vars qw($av_output @virusname @detecting_scanners |
---|
| 5410 | @banned_filename @bad_headers); |
---|
| 5411 | |
---|
| 5412 | use vars qw($amcl_in_obj $smtp_in_obj); # Amavis::In::AMCL and In::SMTP objects |
---|
| 5413 | use vars qw($qmqpqq_in_obj); # Amavis::In::QMQPqq object |
---|
| 5414 | use vars qw($sql_policy $sql_wblist); # Amavis::Lookup::SQL objects |
---|
| 5415 | use vars qw($ldap_policy); # Amavis::Lookup::LDAP objects |
---|
| 5416 | |
---|
| 5417 | # initialize some remaining global variables; |
---|
| 5418 | # invoked after chroot and after privileges have been dropped |
---|
| 5419 | sub after_chroot_init() { |
---|
| 5420 | $child_invocation_count = $child_task_count = 0; |
---|
| 5421 | %modules_basic = %INC; # helps to track missing modules in chroot |
---|
| 5422 | my(@msg); |
---|
| 5423 | my($euid) = $>; # effective UID |
---|
| 5424 | $> = 0; # try to become root |
---|
| 5425 | POSIX::setuid(0) if $> != 0; # and try some more |
---|
| 5426 | if ($> == 0) { # succeded? panic! |
---|
| 5427 | @msg = ("It is possible to change EUID from $euid to root, ABORTING!", |
---|
| 5428 | "Perhaps you forgot to patch the Net::Server - see:", |
---|
| 5429 | " http://www.ijs.si/software/amavisd/#net-server-sec", |
---|
| 5430 | "or start as non-root, e.g. by su(1) or using option -u user"); |
---|
| 5431 | } elsif ($daemon_chroot_dir eq '') { |
---|
| 5432 | # A quick check on vulnerability/protection of a config file |
---|
| 5433 | # (non-exhaustive: doesn't test for symlink tricks and higher directories). |
---|
| 5434 | # The config file has already been executed by now, so it may be |
---|
| 5435 | # too late to feel sorry now, but better late then never. |
---|
| 5436 | for my $config_file (@config_files) { |
---|
| 5437 | my($fh) = IO::File->new; |
---|
| 5438 | my($errn) = lstat($config_file) ? 0 : 0+$!; |
---|
| 5439 | if ($errn) { # not accessible, don't bother to test further |
---|
| 5440 | } elsif ($fh->open($config_file,'+<')) { |
---|
| 5441 | push(@msg, "Config file \"$config_file\" is writable, ". |
---|
| 5442 | "UID $<, EUID $>, EGID $)" ); |
---|
| 5443 | $fh->close; # close, ignoring status |
---|
| 5444 | } elsif (rename($config_file, $config_file.'.moved')) { |
---|
| 5445 | my($m) = 'appears writable (unconfirmed)'; |
---|
| 5446 | if (!-e $config_file && -e $config_file.'.moved') { |
---|
| 5447 | rename($config_file.'.moved', $config_file); # try to rename back |
---|
| 5448 | $m = 'is writable (confirmed)'; |
---|
| 5449 | } |
---|
| 5450 | push(@msg, "Directory of a config file \"$config_file\" $m, ". |
---|
| 5451 | "UID $<, EUID $>, EGID $)" ); |
---|
| 5452 | } |
---|
| 5453 | last if @msg; |
---|
| 5454 | } |
---|
| 5455 | } |
---|
| 5456 | if (@msg) { |
---|
| 5457 | do_log(-3,"FATAL: $_") for @msg; |
---|
| 5458 | print STDERR (map {"$_\n"} @msg); |
---|
| 5459 | die "SECURITY PROBLEM, ABORTING"; |
---|
| 5460 | exit 1; # just in case |
---|
| 5461 | } |
---|
| 5462 | # report versions of some modules |
---|
| 5463 | for my $m ('Amavis::Conf', |
---|
| 5464 | sort map { s/\.pm\z//; s[/][::]g; $_ } grep { /\.pm\z/ } keys %INC){ |
---|
| 5465 | next if !grep { $_ eq $m } qw(Amavis::Conf |
---|
| 5466 | Archive::Tar Archive::Zip Compress::Zlib Convert::TNEF Convert::UUlib |
---|
| 5467 | MIME::Entity MIME::Parser MIME::Tools Mail::Header Mail::Internet |
---|
| 5468 | Mail::ClamAV Mail::SpamAssassin Mail::SpamAssassin::SpamCopURI URI |
---|
| 5469 | Razor2::Client::Version Mail::SPF::Query Authen::SASL |
---|
| 5470 | Net::DNS Net::SMTP Net::Cmd Net::Server Net::LDAP |
---|
| 5471 | DBI BerkeleyDB DB_File SAVI Unix::Syslog Time::HiRes); |
---|
| 5472 | do_log(0, sprintf("Module %-19s %s", $m, $m->VERSION || '?')); |
---|
| 5473 | } |
---|
| 5474 | if (c('forward_method') eq '' && $extra_code_in_smtp) { |
---|
| 5475 | do_log(1,"forward_method in default policy bank is null (milter setup?), ". |
---|
| 5476 | "DISABLING SMTP-in AS A PRECAUTION"); |
---|
| 5477 | $extra_code_in_smtp = undef; |
---|
| 5478 | } |
---|
| 5479 | do_log(0,"Amavis::DB code ".($extra_code_db ?'':" NOT")." loaded"); |
---|
| 5480 | do_log(0,"Amavis::Cache code ".($extra_code_cache ?'':" NOT")." loaded"); |
---|
| 5481 | do_log(0,"Lookup::SQL code ".($extra_code_sql ?'':" NOT")." loaded"); |
---|
| 5482 | do_log(0,"Lookup::LDAP code ".($extra_code_ldap ?'':" NOT")." loaded"); |
---|
| 5483 | do_log(0,"AMCL-in protocol code ".($extra_code_in_amcl?'':" NOT")." loaded"); |
---|
| 5484 | do_log(0,"SMTP-in protocol code ".($extra_code_in_smtp?'':" NOT")." loaded"); |
---|
| 5485 | do_log(0,"QMQPqq-in protocol code ".($extra_code_in_qmqpqq?'':" NOT")." loaded"); |
---|
| 5486 | do_log(0,"ANTI-VIRUS code ".($extra_code_antivirus?'':" NOT")." loaded"); |
---|
| 5487 | do_log(0,"ANTI-SPAM code ".($extra_code_antispam ?'':" NOT")." loaded"); |
---|
| 5488 | do_log(0,"Unpackers code ".($extra_code_unpackers?'':" NOT")." loaded"); |
---|
| 5489 | |
---|
| 5490 | # Prepare a hash of macros to be used in notification message expansion. |
---|
| 5491 | # A key (macro name) must be a single character. Most characters are |
---|
| 5492 | # allowed, but to be on the safe side and for clarity it is suggested |
---|
| 5493 | # that only letters are used. Upper case letters may (as a mnemonic) |
---|
| 5494 | # suggest the value is an array, lower case may suggest the value is |
---|
| 5495 | # a scalar string - but this is only a convention and not enforced. |
---|
| 5496 | # |
---|
| 5497 | # A value may be a reference to a subroutine which will be called later at |
---|
| 5498 | # the time of macro expansion. This way we can provide a method for obtaining |
---|
| 5499 | # information which is not yet available, such as AV scanner results, |
---|
| 5500 | # or provide a lazy evaluation for more expensive calculations. |
---|
| 5501 | # Subroutine will be called in scalar context with no arguments. |
---|
| 5502 | # It may return a scalar string (or undef), or an array reference. |
---|
| 5503 | %builtins = ( |
---|
| 5504 | '.' => undef, |
---|
| 5505 | p => sub {c('policy_bank_path')}, |
---|
| 5506 | # mail reception timestamp (e.g. start of SMTP transaction): |
---|
| 5507 | d => sub {rfc2822_timestamp($MSGINFO->rx_time)}, # rfc2822 local date-time |
---|
| 5508 | # U => sub {iso8601_timestamp($MSGINFO->rx_time)}, # iso8601, local time |
---|
| 5509 | U => sub {iso8601_utc_timestamp($MSGINFO->rx_time)}, # iso8601 UTC |
---|
| 5510 | y => sub {sprintf("%.0f", 1000*get_time_so_far())}, # elapsed time in ms |
---|
| 5511 | u => sub {sprintf("%010d",$MSGINFO->rx_time)}, # s since Unix epoch (UTC) |
---|
| 5512 | |
---|
| 5513 | h => $myhostname, # dns name of this host, or configurable name |
---|
| 5514 | l => sub {my($ip) = $MSGINFO->client_addr; my($val); |
---|
| 5515 | $val = $ip ne '' ? lookup_ip_acl($ip,@{ca('mynetworks_maps')}) |
---|
| 5516 | : lookup(0,$MSGINFO->sender_source, |
---|
| 5517 | @{ca('local_domains_maps')}); |
---|
| 5518 | $val ? 1 : undef}, # sender's client IP (if known) from @mynetworks |
---|
| 5519 | # (if IP is known), or sender domain is local |
---|
| 5520 | s => sub {qquote_rfc2821_local($MSGINFO->sender)}, # original envelope sender in <> |
---|
| 5521 | S => sub { # unmangled sender or sender address to be notified, or empty... |
---|
| 5522 | sanitize_str($MSGINFO->sender_contact) }, # ..if sender unknown |
---|
| 5523 | o => sub { # best attempt at determining true sender (origin) of the virus, |
---|
| 5524 | sanitize_str($MSGINFO->sender_source) }, # normally same as %s |
---|
| 5525 | R => sub {$MSGINFO->recips}, # original message recipients list |
---|
| 5526 | D => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $y}, # succ.delivered |
---|
| 5527 | O => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $n}, # failed recips |
---|
| 5528 | N => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $f}, # short dsn |
---|
| 5529 | Q => sub {$MSGINFO->queue_id}, # MTA queue ID of the message if known |
---|
| 5530 | m => sub { local($_) = $MSGINFO->mime_entity; # Message-ID of the message |
---|
| 5531 | if (defined) { $_ = $_->head->get('Message-ID',0); chomp; |
---|
| 5532 | s/^[ \t]+//; s/[ \t\n]+\z//; # trim whitespace |
---|
| 5533 | # protect space and \n, other special characters |
---|
| 5534 | # will be sanitized before logging |
---|
| 5535 | s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }}, |
---|
| 5536 | r => sub { local($_) = $MSGINFO->mime_entity; # first Resent-Message-ID |
---|
| 5537 | if (defined) { $_ = $_->head->get('Resent-Message-ID',0); chomp; |
---|
| 5538 | s/^[ \t]+//; s/[ \t\n]+\z//; # trim whitespace |
---|
| 5539 | s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }}, |
---|
| 5540 | j => sub { local($_) = $MSGINFO->mime_entity; # Subject of the message |
---|
| 5541 | if (defined) { $_ = $_->head->get('Subject',0); chomp; |
---|
| 5542 | s/\n([ \t])/$1/g; # unfold |
---|
| 5543 | s{([=\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }}, |
---|
| 5544 | b => sub {$MSGINFO->body_digest}, # original message body digest |
---|
| 5545 | n => \&am_id, # amavis internal message id (for log entries) |
---|
| 5546 | i => sub {$VIRUSFILE}, # some quarantine id, e.g. quarantine filename |
---|
| 5547 | q => sub {my($q) = $MSGINFO->quarantined_to; |
---|
| 5548 | !defined($q) ? undef : |
---|
| 5549 | [map { my($m)=$_; $m=~s{^\Q$QUARANTINEDIR\E/}{}; $m } @$q]; |
---|
| 5550 | }, # list of quarantine mailboxes |
---|
| 5551 | v => sub {[split(/[ \t]*\r?\n/,$av_output)]}, # anti-virus scanner output |
---|
| 5552 | V => sub {my(%seen); [grep {!$seen{$_}++} @virusname]}, #unique virus names |
---|
| 5553 | F => sub {@banned_filename<=1 ? \@banned_filename |
---|
| 5554 | : [$banned_filename[0], '...'] }, # list of banned file names |
---|
| 5555 | X => sub {\@bad_headers}, # list of header syntax violations |
---|
| 5556 | W => sub {\@detecting_scanners}, # list of av scanners detecting a virus |
---|
| 5557 | H => sub {[map {my $h=$_; chomp($h); $h} @{$MSGINFO->orig_header}]},# orig hdr |
---|
| 5558 | A => sub {[split(/\r?\n/, $spam_report)]}, # SpamAssassin report lines |
---|
| 5559 | c => sub {!defined $spam_level ? '-' # SA hits/score |
---|
| 5560 | : 0+sprintf("%.3f",$spam_level+min(map {$_->recip_score_boost} |
---|
| 5561 | @{$MSGINFO->per_recip_data}))}, |
---|
| 5562 | z => sub {$MSGINFO->orig_body_size+1+$MSGINFO->orig_header_size}, # mail size |
---|
| 5563 | t => sub { # first entry in the Received trace |
---|
| 5564 | sanitize_str(first_received_from($MSGINFO->mime_entity)) }, |
---|
| 5565 | e => sub { # first valid public IP in the Received trace |
---|
| 5566 | sanitize_str(best_try_originator_ip($MSGINFO->mime_entity)) }, |
---|
| 5567 | a => sub {$MSGINFO->client_addr}, # original SMTP session client IP address |
---|
| 5568 | g => sub { # original SMTP session client DNS name |
---|
| 5569 | sanitize_str($MSGINFO->client_name) }, |
---|
| 5570 | k => sub { my($kill_level); |
---|
| 5571 | scalar(grep # any recipient declared the message be killed ? |
---|
| 5572 | { !$_->recip_whitelisted_sender && |
---|
| 5573 | ($_->recip_blacklisted_sender || |
---|
| 5574 | ($kill_level=lookup(0,$_->recip_addr, |
---|
| 5575 | @{ca('spam_kill_level_maps')}), |
---|
| 5576 | defined $spam_level && defined $kill_level && |
---|
| 5577 | $spam_level + $_->recip_score_boost >= $kill_level) ) |
---|
| 5578 | } @{$MSGINFO->per_recip_data}) }, |
---|
| 5579 | '1'=> sub { my($tag_level); |
---|
| 5580 | scalar(grep # above tag level for any recipient? |
---|
| 5581 | { !$_->recip_whitelisted_sender && |
---|
| 5582 | ($_->recip_blacklisted_sender || |
---|
| 5583 | ($tag_level=lookup(0,$_->recip_addr, |
---|
| 5584 | @{ca('spam_tag_level_maps')}), |
---|
| 5585 | defined $spam_level && defined $tag_level && |
---|
| 5586 | $spam_level + $_->recip_score_boost >= $tag_level) ) |
---|
| 5587 | } @{$MSGINFO->per_recip_data}) }, |
---|
| 5588 | '2'=> sub { my($tag2_level); |
---|
| 5589 | scalar(grep # above tag2 level for any recipient? |
---|
| 5590 | { !$_->recip_whitelisted_sender && |
---|
| 5591 | ($_->recip_blacklisted_sender || |
---|
| 5592 | ($tag2_level=lookup(0,$_->recip_addr, |
---|
| 5593 | @{ca('spam_tag2_level_maps')}), |
---|
| 5594 | defined $spam_level && defined $tag2_level && |
---|
| 5595 | $spam_level + $_->recip_score_boost >= $tag2_level) ) |
---|
| 5596 | } @{$MSGINFO->per_recip_data}) }, |
---|
| 5597 | # macros f, T, C, B will be defined for each notification as appropriate |
---|
| 5598 | # (representing From:, To:, Cc:, and Bcc: respectively) |
---|
| 5599 | # remaining free letters: wxyEGIJKLMPYZ |
---|
| 5600 | ); |
---|
| 5601 | |
---|
| 5602 | # Map local virtual 'localpart' to a mailbox (e.g. to a quarantine filename |
---|
| 5603 | # or a directory). Used by method 'local:', i.e. in mail_to_local_mailbox(), |
---|
| 5604 | # for direct local quarantining. The hash value may be a ref to a pair of |
---|
| 5605 | # fixed strings, or a subroutine ref (which must return a pair of strings |
---|
| 5606 | # (a list, not a list ref)) which makes possible lazy evaluation |
---|
| 5607 | # when some part of the pair is not known before the final delivery time. |
---|
| 5608 | # |
---|
| 5609 | # The first string in a pair must be either: |
---|
| 5610 | # - empty or undef, which will disable saving the message, |
---|
| 5611 | # - a filename, indicating a Unix-style mailbox, |
---|
| 5612 | # - a directory name, indicating a maildir-style mailbox, |
---|
| 5613 | # in which case the second string may provide a suggested file name. |
---|
| 5614 | # |
---|
| 5615 | %Amavis::Conf::local_delivery_aliases = ( |
---|
| 5616 | 'virus-quarantine' => sub { ($QUARANTINEDIR, $VIRUSFILE) }, |
---|
| 5617 | 'banned-quarantine' => sub { ($QUARANTINEDIR, $VIRUSFILE) }, |
---|
| 5618 | 'bad-header-quarantine'=>sub { ($QUARANTINEDIR, $VIRUSFILE) }, |
---|
| 5619 | # 'spam-quarantine' => sub { ($QUARANTINEDIR, $VIRUSFILE) }, # normal |
---|
| 5620 | 'spam-quarantine' => sub { ($QUARANTINEDIR, "$VIRUSFILE.gz") }, # gzipped |
---|
| 5621 | |
---|
| 5622 | # some more examples: |
---|
| 5623 | 'sender-quarantine' => |
---|
| 5624 | sub { my($s) = $MSGINFO->sender; local($1); |
---|
| 5625 | $s =~ s/[^a-zA-Z0-9._@]/=/g; $s =~ s/\@/%/g; |
---|
| 5626 | $s = untaint($s) if $s =~ /^([a-zA-Z0-9._=%]+)\z/; # untaint |
---|
| 5627 | $s =~ s/%/%%/g; # protect % |
---|
| 5628 | ( $QUARANTINEDIR, "sender-$s-%i-%n.gz" ); # suggested file name |
---|
| 5629 | }, |
---|
| 5630 | 'recip-quarantine' => |
---|
| 5631 | sub { ("$QUARANTINEDIR/recip-archive.mbox", undef) }, |
---|
| 5632 | 'ham-quarantine' => |
---|
| 5633 | sub { ("$QUARANTINEDIR/ham.mbox", undef) }, |
---|
| 5634 | 'outgoing-quarantine' => |
---|
| 5635 | sub { ("$QUARANTINEDIR/outgoing.mbox", undef) }, |
---|
| 5636 | 'incoming-quarantine' => |
---|
| 5637 | sub { ("$QUARANTINEDIR/incoming.mbox", undef) }, |
---|
| 5638 | ); |
---|
| 5639 | # store policy names into 'policy_bank_name' fields, if not explicitly set |
---|
| 5640 | for my $name (keys %policy_bank) { |
---|
| 5641 | if (ref($policy_bank{$name}) eq 'HASH' && |
---|
| 5642 | !exists($policy_bank{$name}{'policy_bank_name'})) { |
---|
| 5643 | $policy_bank{$name}{'policy_bank_name'} = $name; |
---|
| 5644 | $policy_bank{$name}{'policy_bank_path'} = $name; |
---|
| 5645 | } |
---|
| 5646 | } |
---|
| 5647 | }; |
---|
| 5648 | |
---|
| 5649 | # overlay the current policy bank by settings from the |
---|
| 5650 | # $policy_bank{$policy_bank_name}, or load the default policy bank (empty name) |
---|
| 5651 | sub load_policy_bank($) { |
---|
| 5652 | my($policy_bank_name) = @_; |
---|
| 5653 | if (!exists $policy_bank{$policy_bank_name}) { |
---|
| 5654 | do_log(-1,"policy bank \"$policy_bank_name\" does not exist, ignored"); |
---|
| 5655 | } elsif ($policy_bank_name eq '') { |
---|
| 5656 | %current_policy_bank = %{$policy_bank{$policy_bank_name}}; |
---|
| 5657 | do_log(4,'loaded base policy bank'); |
---|
| 5658 | } else { |
---|
| 5659 | my($cpbp) = c('policy_bank_path'); # currently loaded bank |
---|
| 5660 | for my $k (keys %{$policy_bank{$policy_bank_name}}) { |
---|
| 5661 | do_log(-1,"loading policy bank \"$policy_bank_name\": ". |
---|
| 5662 | "unknown field \"$k\"") if !exists $current_policy_bank{$k}; |
---|
| 5663 | $current_policy_bank{$k} = $policy_bank{$policy_bank_name}{$k}; |
---|
| 5664 | } |
---|
| 5665 | $current_policy_bank{'policy_bank_path'} = |
---|
| 5666 | ($cpbp eq '' ? '' : $cpbp.'/') . $policy_bank_name; |
---|
| 5667 | do_log(2,sprintf('loaded policy bank "%s"%s', $policy_bank_name, |
---|
| 5668 | $cpbp eq '' ? '' : " over \"$cpbp\"")); |
---|
| 5669 | } |
---|
| 5670 | } |
---|
| 5671 | |
---|
| 5672 | ### Net::Server hook |
---|
| 5673 | ### This hook occurs after chroot, change of user, and change of group has |
---|
| 5674 | ### occured. It allows for preparation before looping begins. |
---|
| 5675 | sub pre_loop_hook { |
---|
| 5676 | my($self) = @_; |
---|
| 5677 | local $SIG{CHLD} = 'DEFAULT'; |
---|
| 5678 | eval { |
---|
| 5679 | after_chroot_init(); # the rest of the top-level initialization |
---|
| 5680 | |
---|
| 5681 | # this needs to be done only after chroot, otherwise paths will be wrong |
---|
| 5682 | find_external_programs([split(/:/, $path, -1)]); |
---|
| 5683 | # do some sanity checking |
---|
| 5684 | my($name) = $TEMPBASE; |
---|
| 5685 | $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne ''; |
---|
| 5686 | my($errn) = stat($TEMPBASE) ? 0 : 0+$!; |
---|
| 5687 | if ($errn==ENOENT) { die "No TEMPBASE directory: $name" } |
---|
| 5688 | elsif ($errn) { die "TEMPBASE directory inaccessible, $!: $name" } |
---|
| 5689 | elsif (!-d _) { die "TEMPBASE is not a directory: $name" } |
---|
| 5690 | elsif (!-w _) { die "TEMPBASE directory is not writable: $name" } |
---|
| 5691 | if ($enable_global_cache && $extra_code_db) { |
---|
| 5692 | my($name) = $db_home; |
---|
| 5693 | $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne ''; |
---|
| 5694 | $errn = stat($db_home) ? 0 : 0+$!; |
---|
| 5695 | if ($errn == ENOENT) { |
---|
| 5696 | die "Please create an empty directory $name to hold a database". |
---|
| 5697 | " (config variable \$db_home)\n" } |
---|
| 5698 | elsif ($errn) { die "db_home inaccessible, $!: $name" } |
---|
| 5699 | elsif (!-d _) { die "db_home is not a directory : $name" } |
---|
| 5700 | elsif (!-w _) { die "db_home directory is not writable: $name" } |
---|
| 5701 | Amavis::DB::init(1); |
---|
| 5702 | } |
---|
| 5703 | if ($QUARANTINEDIR ne '') { |
---|
| 5704 | my($name) = $QUARANTINEDIR; |
---|
| 5705 | $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne ''; |
---|
| 5706 | $errn = stat($QUARANTINEDIR) ? 0 : 0+$!; |
---|
| 5707 | if ($errn == ENOENT) { } # ok |
---|
| 5708 | elsif ($errn) { die "QUARANTINEDIR inaccessible, $!: $name" } |
---|
| 5709 | elsif (-d _ && !-w _) { die "QUARANTINEDIR directory not writable: $name" } |
---|
| 5710 | } |
---|
| 5711 | Amavis::SpamControl::init() if $extra_code_antispam; |
---|
| 5712 | }; |
---|
| 5713 | if ($@ ne '') { |
---|
| 5714 | chomp($@); my($msg) = "TROUBLE in pre_loop_hook: $@"; do_log(-2,$msg); |
---|
| 5715 | die ("Suicide (" . am_id() . ") " . $msg . "\n"); # kills child, not parent |
---|
| 5716 | } |
---|
| 5717 | 1; |
---|
| 5718 | } |
---|
| 5719 | |
---|
| 5720 | ### log routine Net::Server hook |
---|
| 5721 | ### (Sys::Syslog MUST NOT be specified as a value of 'log_file'!) |
---|
| 5722 | # |
---|
| 5723 | # Redirect Net::Server logging to use Amavis' do_log(). |
---|
| 5724 | # The main reason is that Net::Server uses Sys::Syslog |
---|
| 5725 | # (and has two bugs in doing it, at least the Net-Server-0.82), |
---|
| 5726 | # and Amavis users are acustomed to Unix::Syslog. |
---|
| 5727 | sub write_to_log_hook { |
---|
| 5728 | my($self,$level,$msg) = @_; |
---|
| 5729 | my($prop) = $self->{server}; |
---|
| 5730 | local $SIG{CHLD} = 'DEFAULT'; |
---|
| 5731 | chomp($msg); |
---|
| 5732 | do_log(1, "Net::Server: " . $msg); # just call Amavis' traditional logging |
---|
| 5733 | 1; |
---|
| 5734 | } |
---|
| 5735 | |
---|
| 5736 | ### user customizable Net::Server hook |
---|
| 5737 | sub child_init_hook { |
---|
| 5738 | my($self) = @_; |
---|
| 5739 | local $SIG{CHLD} = 'DEFAULT'; |
---|
| 5740 | $0 = 'amavisd (virgin child)'; |
---|
| 5741 | eval { |
---|
| 5742 | $db_env = $snmp_db = $body_digest_cache = undef; # just in case |
---|
| 5743 | Amavis::Timing::init(); snmp_counters_init(); |
---|
| 5744 | if ($extra_code_db) { |
---|
| 5745 | $db_env = Amavis::DB->new; # get access to a bdb environment |
---|
| 5746 | $snmp_db = Amavis::DB::SNMP->new($db_env); |
---|
| 5747 | $snmp_db->register_proc('') if defined $snmp_db; # process alive & idle |
---|
| 5748 | } |
---|
| 5749 | # if $db_env is undef the Amavis::Cache::new creates a memory-based cache |
---|
| 5750 | $body_digest_cache = Amavis::Cache->new($db_env); |
---|
| 5751 | if ($extra_code_db) { # is it worth reporting the timing? |
---|
| 5752 | section_time('bdb-open'); |
---|
| 5753 | do_log(2, Amavis::Timing::report()); # report elapsed times |
---|
| 5754 | } |
---|
| 5755 | }; |
---|
| 5756 | if ($@ ne '') { |
---|
| 5757 | chomp($@); do_log(-2, "TROUBLE in child_init_hook: $@"); |
---|
| 5758 | die "Suicide in child_init_hook: $@\n"; |
---|
| 5759 | } |
---|
| 5760 | Amavis::Timing::go_idle('vir'); |
---|
| 5761 | } |
---|
| 5762 | |
---|
| 5763 | ### user customizable Net::Server hook |
---|
| 5764 | sub post_accept_hook { |
---|
| 5765 | my($self) = @_; |
---|
| 5766 | local $SIG{CHLD} = 'DEFAULT'; |
---|
| 5767 | $child_invocation_count++; |
---|
| 5768 | $0 = sprintf("amavisd (ch%d-accept)", $child_invocation_count); |
---|
| 5769 | Amavis::Timing::go_busy('hi '); |
---|
| 5770 | # establish initial time right after 'accept' |
---|
| 5771 | Amavis::Timing::init(); snmp_counters_init(); |
---|
| 5772 | $snmp_db->register_proc('A') if defined $snmp_db; # in 'accept' state |
---|
| 5773 | load_policy_bank(''); # start with a builting policy bank |
---|
| 5774 | } |
---|
| 5775 | |
---|
| 5776 | ### user customizable Net::Server hook |
---|
| 5777 | ### if this hook returns 1 the request is processed |
---|
| 5778 | ### if this hook returns 0 the request is denied |
---|
| 5779 | sub allow_deny_hook { |
---|
| 5780 | my($self) = @_; |
---|
| 5781 | local($1,$2,$3,$4); # Perl bug: $1 and $2 come tainted from Net::Server ! |
---|
| 5782 | local $SIG{CHLD} = 'DEFAULT'; |
---|
| 5783 | my($prop) = $self->{server}; my($sock) = $prop->{client}; my($bank_name); |
---|
| 5784 | my($is_ux) = UNIVERSAL::can($sock,'NS_proto') && $sock->NS_proto eq 'UNIX'; |
---|
| 5785 | if ($is_ux) { |
---|
| 5786 | $bank_name = $interface_policy{"SOCK"}; # possibly undef |
---|
| 5787 | } else { |
---|
| 5788 | my($myif,$myport) = ($prop->{sockaddr}, $prop->{sockport}); |
---|
| 5789 | if (defined $interface_policy{"$myif:$myport"}) { |
---|
| 5790 | $bank_name = $interface_policy{"$myif:$myport"}; |
---|
| 5791 | } elsif (defined $interface_policy{$myport}) { |
---|
| 5792 | $bank_name = $interface_policy{$myport}; |
---|
| 5793 | } |
---|
| 5794 | } |
---|
| 5795 | load_policy_bank($bank_name) if defined $bank_name && |
---|
| 5796 | $bank_name ne c('policy_bank_name'); |
---|
| 5797 | # note that the new policy bank may have replaced the inet_acl access table |
---|
| 5798 | if ($is_ux) { |
---|
| 5799 | # always permit access - unix sockets are immune to this check |
---|
| 5800 | } else { |
---|
| 5801 | my($permit,$fullkey) = lookup_ip_acl($prop->{peeraddr},ca('inet_acl')); |
---|
| 5802 | if (!$permit) { |
---|
| 5803 | my($msg) = sprintf("DENIED ACCESS from IP %s, policy bank '%s'", |
---|
| 5804 | $prop->{peeraddr}, c('policy_bank_name') ); |
---|
| 5805 | $msg .= ", blocked by rule $fullkey" if defined $fullkey; |
---|
| 5806 | do_log(-1,$msg); |
---|
| 5807 | return 0; |
---|
| 5808 | } |
---|
| 5809 | } |
---|
| 5810 | 1; |
---|
| 5811 | } |
---|
| 5812 | |
---|
| 5813 | # use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC); |
---|
| 5814 | # sub cloexec_on($;$) { |
---|
| 5815 | # my($fd,$name) = @_; my($flags); |
---|
| 5816 | # $flags = fcntl($fd, F_GETFD, 0) |
---|
| 5817 | # or die "Can't get flags from the file descriptor: $!"; |
---|
| 5818 | # if ($flags & FD_CLOEXEC == 0) { |
---|
| 5819 | # do_log(4,"Turning on FD_CLOEXEC flag on $name"); |
---|
| 5820 | # fcntl($fd, F_SETFD, $flags | FD_CLOEXEC) |
---|
| 5821 | # or die "Can't set FD_CLOEXEC on file descriptor $name: $!"; |
---|
| 5822 | # } |
---|
| 5823 | # } |
---|
| 5824 | |
---|
| 5825 | ### The heart of the program |
---|
| 5826 | ### user customizable Net::Server hook |
---|
| 5827 | sub process_request { |
---|
| 5828 | my($self) = shift; |
---|
| 5829 | my($prop) = $self->{server}; my($sock) = $prop->{client}; |
---|
| 5830 | local $SIG{CHLD} = 'DEFAULT'; |
---|
| 5831 | local($1,$2,$3,$4); # Perl bug: $1 and $2 come tainted from Net::Server ! |
---|
| 5832 | # Net::Server assigns STDIN and STDOUT to the socket |
---|
| 5833 | binmode(STDIN) or die "Can't set STDIN to binmode: $!"; |
---|
| 5834 | binmode(STDOUT) or die "Can't set STDOUT to binmode: $!"; |
---|
| 5835 | binmode($sock) or die "Can't set socket to binmode: $!"; |
---|
| 5836 | $| = 1; |
---|
| 5837 | local $SIG{ALRM} = sub { die "timed out\n" }; # do not modify the sig text! |
---|
| 5838 | eval { |
---|
| 5839 | # if ($] < 5.006) { # Perl older than 5.6.0 did not set FD_CLOEXEC on sockets |
---|
| 5840 | # for my $mysock (@{$prop->{sock}}) { cloexec_on($mysock, $mysock) } |
---|
| 5841 | # } |
---|
| 5842 | prolong_timer('new request - timer reset', $child_timeout); # timer init |
---|
| 5843 | if ($extra_code_ldap && !defined $ldap_policy) { |
---|
| 5844 | # make LDAP lookup object |
---|
| 5845 | $ldap_policy = Amavis::Lookup::LDAP->new($default_ldap); |
---|
| 5846 | } |
---|
| 5847 | if (defined $ldap_policy && !$implicit_maps_inserted) { |
---|
| 5848 | # make SQL field lookup objects with incorporated field names |
---|
| 5849 | # fieldtype: B=boolean, N=numeric, S=string, L=list |
---|
| 5850 | # B-, N-, S-, L- returns undef if field does not exist |
---|
| 5851 | # B0: boolean, nonexistent field treated as false, |
---|
| 5852 | # B1: boolean, nonexistent field treated as true |
---|
| 5853 | my $lf = sub{Amavis::Lookup::LDAPattr->new($ldap_policy,@_)}; |
---|
| 5854 | unshift(@Amavis::Conf::virus_lovers_maps, $lf->('amavisVirusLover', 'B-')); |
---|
| 5855 | unshift(@Amavis::Conf::spam_lovers_maps, $lf->('amavisSpamLover', 'B-')); |
---|
| 5856 | unshift(@Amavis::Conf::banned_files_lovers_maps, $lf->('amavisBannedFilesLover', 'B-')); |
---|
| 5857 | unshift(@Amavis::Conf::bad_header_lovers_maps, $lf->('amavisBadHeaderLover', 'B-')); |
---|
| 5858 | unshift(@Amavis::Conf::bypass_virus_checks_maps, $lf->('amavisBypassVirusChecks', 'B-')); |
---|
| 5859 | unshift(@Amavis::Conf::bypass_spam_checks_maps, $lf->('amavisBypassSpamChecks', 'B-')); |
---|
| 5860 | unshift(@Amavis::Conf::bypass_banned_checks_maps,$lf->('amavisBypassBannedChecks', 'B-')); |
---|
| 5861 | unshift(@Amavis::Conf::bypass_header_checks_maps,$lf->('amavisBypassHeaderChecks', 'B-')); |
---|
| 5862 | unshift(@Amavis::Conf::spam_tag_level_maps, $lf->('amavisSpamTagLevel', 'N')); |
---|
| 5863 | unshift(@Amavis::Conf::spam_tag2_level_maps, $lf->('amavisSpamTag2Level', 'N')); |
---|
| 5864 | unshift(@Amavis::Conf::spam_kill_level_maps, $lf->('amavisSpamKillLevel', 'N')); |
---|
| 5865 | unshift(@Amavis::Conf::spam_modifies_subj_maps, $lf->('amavisSpamModifiesSubj', 'B-')); |
---|
| 5866 | unshift(@Amavis::Conf::message_size_limit_maps, $lf->('amavisMessageSizeLimit', 'N-')); |
---|
| 5867 | unshift(@Amavis::Conf::virus_quarantine_to_maps, $lf->('amavisVirusQuarantineTo', 'S-')); |
---|
| 5868 | unshift(@Amavis::Conf::spam_quarantine_to_maps, $lf->('amavisSpamQuarantineTo', 'S-')); |
---|
| 5869 | unshift(@Amavis::Conf::banned_quarantine_to_maps, $lf->('amavisBannedQuarantineTo','S-')); |
---|
| 5870 | unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $lf->('amavisBadHeaderQuarantineTo', 'S-')); |
---|
| 5871 | unshift(@Amavis::Conf::local_domains_maps, $lf->('amavisLocal', |
---|
| 5872 | 'B1')); |
---|
| 5873 | unshift(@Amavis::Conf::warnvirusrecip_maps, $lf->('amavisWarnVirusRecip', 'B-')); |
---|
| 5874 | unshift(@Amavis::Conf::warnbannedrecip_maps, $lf->('amavisWarnBannedRecip', 'B-')); |
---|
| 5875 | unshift(@Amavis::Conf::warnbadhrecip_maps, $lf->('amavisWarnBadHeaderRecip', 'B-')); |
---|
| 5876 | section_time('ldap-prepare'); |
---|
| 5877 | } |
---|
| 5878 | if ($extra_code_sql && @lookup_sql_dsn) { |
---|
| 5879 | # make SQL lookup objects which will hold SELECT and DBI handles |
---|
| 5880 | $sql_wblist = Amavis::Lookup::SQL->new |
---|
| 5881 | if !defined $sql_wblist && defined $sql_select_white_black_list; |
---|
| 5882 | $sql_policy = Amavis::Lookup::SQL->new |
---|
| 5883 | if !defined $sql_policy && defined $sql_select_policy; |
---|
| 5884 | } |
---|
| 5885 | if (defined $sql_policy && !$implicit_maps_inserted) { |
---|
| 5886 | # make SQL field lookup objects with incorporated field names |
---|
| 5887 | # fieldtype: B=boolean, N=numeric, S=string, |
---|
| 5888 | # B-, N-, S- returns undef if field does not exist |
---|
| 5889 | # B0: boolean, nonexistent field treated as false, |
---|
| 5890 | # B1: boolean, nonexistent field treated as true |
---|
| 5891 | my $nf = sub{Amavis::Lookup::SQLfield->new($sql_policy,@_)}; #shorthand |
---|
| 5892 | $user_id_sql = $nf->('id', 'S'); |
---|
| 5893 | unshift(@Amavis::Conf::local_domains_maps, $nf->('local', 'B1')); |
---|
| 5894 | |
---|
| 5895 | unshift(@Amavis::Conf::virus_lovers_maps, $nf->('virus_lover', 'B0')); |
---|
| 5896 | unshift(@Amavis::Conf::spam_lovers_maps, $nf->('spam_lover', 'B-')); |
---|
| 5897 | unshift(@Amavis::Conf::banned_files_lovers_maps, $nf->('banned_files_lover', 'B-')); |
---|
| 5898 | unshift(@Amavis::Conf::bad_header_lovers_maps, $nf->('bad_header_lover', 'B-')); |
---|
| 5899 | |
---|
| 5900 | unshift(@Amavis::Conf::bypass_virus_checks_maps, $nf->('bypass_virus_checks', 'B0')); |
---|
| 5901 | unshift(@Amavis::Conf::bypass_spam_checks_maps, $nf->('bypass_spam_checks', 'B0')); |
---|
| 5902 | unshift(@Amavis::Conf::bypass_banned_checks_maps, $nf->('bypass_banned_checks', 'B-')); |
---|
| 5903 | unshift(@Amavis::Conf::bypass_header_checks_maps, $nf->('bypass_header_checks', 'B-')); |
---|
| 5904 | |
---|
| 5905 | unshift(@Amavis::Conf::spam_tag_level_maps, $nf->('spam_tag_level', 'N')); |
---|
| 5906 | unshift(@Amavis::Conf::spam_tag2_level_maps, $nf->('spam_tag2_level', 'N')); |
---|
| 5907 | unshift(@Amavis::Conf::spam_kill_level_maps, $nf->('spam_kill_level', 'N')); |
---|
| 5908 | unshift(@Amavis::Conf::spam_dsn_cutoff_level_maps,$nf->('spam_dsn_cutoff_level','N-')); |
---|
| 5909 | |
---|
| 5910 | unshift(@Amavis::Conf::spam_modifies_subj_maps, $nf->('spam_modifies_subj', 'B-')); |
---|
| 5911 | unshift(@Amavis::Conf::spam_subject_tag_maps, $nf->('spam_subject_tag', 'S-')); |
---|
| 5912 | unshift(@Amavis::Conf::spam_subject_tag2_maps, $nf->('spam_subject_tag2', 'S-')); |
---|
| 5913 | |
---|
| 5914 | unshift(@Amavis::Conf::virus_quarantine_to_maps, $nf->('virus_quarantine_to', 'S-')); |
---|
| 5915 | unshift(@Amavis::Conf::banned_quarantine_to_maps, $nf->('banned_quarantine_to', 'S-')); |
---|
| 5916 | unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $nf->('bad_header_quarantine_to','S-')); |
---|
| 5917 | unshift(@Amavis::Conf::spam_quarantine_to_maps, $nf->('spam_quarantine_to', 'S-')); |
---|
| 5918 | unshift(@Amavis::Conf::message_size_limit_maps, $nf->('message_size_limit', 'N-')); |
---|
| 5919 | |
---|
| 5920 | unshift(@Amavis::Conf::addr_extension_virus_maps, $nf->('addr_extension_virus', 'S-')); |
---|
| 5921 | unshift(@Amavis::Conf::addr_extension_spam_maps, $nf->('addr_extension_spam', 'S-')); |
---|
| 5922 | unshift(@Amavis::Conf::addr_extension_banned_maps,$nf->('addr_extension_banned','S-')); |
---|
| 5923 | unshift(@Amavis::Conf::addr_extension_bad_header_maps,$nf->('addr_extension_bad_header','S-')); |
---|
| 5924 | |
---|
| 5925 | unshift(@Amavis::Conf::warnvirusrecip_maps, $nf->('warnvirusrecip', 'B-')); |
---|
| 5926 | unshift(@Amavis::Conf::warnbannedrecip_maps, $nf->('warnbannedrecip', 'B-')); |
---|
| 5927 | unshift(@Amavis::Conf::warnbadhrecip_maps, $nf->('warnbadhrecip', 'B-')); |
---|
| 5928 | |
---|
| 5929 | unshift(@Amavis::Conf::newvirus_admin_maps, $nf->('newvirus_admin', 'S-')); |
---|
| 5930 | unshift(@Amavis::Conf::virus_admin_maps, $nf->('virus_admin', 'S-')); |
---|
| 5931 | unshift(@Amavis::Conf::banned_admin_maps, $nf->('banned_admin', 'S-')); |
---|
| 5932 | unshift(@Amavis::Conf::bad_header_admin_maps, $nf->('bad_header_admin', 'S-')); |
---|
| 5933 | unshift(@Amavis::Conf::spam_admin_maps, $nf->('spam_admin', 'S-')); |
---|
| 5934 | |
---|
| 5935 | section_time('sql-prepare'); |
---|
| 5936 | } |
---|
| 5937 | Amavis::Conf::label_default_maps() if !$implicit_maps_inserted; |
---|
| 5938 | $implicit_maps_inserted = 1; |
---|
| 5939 | |
---|
| 5940 | my($conn) = Amavis::In::Connection->new; |
---|
| 5941 | $CONN = $conn; # ugly - save in a global |
---|
| 5942 | $conn->proto($sock->NS_proto); |
---|
| 5943 | my($suggested_protocol) = c('protocol'); # suggested by the policy bank |
---|
| 5944 | do_log(5,"process_request: suggested_protocol=\"$suggested_protocol\" on ". |
---|
| 5945 | $sock->NS_proto); |
---|
| 5946 | if ($sock->NS_proto eq 'UNIX') { # traditional amavis helper program |
---|
| 5947 | if ($suggested_protocol eq 'COURIER') { |
---|
| 5948 | die "unavailable support for protocol: $suggested_protocol"; |
---|
| 5949 | } elsif ($suggested_protocol eq 'AM.PDP') { |
---|
| 5950 | $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj; |
---|
| 5951 | $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0); |
---|
| 5952 | } else { # default to old amavis helper program protocol |
---|
| 5953 | $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj; |
---|
| 5954 | $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 1); |
---|
| 5955 | } |
---|
| 5956 | } elsif ($sock->NS_proto eq 'TCP') { |
---|
| 5957 | $conn->socket_ip($prop->{sockaddr}); |
---|
| 5958 | $conn->socket_port($prop->{sockport}); |
---|
| 5959 | $conn->client_ip($prop->{peeraddr}); |
---|
| 5960 | if ($suggested_protocol eq 'TCP-LOOKUP') { # postfix maps (experimental) |
---|
| 5961 | process_tcp_lookup_request($sock, $conn); |
---|
| 5962 | do_log(2, Amavis::Timing::report()); # report elapsed times |
---|
| 5963 | } elsif ($suggested_protocol eq 'AM.PDP') { |
---|
| 5964 | # amavis policy delegation protocol (e.g. new milter helper program) |
---|
| 5965 | $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj; |
---|
| 5966 | $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0); |
---|
| 5967 | } elsif ($suggested_protocol eq 'QMQPqq') { |
---|
| 5968 | if (!$extra_code_in_qmqpqq) { |
---|
| 5969 | die "incoming TCP connection, but dynamic QMQPqq code not loaded"; |
---|
| 5970 | } |
---|
| 5971 | $qmqpqq_in_obj = Amavis::In::QMQPqq->new if !$qmqpqq_in_obj; |
---|
| 5972 | $qmqpqq_in_obj->process_qmqpqq_request($sock,$conn,\&check_mail); |
---|
| 5973 | } else { # defaults to SMTP or LMTP |
---|
| 5974 | if (!$extra_code_in_smtp) { |
---|
| 5975 | die "incoming TCP connection, but dynamic SMTP/LMTP code not loaded"; |
---|
| 5976 | } |
---|
| 5977 | $smtp_in_obj = Amavis::In::SMTP->new if !$smtp_in_obj; |
---|
| 5978 | $smtp_in_obj->process_smtp_request( |
---|
| 5979 | $sock, ($suggested_protocol eq 'LMTP'?1:0), $conn, \&check_mail); |
---|
| 5980 | } |
---|
| 5981 | } else { |
---|
| 5982 | die ("unsupported protocol: $suggested_protocol, " . $sock->NS_proto); |
---|
| 5983 | } |
---|
| 5984 | }; # eval |
---|
| 5985 | alarm(0); # stop the timer |
---|
| 5986 | if ($@ ne '') { |
---|
| 5987 | chomp($@); my($timed_out) = $@ eq "timed out"; |
---|
| 5988 | my($msg) = $timed_out ? "Child task exceeded $child_timeout seconds, abort" |
---|
| 5989 | : "TROUBLE in process_request: $@"; |
---|
| 5990 | do_log(-2, $msg); |
---|
| 5991 | $smtp_in_obj->preserve_evidence(1) if $smtp_in_obj && !$timed_out; |
---|
| 5992 | # kills a child, hopefully preserving tempdir; does not kill parent |
---|
| 5993 | do_log(-1, "Requesting process rundown after fatal error"); |
---|
| 5994 | $self->done(1); |
---|
| 5995 | # die ("Suicide (" . am_id() . ") " . $msg . "\n"); |
---|
| 5996 | } |
---|
| 5997 | |
---|
| 5998 | my(@modules_extra) = grep {!exists $modules_basic{$_}} keys %INC; |
---|
| 5999 | # do_log(0, "modules loaded: ".join(", ", sort keys %modules_basic)); |
---|
| 6000 | do_log(1, "extra modules loaded: ". |
---|
| 6001 | join(", ", sort @modules_extra)) if @modules_extra; |
---|
| 6002 | |
---|
| 6003 | # if ($child_task_count >= $max_requests && |
---|
| 6004 | # $child_invocation_count < $max_requests) { |
---|
| 6005 | if ($child_task_count > $max_requests) { |
---|
| 6006 | # in case of multiple-transaction protocols (e.g. SMTP, LMTP) |
---|
| 6007 | # we do not like to keep running indefinitely at the MTA's mercy |
---|
| 6008 | do_log(1, "Requesting process rundown after $child_task_count tasks ". |
---|
| 6009 | "(and $child_invocation_count sessions)"); |
---|
| 6010 | $self->done(1); |
---|
| 6011 | } |
---|
| 6012 | } |
---|
| 6013 | |
---|
| 6014 | ### override Net::Server::PreForkSimple::done |
---|
| 6015 | ### to be able to rundown the child process prematurely |
---|
| 6016 | sub done(@) { |
---|
| 6017 | my($self) = shift; |
---|
| 6018 | if (@_) { $self->{server}->{done} = shift } |
---|
| 6019 | elsif (!$self->{server}->{done}) |
---|
| 6020 | { $self->{server}->{done} = $self->SUPER::done } |
---|
| 6021 | $self->{server}->{done}; |
---|
| 6022 | } |
---|
| 6023 | |
---|
| 6024 | ### Net::Server hook |
---|
| 6025 | sub post_process_request_hook { |
---|
| 6026 | my($self) = @_; |
---|
| 6027 | local $SIG{CHLD} = 'DEFAULT'; |
---|
| 6028 | debug_oneshot(0); |
---|
| 6029 | $0 = sprintf("amavisd (ch%d-avail)", $child_invocation_count); |
---|
| 6030 | alarm(0); do_log(5,"post_process_request_hook: timer stopped"); |
---|
| 6031 | $snmp_db->register_proc('') if defined $snmp_db; # process is alive and idle |
---|
| 6032 | Amavis::Timing::go_idle('bye'); Amavis::Timing::report_load(); |
---|
| 6033 | } |
---|
| 6034 | |
---|
| 6035 | ### Child is about to be terminated |
---|
| 6036 | ### user customizable Net::Server hook |
---|
| 6037 | sub child_finish_hook { |
---|
| 6038 | my($self) = @_; |
---|
| 6039 | local $SIG{CHLD} = 'DEFAULT'; |
---|
| 6040 | # for my $m (sort map { s/\.pm\z//; s[/][::]g; $_ } grep { /\.pm\z/ } keys %INC){ |
---|
| 6041 | # do_log(0, sprintf("Module %-19s %s", $m, $m->VERSION || '?')) |
---|
| 6042 | # if grep {$m=~/^$_/} qw(Mail::ClamAV Mail::SpamAssassin Razor2 Net::DNS); |
---|
| 6043 | # } |
---|
| 6044 | $0 = sprintf("amavisd (ch%d-finish)", $child_invocation_count); |
---|
| 6045 | do_log(5,"child_finish_hook: invoking DESTROY methods"); |
---|
| 6046 | $smtp_in_obj = undef; # calls Amavis::In::SMTP::DESTROY |
---|
| 6047 | $qmqpqq_in_obj = undef; # calls Amavis::In::QMQPqq::DESTROY |
---|
| 6048 | $amcl_in_obj = undef; # (currently does nothing for Amavis::In::AMCL) |
---|
| 6049 | $sql_wblist = undef; # calls Amavis::Lookup::SQL::DESTROY |
---|
| 6050 | $sql_policy = undef; # calls Amavis::Lookup::SQL::DESTROY |
---|
| 6051 | $ldap_policy = undef; # calls Amavis::Lookup::LDAP::DESTROY |
---|
| 6052 | $body_digest_cache = undef; # calls Amavis::Cache::DESTROY |
---|
| 6053 | eval { $snmp_db->register_proc(undef) } if defined $snmp_db; # going away |
---|
| 6054 | $snmp_db = undef; # calls Amavis::DB::SNMP::DESTROY |
---|
| 6055 | $db_env = undef; |
---|
| 6056 | } |
---|
| 6057 | |
---|
| 6058 | sub END { # runs before exiting the module |
---|
| 6059 | do_log(5,"at the END handler: invoking DESTROY methods"); |
---|
| 6060 | $smtp_in_obj = undef; # at end calls Amavis::In::SMTP::DESTROY |
---|
| 6061 | $qmqpqq_in_obj = undef; # at end calls Amavis::In::QMQPqq::DESTROY |
---|
| 6062 | $amcl_in_obj = undef; # (currently does nothing for Amavis::In::AMCL) |
---|
| 6063 | $sql_wblist = undef; # at end calls Amavis::Lookup::SQL::DESTROY |
---|
| 6064 | $sql_policy = undef; # at end calls Amavis::Lookup::SQL::DESTROY |
---|
| 6065 | $ldap_policy = undef; # at end calls Amavis::Lookup::LDAP::DESTROY |
---|
| 6066 | $body_digest_cache = undef; # at end calls Amavis::Cache::DESTROY |
---|
| 6067 | eval { $snmp_db->register_proc(undef) } if defined $snmp_db; # going away |
---|
| 6068 | $snmp_db = undef; # at end calls Amavis::DB::SNMP::DESTROY |
---|
| 6069 | $db_env = undef; |
---|
| 6070 | } |
---|
| 6071 | |
---|
| 6072 | # implements Postfix TCP lookup server, see tcp_table(5) man page; experimental |
---|
| 6073 | sub process_tcp_lookup_request($$) { |
---|
| 6074 | my($sock, $conn) = @_; |
---|
| 6075 | local($/) = "\012"; # set line terminator to LF (regardless of platform) |
---|
| 6076 | my($req_cnt); |
---|
| 6077 | while (<$sock>) { |
---|
| 6078 | $req_cnt++; my($level) = 0; |
---|
| 6079 | my($resp_code, $resp_msg) = (400, 'INTERNAL ERROR'); |
---|
| 6080 | if (/^get (.*?)\015?\012\z/si) { |
---|
| 6081 | my($key) = tcp_lookup_decode($1); |
---|
| 6082 | my($sl); $sl = lookup(0,$key, @{ca('spam_lovers_maps')}); |
---|
| 6083 | $resp_code = 200; $level = 2; |
---|
| 6084 | $resp_msg = $sl ? "OK Recipient <$key> IS spam lover" |
---|
| 6085 | : "DUNNO Recipient <$key> is NOT spam lover"; |
---|
| 6086 | } elsif (/^put ([^ ]*) (.*?)\015?\012\z/si) { |
---|
| 6087 | $resp_code = 500; $resp_msg = 'request not implemented: ' . $_; |
---|
| 6088 | } else { |
---|
| 6089 | $resp_code = 500; $resp_msg = 'illegal request: ' . $_; |
---|
| 6090 | } |
---|
| 6091 | do_log($level, "tcp_lookup($req_cnt): $resp_code $resp_msg"); |
---|
| 6092 | $sock->printf("%03d %s\012", $resp_code, tcp_lookup_encode($resp_msg)) |
---|
| 6093 | or die "Can't write to tcp_lookup socket: $!"; |
---|
| 6094 | } |
---|
| 6095 | do_log(0, "tcp_lookup: RUNDOWN after $req_cnt requests"); |
---|
| 6096 | } |
---|
| 6097 | |
---|
| 6098 | sub tcp_lookup_encode($) { |
---|
| 6099 | my($str) = @_; |
---|
| 6100 | $str =~ s/[^\041-\044\046-\176]/sprintf("%%%02x",ord($&))/eg; |
---|
| 6101 | $str; |
---|
| 6102 | } |
---|
| 6103 | |
---|
| 6104 | sub tcp_lookup_decode($) { |
---|
| 6105 | my($str) = @_; |
---|
| 6106 | $str =~ s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg; |
---|
| 6107 | $str; |
---|
| 6108 | } |
---|
| 6109 | |
---|
| 6110 | # Checks the message stored on a file. File must already |
---|
| 6111 | # be open on file handle $msginfo->mail_text; it need not be positioned |
---|
| 6112 | # properly, check_mail must not close the file handle. |
---|
| 6113 | # |
---|
| 6114 | sub check_mail($$$$) { |
---|
| 6115 | my($conn, $msginfo, $dsn_per_recip_capable, $tempdir) = @_; |
---|
| 6116 | |
---|
| 6117 | my($am_id) = am_id(); |
---|
| 6118 | $snmp_db->register_proc($am_id) if defined $snmp_db; |
---|
| 6119 | my($fh) = $msginfo->mail_text; my(@recips) = @{$msginfo->recips}; |
---|
| 6120 | |
---|
| 6121 | $MSGINFO = $msginfo; # ugly - save in a global, to make it accessible |
---|
| 6122 | # to %builtins |
---|
| 6123 | # check_mail() may be called several times per child lifetime and/or |
---|
| 6124 | # per-SMTP session. The variable $child_task_count is mainly used |
---|
| 6125 | # by AV-scanner interfaces, e.g. to initialize when invoked |
---|
| 6126 | # for the first time during child process lifetime. |
---|
| 6127 | $child_task_count++; |
---|
| 6128 | |
---|
| 6129 | # reset certain global variables for each task |
---|
| 6130 | $VIRUSFILE = undef; $av_output = undef; @detecting_scanners = (); |
---|
| 6131 | @virusname = (); @banned_filename = (); @bad_headers = (); |
---|
| 6132 | $spam_level = undef; $spam_status = undef; $spam_report = undef; |
---|
| 6133 | |
---|
| 6134 | # comment out to retain SQL cache entries for the whole child lifetime: |
---|
| 6135 | $sql_policy->clear_cache if defined $sql_policy; |
---|
| 6136 | $sql_wblist->clear_cache if defined $sql_wblist; |
---|
| 6137 | |
---|
| 6138 | # comment out to retain LDAP cache entries for the whole child lifetime: |
---|
| 6139 | $ldap_policy->clear_cache if defined $ldap_policy; |
---|
| 6140 | |
---|
| 6141 | # also measures mail size and saves mail header |
---|
| 6142 | $body_digest = get_body_digest($fh, $msginfo); |
---|
| 6143 | |
---|
| 6144 | my($mail_size) = $msginfo->msg_size; # use ESMTP size if available |
---|
| 6145 | $mail_size = $msginfo->orig_header_size + 1 + $msginfo->orig_body_size |
---|
| 6146 | if $mail_size <= 0; |
---|
| 6147 | # $mail_size = -s $msginfo->mail_text_fn; # get it from a file system |
---|
| 6148 | |
---|
| 6149 | my($file_generator_object) = # maxfiles 0 disables the $MAXFILES limit |
---|
| 6150 | Amavis::Unpackers::NewFilename->new($MAXFILES?$MAXFILES:undef, $mail_size); |
---|
| 6151 | Amavis::Unpackers::Part::init($file_generator_object); # fudge: keep in variable |
---|
| 6152 | my($parts_root) = Amavis::Unpackers::Part->new; |
---|
| 6153 | $msginfo->parts_root($parts_root); |
---|
| 6154 | my($smtp_resp, $exit_code, $preserve_evidence); my($virus_dejavu) = 0; |
---|
| 6155 | my($banned_filename_checked,$virus_presence_checked,$spam_presence_checked); |
---|
| 6156 | |
---|
| 6157 | # matching banned rules suggest DSN to be suppressed? |
---|
| 6158 | my($banned_dsn_suppress) = 0; |
---|
| 6159 | |
---|
| 6160 | # is any mail component password protected or otherwise non-decodable? |
---|
| 6161 | my($any_undecipherable) = 0; |
---|
| 6162 | |
---|
| 6163 | my($cl_ip) = $msginfo->client_addr; my($pbn) = c('policy_bank_path'); |
---|
| 6164 | do_log(1,sprintf("Checking: %s%s%s -> %s", |
---|
| 6165 | $pbn eq '' ? '' : "$pbn ", |
---|
| 6166 | $cl_ip eq '' ? '' : "[$cl_ip] ", |
---|
| 6167 | qquote_rfc2821_local($msginfo->sender), |
---|
| 6168 | join(',', qquote_rfc2821_local(@recips)) )); |
---|
| 6169 | |
---|
| 6170 | my($mime_err); # undef, or MIME parsing error string as given by MIME::Parser |
---|
| 6171 | my($hold); # set to some string to cause the message to be |
---|
| 6172 | # placed on hold (frozen) by MTA. This can be used |
---|
| 6173 | # in cases when we stumble across some permanent problem |
---|
| 6174 | # making us unable to decide if the message is to be |
---|
| 6175 | # really delivered. |
---|
| 6176 | my($which_section); |
---|
| 6177 | eval { |
---|
| 6178 | snmp_count('InMsgs'); |
---|
| 6179 | snmp_count('InMsgsNullRPath') if $msginfo->sender eq ''; |
---|
| 6180 | if (@recips == 1) { snmp_count( 'InMsgsRecips' ) } |
---|
| 6181 | elsif (@recips > 1) { snmp_count( ['InMsgsRecips',scalar(@recips)] ) } |
---|
| 6182 | |
---|
| 6183 | $which_section = "creating_partsdir"; |
---|
| 6184 | if (-d "$tempdir/parts") { |
---|
| 6185 | # mkdir is a costly operation (must be atomic, flushes buffers). |
---|
| 6186 | # If we can re-use directory 'parts' from the previous invocation |
---|
| 6187 | # it saves us precious time. Together with matching rmdir this can |
---|
| 6188 | # amount to 10-15 % of total elapsed time !!! (no spam checking) |
---|
| 6189 | } else { |
---|
| 6190 | mkdir("$tempdir/parts", 0750) |
---|
| 6191 | or die "Can't create directory $tempdir/parts: $!"; |
---|
| 6192 | section_time('mkdir parts'); |
---|
| 6193 | } |
---|
| 6194 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
| 6195 | |
---|
| 6196 | # FIRST: what kind of e-mail did we get? call content scanners |
---|
| 6197 | |
---|
| 6198 | # already in cache? |
---|
| 6199 | $which_section = "cached"; |
---|
| 6200 | snmp_count('CacheAttempts'); |
---|
| 6201 | my($cache_entry); my($now) = time; |
---|
| 6202 | my($cache_entry_ttl) = |
---|
| 6203 | max($virus_check_negative_ttl, $virus_check_positive_ttl, |
---|
| 6204 | $spam_check_negative_ttl, $spam_check_positive_ttl); |
---|
| 6205 | my($now_utc_iso8601) = iso8601_utc_timestamp($now,1); |
---|
| 6206 | my($expires_utc_iso8601) = iso8601_utc_timestamp($now+$cache_entry_ttl,1); |
---|
| 6207 | $cache_entry = $body_digest_cache->get($body_digest) |
---|
| 6208 | if $body_digest_cache && defined $body_digest; |
---|
| 6209 | if (!defined $cache_entry) { |
---|
| 6210 | snmp_count('CacheMisses'); |
---|
| 6211 | $cache_entry->{'ctime'} = $now_utc_iso8601; # create a new cache record |
---|
| 6212 | } else { |
---|
| 6213 | snmp_count('CacheHits'); |
---|
| 6214 | $banned_filename_checked = defined $cache_entry->{'FB'} ? 1 : 0; |
---|
| 6215 | $virus_presence_checked = defined $cache_entry->{'VN'} ? 1 : 0; |
---|
| 6216 | |
---|
| 6217 | # spam level and spam report may be influenced by mail header, not only |
---|
| 6218 | # by mail body, so caching based on body is only a close approximation; |
---|
| 6219 | # ignore spam cache if body is too small |
---|
| 6220 | $spam_presence_checked = defined $cache_entry->{'SL'} ? 1 : 0; |
---|
| 6221 | if ($msginfo->orig_body_size < 200) { $spam_presence_checked = 0 } |
---|
| 6222 | |
---|
| 6223 | if ($virus_presence_checked && defined $cache_entry->{'Vt'}) { |
---|
| 6224 | # check for expiration of cached virus test results |
---|
| 6225 | my($ttl) = !@{$cache_entry->{'VN'}} ? $virus_check_negative_ttl |
---|
| 6226 | : $virus_check_positive_ttl; |
---|
| 6227 | if ($now > $cache_entry->{'Vt'} + $ttl) { |
---|
| 6228 | do_log(2,"Cached virus check expired, TTL = $ttl s"); |
---|
| 6229 | $virus_presence_checked = 0; |
---|
| 6230 | } |
---|
| 6231 | } |
---|
| 6232 | if ($spam_presence_checked && defined $cache_entry->{'St'}) { |
---|
| 6233 | # check for expiration of cached spam test results |
---|
| 6234 | # (note: hard-wired spam level 6) |
---|
| 6235 | my($ttl) = $cache_entry->{'SL'} < 6 ? $spam_check_negative_ttl |
---|
| 6236 | : $spam_check_positive_ttl; |
---|
| 6237 | if ($now > $cache_entry->{'St'} + $ttl) { |
---|
| 6238 | do_log(2,"Cached spam check expired, TTL = $ttl s"); |
---|
| 6239 | $spam_presence_checked = 0; |
---|
| 6240 | } |
---|
| 6241 | } |
---|
| 6242 | if ($virus_presence_checked) { |
---|
| 6243 | $av_output = $cache_entry->{'VO'}; |
---|
| 6244 | @virusname = @{$cache_entry->{'VN'}}; |
---|
| 6245 | @detecting_scanners = @{$cache_entry->{'VD'}}; |
---|
| 6246 | $virus_dejavu = 1; |
---|
| 6247 | } |
---|
| 6248 | if ($banned_filename_checked) { |
---|
| 6249 | @banned_filename = @{$cache_entry->{'FB'}}; |
---|
| 6250 | $banned_dsn_suppress = $cache_entry->{'FS'}; |
---|
| 6251 | } |
---|
| 6252 | ($spam_level, $spam_status, $spam_report) = @$cache_entry{'SL','SS','SR'} |
---|
| 6253 | if $spam_presence_checked; |
---|
| 6254 | do_log(1,sprintf("cached %s from <%s> (%s,%s,%s)", |
---|
| 6255 | $body_digest, $msginfo->sender, |
---|
| 6256 | $banned_filename_checked, $virus_presence_checked, |
---|
| 6257 | $spam_presence_checked)); |
---|
| 6258 | snmp_count('CacheHitsVirusCheck') if $virus_presence_checked; |
---|
| 6259 | snmp_count('CacheHitsVirusMsgs') if @virusname; |
---|
| 6260 | snmp_count('CacheHitsSpamCheck') if $spam_presence_checked; |
---|
| 6261 | snmp_count('CacheHitsSpamMsgs') if $spam_level >= 6; # a hack |
---|
| 6262 | # snmp_count('CacheHitsBannedCheck') if $banned_filename_checked; |
---|
| 6263 | # snmp_count('CacheHitsBannedMsgs') if @banned_filename; |
---|
| 6264 | do_log(5,sprintf("cache entry age: %s c=%s a=%s", |
---|
| 6265 | (@virusname ? 'V' : $spam_level > 5 ? 'S' : '.'), |
---|
| 6266 | $cache_entry->{'ctime'}, $cache_entry->{'atime'} )); |
---|
| 6267 | } # if defined $cache_entry |
---|
| 6268 | |
---|
| 6269 | my($will_do_virus_scanning) = # virus scanning will be needed? |
---|
| 6270 | !$virus_presence_checked && $extra_code_antivirus && |
---|
| 6271 | grep {!lookup(0,$_, @{ca('bypass_virus_checks_maps')})} @recips; |
---|
| 6272 | |
---|
| 6273 | my($will_do_banned_checking) = # banned name checking will be needed? |
---|
| 6274 | !$banned_filename_checked && |
---|
| 6275 | (@{ca('banned_filename_maps')} || cr('banned_namepath_re')) && |
---|
| 6276 | grep {!lookup(0,$_, @{ca('bypass_banned_checks_maps')})} @recips; |
---|
| 6277 | |
---|
| 6278 | # will do decoding parts as deeply as possible? only if needed |
---|
| 6279 | my($will_do_parts_decoding) = |
---|
| 6280 | !c('bypass_decode_parts') && |
---|
| 6281 | ($will_do_virus_scanning || $will_do_banned_checking); |
---|
| 6282 | |
---|
| 6283 | $which_section = "mime_decode-1"; |
---|
| 6284 | my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root); |
---|
| 6285 | $msginfo->mime_entity($ent); |
---|
| 6286 | prolong_timer($which_section); |
---|
| 6287 | |
---|
| 6288 | if ($will_do_parts_decoding) { |
---|
| 6289 | # decoding parts can take a lot of time! |
---|
| 6290 | snmp_count('OpsDec'); |
---|
| 6291 | ($hold,$any_undecipherable) = |
---|
| 6292 | Amavis::Unpackers::decompose_mail($tempdir,$file_generator_object); |
---|
| 6293 | } |
---|
| 6294 | if (grep {!lookup(0,$_,@{ca('bypass_header_checks_maps')})} @recips) { |
---|
| 6295 | push(@bad_headers, "MIME error: ".$mime_err) if $mime_err ne ''; |
---|
| 6296 | push(@bad_headers, check_header_validity($conn,$msginfo)); |
---|
| 6297 | } |
---|
| 6298 | if ($will_do_banned_checking) { # check for banned file contents |
---|
| 6299 | $which_section = "check-banned"; |
---|
| 6300 | my($banned_part_descr_ref, $banned_matching_keys_ref, $banned_rhs_ref) = |
---|
| 6301 | check_for_banned_names($parts_root); |
---|
| 6302 | for my $j (0..$#{$banned_part_descr_ref}) { |
---|
| 6303 | if ($banned_rhs_ref->[$j] =~ /^DISCARD/) { |
---|
| 6304 | $banned_dsn_suppress = 1; |
---|
| 6305 | do_log(4,sprintf('BANNED:%s: %s', $banned_rhs_ref->[$j], |
---|
| 6306 | $banned_part_descr_ref->[$j])); |
---|
| 6307 | } |
---|
| 6308 | } |
---|
| 6309 | push(@banned_filename, @$banned_part_descr_ref); |
---|
| 6310 | } |
---|
| 6311 | $cache_entry->{'FB'} = \@banned_filename; |
---|
| 6312 | $cache_entry->{'FS'} = $banned_dsn_suppress; |
---|
| 6313 | |
---|
| 6314 | if ($virus_presence_checked) { |
---|
| 6315 | do_log(5, "virus_presence cached, skipping virus_scan"); |
---|
| 6316 | } elsif (!$extra_code_antivirus) { |
---|
| 6317 | do_log(5, "no anti-virus code loaded, skipping virus_scan"); |
---|
| 6318 | } elsif (!grep {!lookup(0,$_,@{ca('bypass_virus_checks_maps')})} @recips) { |
---|
| 6319 | do_log(5, "bypassing of virus checks requested"); |
---|
| 6320 | } elsif ($hold ne '') { # protect virus scanner from mail bombs |
---|
| 6321 | do_log(0, "NOTICE: Virus scanning skipped: $hold"); |
---|
| 6322 | $will_do_virus_scanning = 0; |
---|
| 6323 | } else { |
---|
| 6324 | if (!$will_do_virus_scanning) |
---|
| 6325 | { do_log(-1, "NOTICE: will_do_virus_scanning is false???") } |
---|
| 6326 | if (!defined($msginfo->mime_entity)) { |
---|
| 6327 | $which_section = "mime_decode-3"; |
---|
| 6328 | my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root); |
---|
| 6329 | $msginfo->mime_entity($ent); |
---|
| 6330 | prolong_timer($which_section); |
---|
| 6331 | } |
---|
| 6332 | # special case to make available a complete mail file for inspection |
---|
| 6333 | if ($mime_err ne '' || |
---|
| 6334 | lookup(0,'MAIL',@keep_decoded_original_maps) || |
---|
| 6335 | $any_undecipherable && lookup(0,'MAIL-UNDECIPHERABLE', |
---|
| 6336 | @keep_decoded_original_maps)) { |
---|
| 6337 | # keep the original email.txt by making a hard link to it in ./parts/ |
---|
| 6338 | $which_section = "linking-to-MAIL"; |
---|
| 6339 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts", |
---|
| 6340 | $parts_root); |
---|
| 6341 | my($newpart) = $newpart_obj->full_name; |
---|
| 6342 | do_log(2, "providing full original message to scanners as $newpart". |
---|
| 6343 | (!$any_undecipherable ?'' :", $any_undecipherable undecipherable"). |
---|
| 6344 | ($mime_err eq '' ? '' : ", MIME error: $mime_err") ); |
---|
| 6345 | link($msginfo->mail_text_fn, $newpart) |
---|
| 6346 | or die sprintf("Can't create hard link %s to %s: %s", |
---|
| 6347 | $newpart, $msginfo->mail_text_fn, $!); |
---|
| 6348 | $newpart_obj->type_short('MAIL'); |
---|
| 6349 | $newpart_obj->type_declared('message/rfc822'); |
---|
| 6350 | } |
---|
| 6351 | $which_section = "virus_scan"; |
---|
| 6352 | # some virus scanners behave badly if interrupted, |
---|
| 6353 | # so for now just turn off the timer |
---|
| 6354 | my($remaining_time) = alarm(0); # check time left, stop timer |
---|
| 6355 | my($av_ret); |
---|
| 6356 | eval { |
---|
| 6357 | my($vn, $ds); |
---|
| 6358 | ($av_ret, $av_output, $vn, $ds) = |
---|
| 6359 | Amavis::AV::virus_scan($tempdir, $child_task_count==1, $parts_root); |
---|
| 6360 | @virusname = @$vn; @detecting_scanners = @$ds; # copy |
---|
| 6361 | }; |
---|
| 6362 | prolong_timer($which_section, $remaining_time); # restart timer |
---|
| 6363 | if ($@ ne '') { |
---|
| 6364 | chomp($@); |
---|
| 6365 | if ($@ eq "timed out") { |
---|
| 6366 | @virusname = (); $av_ret = 0; # assume not a virus! |
---|
| 6367 | do_log(-1, "virus_scan TIMED OUT, ASSUME NOT A VIRUS !!!"); |
---|
| 6368 | } else { |
---|
| 6369 | $hold = "virus_scan: $@"; # request HOLD |
---|
| 6370 | $av_ret = 0; # pretend it was ok (msg should be held) |
---|
| 6371 | die "$hold\n"; # die, TEMPFAIL is preferred to HOLD |
---|
| 6372 | } |
---|
| 6373 | } |
---|
| 6374 | snmp_count('OpsVirusCheck'); |
---|
| 6375 | defined($av_ret) or die "All virus scanners failed!"; |
---|
| 6376 | @$cache_entry{'Vt','VO','VN','VD'} = |
---|
| 6377 | ($now, $av_output, \@virusname, \@detecting_scanners); |
---|
| 6378 | $virus_presence_checked = 1; |
---|
| 6379 | if (defined $snmp_db && @virusname) { |
---|
| 6380 | $which_section = "read_counters"; |
---|
| 6381 | $virus_dejavu = 1 if !grep {$_==0} # none with counter zero or undef |
---|
| 6382 | @{$snmp_db->read_counters(map {"virus.byname.$_"} @virusname)}; |
---|
| 6383 | section_time($which_section); |
---|
| 6384 | } |
---|
| 6385 | } |
---|
| 6386 | |
---|
| 6387 | my($sender_contact,$sender_source); |
---|
| 6388 | if (!@virusname) { $sender_contact = $sender_source = $msginfo->sender } |
---|
| 6389 | else { |
---|
| 6390 | ($sender_contact,$sender_source) = best_try_originator( |
---|
| 6391 | $msginfo->sender, $msginfo->mime_entity, \@virusname); |
---|
| 6392 | section_time('best_try_originator'); |
---|
| 6393 | } |
---|
| 6394 | $msginfo->sender_contact($sender_contact); # save it |
---|
| 6395 | $msginfo->sender_source($sender_source); # save it |
---|
| 6396 | |
---|
| 6397 | # consider doing spam scanning |
---|
| 6398 | if (!$extra_code_antispam) { |
---|
| 6399 | do_log(5, "no anti-spam code loaded, skipping spam_scan"); |
---|
| 6400 | } elsif (@virusname || @banned_filename) { |
---|
| 6401 | do_log(5, "infected or banned contents, skipping spam_scan"); |
---|
| 6402 | } elsif (!grep {!lookup(0,$_,@{ca('bypass_spam_checks_maps')})} @recips) { |
---|
| 6403 | do_log(5, "bypassing of spam checks requested"); |
---|
| 6404 | } else { |
---|
| 6405 | $which_section = "spam-wb-list"; |
---|
| 6406 | my($any_wbl, $all_wbl) = Amavis::SpamControl::white_black_list( |
---|
| 6407 | $conn, $msginfo, $sql_wblist, $user_id_sql, $ldap_policy); |
---|
| 6408 | section_time($which_section); |
---|
| 6409 | if ($all_wbl) { |
---|
| 6410 | do_log(5, "sender white/blacklisted, skipping spam_scan"); |
---|
| 6411 | } elsif ($spam_presence_checked) { |
---|
| 6412 | do_log(5, "spam_presence cached, skipping spam_scan"); |
---|
| 6413 | } else { |
---|
| 6414 | $which_section = "spam_scan"; |
---|
| 6415 | ($spam_level, $spam_status, $spam_report) = |
---|
| 6416 | Amavis::SpamControl::spam_scan($conn, $msginfo); |
---|
| 6417 | prolong_timer($which_section); |
---|
| 6418 | snmp_count('OpsSpamCheck'); |
---|
| 6419 | @$cache_entry{'St','SL','SS','SR'} = |
---|
| 6420 | ($now, $spam_level, $spam_status, $spam_report); |
---|
| 6421 | $spam_presence_checked = 1; |
---|
| 6422 | } |
---|
| 6423 | } |
---|
| 6424 | |
---|
| 6425 | # store to cache |
---|
| 6426 | $cache_entry->{'atime'} = $now_utc_iso8601; # update accessed timestamp |
---|
| 6427 | $body_digest_cache->set($body_digest,$cache_entry, |
---|
| 6428 | $now_utc_iso8601,$expires_utc_iso8601) |
---|
| 6429 | if $body_digest_cache && defined $body_digest; |
---|
| 6430 | $cache_entry = undef; # discard the object, it is no longer needed |
---|
| 6431 | section_time('update_cache'); |
---|
| 6432 | |
---|
| 6433 | snmp_count("virus.byname.$_") for @virusname; |
---|
| 6434 | |
---|
| 6435 | # SECOND: now that we know what we got, decide what to do with it |
---|
| 6436 | |
---|
| 6437 | my($considered_spam_by_some_recips,$considered_oversize_by_some_recips); |
---|
| 6438 | |
---|
| 6439 | if (@virusname || @banned_filename) { # virus or banned filename found |
---|
| 6440 | # bad_headers do not enter this section, although code is ready for them; |
---|
| 6441 | # we'll handle bad headers later, if mail turns out not to be spam |
---|
| 6442 | $which_section = "deal_with_virus_or_banned"; |
---|
| 6443 | my($final_destiny) = @virusname ? c('final_virus_destiny') |
---|
| 6444 | : @banned_filename ? c('final_banned_destiny') |
---|
| 6445 | : @bad_headers ? c('final_bad_header_destiny') |
---|
| 6446 | : D_PASS; |
---|
| 6447 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 6448 | next if $r->recip_done; # already dealt with |
---|
| 6449 | if ($final_destiny == D_PASS) { |
---|
| 6450 | # recipient wants this message, malicious or not |
---|
| 6451 | } elsif ((!@virusname || # not a virus or we want it |
---|
| 6452 | lookup(0,$r->recip_addr, @{ca('virus_lovers_maps')})) && |
---|
| 6453 | (!@banned_filename || # not banned or we want it |
---|
| 6454 | lookup(0,$r->recip_addr, @{ca('banned_files_lovers_maps')})) && |
---|
| 6455 | (!@bad_headers || # not bad header or we want it |
---|
| 6456 | lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')})) ) |
---|
| 6457 | { |
---|
| 6458 | # clean, or recipient wants it |
---|
| 6459 | } else { # change mail destiny for those not wanting malware |
---|
| 6460 | $r->recip_destiny($final_destiny); |
---|
| 6461 | my($reason); |
---|
| 6462 | if (@virusname) |
---|
| 6463 | { $reason = "VIRUS: " . join(", ", @virusname) } |
---|
| 6464 | elsif (@banned_filename) |
---|
| 6465 | { $reason = "BANNED: " . join(", ", @banned_filename) } |
---|
| 6466 | elsif (@bad_headers) |
---|
| 6467 | { $reason = "BAD_HEADER: " . join(", ", @bad_headers) } |
---|
| 6468 | $reason = substr($reason,0,100)."..." if length($reason) > 100+3; |
---|
| 6469 | $r->recip_smtp_response( ($final_destiny == D_DISCARD |
---|
| 6470 | ? "250 2.7.1 Ok, discarded" |
---|
| 6471 | : "550 5.7.1 Message content rejected") . |
---|
| 6472 | ", id=$am_id - $reason"); |
---|
| 6473 | $r->recip_done(1); |
---|
| 6474 | } |
---|
| 6475 | } |
---|
| 6476 | $which_section = "virus_or_banned quar+notif"; |
---|
| 6477 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
| 6478 | # send notifications, quarantine it |
---|
| 6479 | do_virus($conn, $msginfo, $virus_dejavu); |
---|
| 6480 | |
---|
| 6481 | } else { # perhaps some recips consider it spam? |
---|
| 6482 | # spaminess is an individual matter, we must compare spam level |
---|
| 6483 | # with each recipient setting, there is no global criterium |
---|
| 6484 | # that the mail is spam |
---|
| 6485 | $which_section = "deal_with_spam"; |
---|
| 6486 | my($final_destiny) = c('final_spam_destiny'); |
---|
| 6487 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 6488 | next if $r->recip_done; # already dealt with |
---|
| 6489 | my($kill_level); |
---|
| 6490 | $kill_level = lookup(0,$r->recip_addr, @{ca('spam_kill_level_maps')}); |
---|
| 6491 | my($boost) = $r->recip_score_boost; |
---|
| 6492 | my($should_be_killed) = |
---|
| 6493 | !$r->recip_whitelisted_sender && |
---|
| 6494 | ($r->recip_blacklisted_sender || |
---|
| 6495 | (defined $spam_level && defined $kill_level ? |
---|
| 6496 | $spam_level+$boost >= $kill_level : 0) ); |
---|
| 6497 | next unless $should_be_killed; |
---|
| 6498 | # message is at or above kill level, or sender is blacklisted |
---|
| 6499 | $considered_spam_by_some_recips = 1; |
---|
| 6500 | if ($final_destiny == D_PASS || |
---|
| 6501 | lookup(0,$r->recip_addr, @{ca('spam_lovers_maps')})) { |
---|
| 6502 | # do nothing, recipient wants this message, even if spam |
---|
| 6503 | } else { # change mail destiny for those not wanting spam |
---|
| 6504 | ll(3) && do_log(3,sprintf( |
---|
| 6505 | "SPAM-KILL, %s -> %s, hits=%s, kill=%s%s", |
---|
| 6506 | qquote_rfc2821_local($msginfo->sender, $r->recip_addr), |
---|
| 6507 | (!defined $spam_level ? 'x' |
---|
| 6508 | : !defined $boost ? $spam_level |
---|
| 6509 | : $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost), |
---|
| 6510 | !defined $kill_level ? 'x' : 0+sprintf("%.3f",$kill_level), |
---|
| 6511 | $r->recip_blacklisted_sender ? ', BLACKLISTED' : '')); |
---|
| 6512 | $r->recip_destiny($final_destiny); |
---|
| 6513 | my($reason) = |
---|
| 6514 | $r->recip_blacklisted_sender ? 'sender blacklisted' : 'UBE'; |
---|
| 6515 | $r->recip_smtp_response(($final_destiny == D_DISCARD |
---|
| 6516 | ? "250 2.7.1 Ok, discarded, $reason" |
---|
| 6517 | : "550 5.7.1 Message content rejected, $reason" |
---|
| 6518 | ) . ", id=$am_id"); |
---|
| 6519 | $r->recip_done(1); |
---|
| 6520 | } |
---|
| 6521 | } |
---|
| 6522 | if ($considered_spam_by_some_recips) { |
---|
| 6523 | $which_section = "spam quar+notif"; |
---|
| 6524 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
| 6525 | do_spam($conn, $msginfo); |
---|
| 6526 | section_time('post-do_spam'); |
---|
| 6527 | } |
---|
| 6528 | } |
---|
| 6529 | |
---|
| 6530 | if (@bad_headers) { # invalid mail headers |
---|
| 6531 | $which_section = "deal_with_bad_headers"; |
---|
| 6532 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
| 6533 | my($is_bulk) = $msginfo->mime_entity->head->get('precedence', 0); |
---|
| 6534 | chomp($is_bulk); |
---|
| 6535 | do_log(1,sprintf("BAD HEADER from %s<%s>: %s", |
---|
| 6536 | $is_bulk eq '' ? '' : "($is_bulk) ", $msginfo->sender, |
---|
| 6537 | $bad_headers[0])); |
---|
| 6538 | $is_bulk = $is_bulk=~/^(bulk|list|junk)/i ? $1 : undef; |
---|
| 6539 | if (defined $is_bulk || $msginfo->sender eq '') { |
---|
| 6540 | # have mercy on mailing lists and DSN |
---|
| 6541 | } else { |
---|
| 6542 | my($any_badh); my($final_destiny) = c('final_bad_header_destiny'); |
---|
| 6543 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 6544 | next if $r->recip_done; # already dealt with |
---|
| 6545 | if ($final_destiny == D_PASS || |
---|
| 6546 | lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')})) |
---|
| 6547 | { |
---|
| 6548 | # recipient wants this message, broken or not |
---|
| 6549 | } else { # change mail destiny for those not wanting it |
---|
| 6550 | $r->recip_destiny($final_destiny); |
---|
| 6551 | my($reason) = (split(/\n/, $bad_headers[0]))[0]; |
---|
| 6552 | $r->recip_smtp_response(($final_destiny == D_DISCARD |
---|
| 6553 | ? "250 2.6.0 Ok, message with invalid header discarded" |
---|
| 6554 | : "554 5.6.0 Message with invalid header rejected" |
---|
| 6555 | ) . ", id=$am_id - $reason"); |
---|
| 6556 | $r->recip_done(1); |
---|
| 6557 | $any_badh++; |
---|
| 6558 | } |
---|
| 6559 | } |
---|
| 6560 | if ($any_badh) { # we use the same code as for viruses or banned |
---|
| 6561 | # but only if it wasn't already handled as spam |
---|
| 6562 | do_virus($conn, $msginfo, 0); # send notifications, quarantine it |
---|
| 6563 | } |
---|
| 6564 | } |
---|
| 6565 | section_time($which_section); |
---|
| 6566 | } |
---|
| 6567 | |
---|
| 6568 | my($mslm) = ca('message_size_limit_maps'); |
---|
| 6569 | if (@$mslm) { |
---|
| 6570 | $which_section = "deal_with_mail_size"; |
---|
| 6571 | my($mail_size) = $msginfo->msg_size; |
---|
| 6572 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 6573 | next if $r->recip_done; # already dealt with |
---|
| 6574 | my($size_limit) = lookup(0,$r->recip_addr, @$mslm); |
---|
| 6575 | $size_limit = 65536 |
---|
| 6576 | if $size_limit && $size_limit < 65536; # rfc2821 |
---|
| 6577 | if ($size_limit && $mail_size > $size_limit) { |
---|
| 6578 | do_log(1,sprintf("OVERSIZE from <%s> to <%s>: size %s B, limit %s B", |
---|
| 6579 | $msginfo->sender, $r->recip_addr, $mail_size, $size_limit)) |
---|
| 6580 | if !$considered_oversize_by_some_recips; |
---|
| 6581 | $considered_oversize_by_some_recips = 1; |
---|
| 6582 | $r->recip_destiny(D_BOUNCE); |
---|
| 6583 | $r->recip_smtp_response("552 5.3.4 Message size ($mail_size B) ". |
---|
| 6584 | "exceeds fixed maximium message size of $size_limit B, id=$am_id"); |
---|
| 6585 | $r->recip_done(1); |
---|
| 6586 | } |
---|
| 6587 | } |
---|
| 6588 | section_time($which_section); |
---|
| 6589 | } |
---|
| 6590 | |
---|
| 6591 | $which_section = "snooping_quarantine"; |
---|
| 6592 | # do_quarantine($conn, $msginfo, Amavis::Out::EditHeader->new, |
---|
| 6593 | # ['sender-quarantine'], 'local:user-%i-%n' |
---|
| 6594 | # ) if lookup(0,$msginfo->sender, ['user1@domain','user2@domain']); |
---|
| 6595 | # do_quarantine($conn, $msginfo, Amavis::Out::EditHeader->new, |
---|
| 6596 | # ['incoming-quarantine'], 'local:all-%i-%n'); |
---|
| 6597 | # do_quarantine($conn, $msginfo, Amavis::Out::EditHeader->new, |
---|
| 6598 | # ['archive@localhost'], 'local:all-%i-%n'); |
---|
| 6599 | # section_time($which_section); |
---|
| 6600 | |
---|
| 6601 | $which_section = "checking_sender_ip"; |
---|
| 6602 | my(@recips) = @{$msginfo->recips}; |
---|
| 6603 | if ($considered_spam_by_some_recips && @recips==1 && |
---|
| 6604 | $recips[0] eq $msginfo->sender && |
---|
| 6605 | lookup(0,$msginfo->sender, @{ca('local_domains_maps')})) |
---|
| 6606 | { |
---|
| 6607 | my($cl_ip) = $msginfo->client_addr; |
---|
| 6608 | if ($cl_ip eq '') { |
---|
| 6609 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
| 6610 | $cl_ip = fish_out_ip_from_received( |
---|
| 6611 | $msginfo->mime_entity->head->get('received',0)); |
---|
| 6612 | } |
---|
| 6613 | if ($cl_ip ne '') { |
---|
| 6614 | my($is_our_ip) = |
---|
| 6615 | eval { lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) }; |
---|
| 6616 | if ($@ ne '' && !$is_our_ip) { |
---|
| 6617 | do_log(0, "FAKE SENDER, SPAM: $cl_ip, " . $msginfo->sender); |
---|
| 6618 | $msginfo->sender_contact(undef); # believed to be faked |
---|
| 6619 | } |
---|
| 6620 | } |
---|
| 6621 | } |
---|
| 6622 | |
---|
| 6623 | if ($hold ne '') { do_log(-1, "NOTICE: HOLD reason: $hold") } |
---|
| 6624 | |
---|
| 6625 | # THIRD: now that we know what to do with it, do it! |
---|
| 6626 | |
---|
| 6627 | my($which_content_counter) = |
---|
| 6628 | @virusname ? 'ContentVirusMsgs' |
---|
| 6629 | : @banned_filename ? 'ContentBannedMsgs' |
---|
| 6630 | : $considered_spam_by_some_recips ? 'ContentSpamMsgs' |
---|
| 6631 | : @bad_headers ? 'ContentBadHdrMsgs' |
---|
| 6632 | : $considered_oversize_by_some_recips ? 'ContentOversizeMsgs' |
---|
| 6633 | : 'ContentCleanMsgs'; |
---|
| 6634 | snmp_count($which_content_counter); |
---|
| 6635 | |
---|
| 6636 | my($hdr_edits) = $msginfo->header_edits; |
---|
| 6637 | if (!$hdr_edits) { |
---|
| 6638 | $hdr_edits = Amavis::Out::EditHeader->new; |
---|
| 6639 | $msginfo->header_edits($hdr_edits); |
---|
| 6640 | } |
---|
| 6641 | if ($msginfo->delivery_method eq '') { # AM.PDP or AM.CL (milter) |
---|
| 6642 | $which_section = "AM.PDP headers"; |
---|
| 6643 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
| 6644 | $hdr_edits = add_forwarding_header_edits_common( |
---|
| 6645 | $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable, |
---|
| 6646 | $virus_presence_checked, $spam_presence_checked, undef); |
---|
| 6647 | my($done_all); |
---|
| 6648 | my($recip_cl); # ref to a list of similar recip objects |
---|
| 6649 | ($hdr_edits, $recip_cl, $done_all) = |
---|
| 6650 | add_forwarding_header_edits_per_recip( |
---|
| 6651 | $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable, |
---|
| 6652 | $virus_presence_checked, $spam_presence_checked, undef, undef); |
---|
| 6653 | $msginfo->header_edits($hdr_edits); # store edits (redundant?) |
---|
| 6654 | if (@$recip_cl && !$done_all) { |
---|
| 6655 | do_log(-1, "AM.PDP: CLIENTS REQUIRE DIFFERENT HEADERS"); |
---|
| 6656 | }; |
---|
| 6657 | } elsif (grep { !$_->recip_done } @{$msginfo->per_recip_data}) { # forward |
---|
| 6658 | # To be delivered explicitly - only to those recipients not yet marked |
---|
| 6659 | # as 'done' by the above content filtering sections. |
---|
| 6660 | $which_section = "forwarding"; |
---|
| 6661 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
| 6662 | # a quick-fix solution to defang dangerous contents |
---|
| 6663 | my($mail_defanged); # nonempty indicates mail body is replaced |
---|
| 6664 | my($explanation); |
---|
| 6665 | if ($hold ne '') { $explanation = |
---|
| 6666 | "WARNING: possible mail bomb, NOT CHECKED FOR VIRUSES:\n $hold"; |
---|
| 6667 | } elsif (@virusname) { $explanation = |
---|
| 6668 | 'WARNING: contains virus '.join(' ',@virusname) if c('defang_virus'); |
---|
| 6669 | } elsif (@banned_filename) { $explanation = |
---|
| 6670 | "WARNING: contains banned part" if c('defang_banned'); |
---|
| 6671 | } elsif ($any_undecipherable) { $explanation = |
---|
| 6672 | "WARNING: contains undecipherable part" if c('defang_undecipherable'); |
---|
| 6673 | } elsif ($considered_spam_by_some_recips) { $explanation = |
---|
| 6674 | $spam_report if c('defang_spam'); |
---|
| 6675 | } elsif (@bad_headers) { $explanation = |
---|
| 6676 | 'WARNING: bad headers '.join(' ',@bad_headers) if c('defang_bad_header'); |
---|
| 6677 | } else { $explanation = '(clean)' if c('defang_all') } |
---|
| 6678 | if (defined $explanation) { # malware |
---|
| 6679 | $explanation .= "\n" if $explanation !~ /\n\z/; |
---|
| 6680 | my($s) = $explanation; $s=~s/[ \t\n]+\z//; |
---|
| 6681 | if (length($s) > 100) { $s = substr($s,0,100-3) . "..." } |
---|
| 6682 | do_log(1, "DEFANGING MAIL: $s"); |
---|
| 6683 | my($d) = defanged_mime_entity($conn,$msginfo,$explanation); |
---|
| 6684 | $msginfo->mail_text($d); # substitute mail with rewritten version |
---|
| 6685 | $msginfo->mail_text_fn(undef); # remove filename information |
---|
| 6686 | $mail_defanged = 'Original mail wrapped as attachment (defanged)'; |
---|
| 6687 | section_time('defang'); |
---|
| 6688 | } |
---|
| 6689 | $hdr_edits = add_forwarding_header_edits_common( |
---|
| 6690 | $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable, |
---|
| 6691 | $virus_presence_checked, $spam_presence_checked, $mail_defanged); |
---|
| 6692 | for (;;) { # do the delivery |
---|
| 6693 | my($r_hdr_edits) = Amavis::Out::EditHeader->new; # per-recip edits set |
---|
| 6694 | $r_hdr_edits->inherit_header_edits($hdr_edits); |
---|
| 6695 | my($done_all); |
---|
| 6696 | my($recip_cl); # ref to a list of similar recip objects |
---|
| 6697 | ($r_hdr_edits, $recip_cl, $done_all) = |
---|
| 6698 | add_forwarding_header_edits_per_recip( |
---|
| 6699 | $conn, $msginfo, $r_hdr_edits, $hold, $any_undecipherable, |
---|
| 6700 | $virus_presence_checked, $spam_presence_checked, |
---|
| 6701 | $mail_defanged, undef); |
---|
| 6702 | last if !@$recip_cl; |
---|
| 6703 | $msginfo->header_edits($r_hdr_edits); # store edits |
---|
| 6704 | mail_dispatch($conn, $msginfo, 0, |
---|
| 6705 | sub { my($r) = @_; grep { $_ eq $r } @$recip_cl }); |
---|
| 6706 | snmp_count('OutForwMsgs'); |
---|
| 6707 | snmp_count('OutForwHoldMsgs') if $hold ne ''; |
---|
| 6708 | last if $done_all; |
---|
| 6709 | } |
---|
| 6710 | } |
---|
| 6711 | prolong_timer($which_section); |
---|
| 6712 | |
---|
| 6713 | $which_section = "delivery-notification"; |
---|
| 6714 | my($dsn_needed); |
---|
| 6715 | ($smtp_resp, $exit_code, $dsn_needed) = |
---|
| 6716 | one_response_for_all($msginfo, $dsn_per_recip_capable, $am_id); |
---|
| 6717 | my($warnsender_with_pass) = |
---|
| 6718 | $smtp_resp =~ /^2/ && !$dsn_needed && |
---|
| 6719 | (@virusname && c('warnvirussender') || |
---|
| 6720 | @banned_filename && c('warnbannedsender') || |
---|
| 6721 | $considered_spam_by_some_recips && c('warnspamsender') || |
---|
| 6722 | @bad_headers && c('warnbadhsender') ); |
---|
| 6723 | ll(4) && do_log(4,sprintf( |
---|
| 6724 | "warnsender_with_pass=%s (%s,%s,%s,%s), dsn_needed=%s, exit=%s, %s", |
---|
| 6725 | $warnsender_with_pass, |
---|
| 6726 | c('warnvirussender'),c('warnbannedsender'), |
---|
| 6727 | c('warnbadhsender'),c('warnspamsender'), |
---|
| 6728 | $dsn_needed,$exit_code,$smtp_resp)); |
---|
| 6729 | if ($dsn_needed || $warnsender_with_pass) { |
---|
| 6730 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
| 6731 | my($what_bad_content) = join(' & ', |
---|
| 6732 | !@virusname ? () : 'VIRUS', |
---|
| 6733 | !@banned_filename ? () : 'BANNED', |
---|
| 6734 | !$considered_spam_by_some_recips ? () : 'SPAM', |
---|
| 6735 | !@bad_headers ? () : 'BAD HEADER', |
---|
| 6736 | !$considered_oversize_by_some_recips ? () : 'OVERSIZE'); |
---|
| 6737 | my($notification); my($dsn_cutoff_level); |
---|
| 6738 | if ($msginfo->sender eq '') { # don't respond to null reverse path |
---|
| 6739 | my($msg) = "DSN contains $what_bad_content; bounce is not bouncible"; |
---|
| 6740 | if (!$dsn_needed) { do_log(4, $msg) } |
---|
| 6741 | else { do_log(1, "NOTICE: $msg, mail intentionally dropped") } |
---|
| 6742 | $msginfo->dsn_sent(2); # pretend the message was bounced |
---|
| 6743 | } elsif ($msginfo->sender_contact eq '') { |
---|
| 6744 | my($msg) = sprintf("Not sending DSN to believed-to-be-faked " |
---|
| 6745 | . "sender <%s>, mail containing %s", |
---|
| 6746 | $msginfo->sender, $what_bad_content); |
---|
| 6747 | if (!$dsn_needed) { do_log(4, $msg) } |
---|
| 6748 | else { do_log(2, "NOTICE: $msg intentionally dropped") } |
---|
| 6749 | $msginfo->dsn_sent(2); # pretend the message was bounced |
---|
| 6750 | } elsif ($banned_dsn_suppress) { |
---|
| 6751 | my($msg) = "Not sending DSN, as suggested by banned rule"; |
---|
| 6752 | if (!$dsn_needed) { do_log(4, $msg) } |
---|
| 6753 | else { do_log(1, "NOTICE: $msg, mail intentionally dropped") } |
---|
| 6754 | $msginfo->dsn_sent(2); # pretend the message was bounced |
---|
| 6755 | } elsif (defined $spam_level && |
---|
| 6756 | !grep { $spam_level + $_->recip_score_boost < |
---|
| 6757 | lookup(0,$_->recip_addr, |
---|
| 6758 | @{ca('spam_dsn_cutoff_level_maps')}) } |
---|
| 6759 | @{$msginfo->per_recip_data} ) { |
---|
| 6760 | my($msg) = "Not sending DSN, spam level exceeds DSN cutoff level"; |
---|
| 6761 | if (!$dsn_needed) { do_log(4, $msg) } |
---|
| 6762 | else { do_log(1, "NOTICE: $msg, mail intentionally dropped") } |
---|
| 6763 | $msginfo->dsn_sent(2); # pretend the message was bounced |
---|
| 6764 | } elsif ((@virusname || @banned_filename || |
---|
| 6765 | $considered_spam_by_some_recips || @bad_headers || |
---|
| 6766 | $considered_oversize_by_some_recips) && |
---|
| 6767 | $msginfo->mime_entity->head->get('precedence',0) |
---|
| 6768 | =~ /^(bulk|list|junk)/i ) |
---|
| 6769 | { |
---|
| 6770 | my($msg) = sprintf("Not sending DSN in response to bulk mail " |
---|
| 6771 | . "from <%s> containing %s", |
---|
| 6772 | $msginfo->sender, $what_bad_content); |
---|
| 6773 | if (!$dsn_needed) { do_log(4, $msg) } |
---|
| 6774 | else { do_log(1, "NOTICE: $msg, mail intentionally dropped") } |
---|
| 6775 | $msginfo->dsn_sent(2); # pretend the message was bounced |
---|
| 6776 | } else { # prepare a notification |
---|
| 6777 | my($which_dsn_counter,$dsnmsgref); |
---|
| 6778 | ### TODO: better selection of DSN reason is needed! |
---|
| 6779 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 6780 | next if !$r->recip_done; |
---|
| 6781 | local($_) = $r->recip_smtp_response; |
---|
| 6782 | ($which_dsn_counter,$dsnmsgref) = |
---|
| 6783 | /^5.*\bVIRUS\b/ ? |
---|
| 6784 | ('OutDsnVirusMsgs', cr('notify_virus_sender_templ')) |
---|
| 6785 | : /^5.*\bBANNED\b/ ? |
---|
| 6786 | ('OutDsnBannedMsgs',cr('notify_virus_sender_templ')) |
---|
| 6787 | : /^5.*\b(?:UBE|blacklisted)\b/ ? |
---|
| 6788 | ('OutDsnSpamMsgs', cr('notify_spam_sender_templ')) |
---|
| 6789 | : /^5.*\bheader\b/ ? |
---|
| 6790 | ('OutDsnBadHdrMsgs',cr('notify_sender_templ')) |
---|
| 6791 | : ('OutDsnOtherMsgs', cr('notify_sender_templ')); |
---|
| 6792 | } |
---|
| 6793 | # generate delivery status notification according to rfc3462 |
---|
| 6794 | # and rfc3464 if needed |
---|
| 6795 | $notification = delivery_status_notification($conn, $msginfo, |
---|
| 6796 | $warnsender_with_pass, \%builtins, $dsnmsgref) if $dsnmsgref; |
---|
| 6797 | snmp_count($which_dsn_counter) if defined $notification; |
---|
| 6798 | } |
---|
| 6799 | if (defined $notification) { # dsn needed, send delivery notification |
---|
| 6800 | mail_dispatch($conn, $notification, 1); |
---|
| 6801 | snmp_count('OutDsnMsgs'); |
---|
| 6802 | my($n_smtp_resp, $n_exit_code, $n_dsn_needed) = |
---|
| 6803 | one_response_for_all($notification, 0, $am_id); # check status |
---|
| 6804 | if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # dsn successful? |
---|
| 6805 | $msginfo->dsn_sent(1); # mark the message as bounced |
---|
| 6806 | } elsif ($n_smtp_resp =~ /^4/) { |
---|
| 6807 | snmp_count('OutDsnTempFails'); |
---|
| 6808 | die sprintf("temporarily unable to send DSN to <%s>: %s", |
---|
| 6809 | $msginfo->sender_contact, $n_smtp_resp); |
---|
| 6810 | } else { |
---|
| 6811 | snmp_count('OutDsnRejects'); |
---|
| 6812 | do_log(-1,sprintf("NOTICE: UNABLE TO SEND DSN to <%s>: %s", |
---|
| 6813 | $msginfo->sender, $n_smtp_resp)); |
---|
| 6814 | # # if dsn can not be sent, try to send it to postmaster |
---|
| 6815 | # $notification->recips(['postmaster']); |
---|
| 6816 | # # attempt double bounce |
---|
| 6817 | # mail_dispatch($conn, $notification, 1); |
---|
| 6818 | } |
---|
| 6819 | # $notification->purge; |
---|
| 6820 | } |
---|
| 6821 | } |
---|
| 6822 | prolong_timer($which_section); |
---|
| 6823 | |
---|
| 6824 | # generate customized log report at log level 0 - this is usually the |
---|
| 6825 | # only log entry interesting to administrators during normal operation |
---|
| 6826 | $which_section = 'main_log_entry'; |
---|
| 6827 | my(%mybuiltins) = %builtins; # make a local copy |
---|
| 6828 | { # do a per-mail log entry |
---|
| 6829 | my($s) = $spam_status; $s =~ s/^tests=//; my(@s) = split(/,/,$s); |
---|
| 6830 | if (@s > 10) { $#s = 9; push(@s,"...") } |
---|
| 6831 | $mybuiltins{'T'} = \@s; # macro %T has overloaded semantics, ugly |
---|
| 6832 | my($strr) = expand(cr('log_templ'), \%mybuiltins); |
---|
| 6833 | for my $logline (split(/[ \t]*\n/, $$strr)) { |
---|
| 6834 | do_log(0, $logline) if $logline ne ''; |
---|
| 6835 | } |
---|
| 6836 | } |
---|
| 6837 | if (c('log_recip_templ') ne '') { # do per-recipient log entries |
---|
| 6838 | # redefine macros with a by-recipient semantics |
---|
| 6839 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 6840 | # recipient counter in macro %. may indicate to the template |
---|
| 6841 | # that a per-recipient expansion semantics is expected |
---|
| 6842 | $mybuiltins{'.'}++; |
---|
| 6843 | my($recip) = $r->recip_addr; |
---|
| 6844 | my($smtp_resp) = $r->recip_smtp_response; |
---|
| 6845 | my($qrecip_addr) = scalar(qquote_rfc2821_local($recip)); |
---|
| 6846 | $mybuiltins{'D'} = $mybuiltins{'O'} = $mybuiltins{'N'} = undef; |
---|
| 6847 | if ($r->recip_destiny==D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)){ |
---|
| 6848 | $mybuiltins{'D'} = $qrecip_addr; |
---|
| 6849 | } else { |
---|
| 6850 | $mybuiltins{'O'} = $qrecip_addr; |
---|
| 6851 | my($remote_mta) = $r->recip_remote_mta; |
---|
| 6852 | $mybuiltins{'N'} = sprintf("%s:%s\n %s", $qrecip_addr, |
---|
| 6853 | ($remote_mta eq '' ? '' : " $remote_mta said:"), $smtp_resp); |
---|
| 6854 | } |
---|
| 6855 | my($blacklisted) = $r->recip_blacklisted_sender; |
---|
| 6856 | my($whitelisted) = $r->recip_whitelisted_sender; |
---|
| 6857 | my($boost) = $r->recip_score_boost; |
---|
| 6858 | my($is_local,$tag_level,$tag2_level,$kill_level); |
---|
| 6859 | $is_local = lookup(0,$recip, @{ca('local_domains_maps')}); |
---|
| 6860 | $tag_level = lookup(0,$recip, @{ca('spam_tag_level_maps')}); |
---|
| 6861 | $tag2_level = lookup(0,$recip, @{ca('spam_tag2_level_maps')}); |
---|
| 6862 | $kill_level = lookup(0,$recip, @{ca('spam_kill_level_maps')}); |
---|
| 6863 | my($do_tag) = |
---|
| 6864 | $blacklisted || !defined $tag_level || |
---|
| 6865 | (defined $spam_level ? $spam_level+$boost >= $tag_level |
---|
| 6866 | : $whitelisted ? (-10 >= $tag_level) : 0); |
---|
| 6867 | my($do_tag2) = !$whitelisted && |
---|
| 6868 | ( $blacklisted || |
---|
| 6869 | (defined $spam_level && defined $tag2_level ? |
---|
| 6870 | $spam_level+$boost >= $tag2_level : 0) ); |
---|
| 6871 | my($do_kill) = !$whitelisted && |
---|
| 6872 | ( $blacklisted || |
---|
| 6873 | (defined $spam_level && defined $kill_level ? |
---|
| 6874 | $spam_level+$boost >= $kill_level : 0) ); |
---|
| 6875 | for ($do_tag,$do_tag2,$do_kill) { $_ = $_ ? 'Y' : '0' } # normalize |
---|
| 6876 | for ($is_local) { $_ = $_ ? 'L' : '0' } # normalize |
---|
| 6877 | for ($tag_level,$tag2_level,$kill_level) { $_ = 'x' if !defined($_) } |
---|
| 6878 | $mybuiltins{'R'} = $recip; |
---|
| 6879 | $mybuiltins{'c'} = !defined $spam_level ? '-' |
---|
| 6880 | : 0+sprintf("%.3f",$spam_level+$boost); |
---|
| 6881 | @mybuiltins{('0','1','2','k')} = ($is_local,$do_tag,$do_tag2,$do_kill); |
---|
| 6882 | # macros %3, %4, %5 are experimental, until a better solution is found |
---|
| 6883 | @mybuiltins{('3','4','5')} = ($tag_level,$tag2_level,$kill_level); |
---|
| 6884 | my($strr) = expand(cr('log_recip_templ'), \%mybuiltins); |
---|
| 6885 | for my $logline (split(/[ \t]*\n/, $$strr)) { |
---|
| 6886 | do_log(0, $logline) if $logline ne ''; |
---|
| 6887 | } |
---|
| 6888 | } |
---|
| 6889 | } |
---|
| 6890 | section_time($which_section); |
---|
| 6891 | |
---|
| 6892 | $which_section = 'finishing'; |
---|
| 6893 | $snmp_db->update_counters if defined $snmp_db; |
---|
| 6894 | section_time('update_snmp'); |
---|
| 6895 | |
---|
| 6896 | }; # end eval |
---|
| 6897 | if ($@ ne '') { |
---|
| 6898 | chomp($@); |
---|
[929bb42] | 6899 | $preserve_evidence = 0; |
---|
[c5c522c] | 6900 | my($msg) = "$which_section FAILED: $@"; |
---|
| 6901 | do_log(-2, "TROUBLE in check_mail: $msg"); |
---|
| 6902 | $smtp_resp = "451 4.5.0 Error in processing, id=$am_id, $msg"; |
---|
| 6903 | $exit_code = EX_TEMPFAIL; |
---|
| 6904 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 6905 | next if $r->recip_done; |
---|
| 6906 | $r->recip_smtp_response($smtp_resp); $r->recip_done(1); |
---|
| 6907 | } |
---|
| 6908 | } |
---|
| 6909 | # if ($hold ne '') { |
---|
| 6910 | # do_log(-1, "NOTICE: Evidence is to be preserved: $hold"); |
---|
| 6911 | # $preserve_evidence = 1; |
---|
| 6912 | # } |
---|
| 6913 | if (!$preserve_evidence && debug_oneshot()) { |
---|
| 6914 | do_log(0, "DEBUG_ONESHOT CAUSES EVIDENCE TO BE PRESERVED"); |
---|
| 6915 | $preserve_evidence = 1; |
---|
| 6916 | } |
---|
| 6917 | |
---|
| 6918 | my($which_counter) = 'InUnknown'; |
---|
| 6919 | if ($smtp_resp =~ /^4/) { $which_counter = 'InTempFails' } |
---|
| 6920 | elsif ($smtp_resp =~ /^5/) { $which_counter = 'InRejects' } |
---|
| 6921 | elsif ($smtp_resp =~ /^2/) { |
---|
| 6922 | my($dsn_sent) = $msginfo->dsn_sent; |
---|
| 6923 | if (!$dsn_sent) { $which_counter = $msginfo->delivery_method ne '' |
---|
| 6924 | ? 'InAccepts' : 'InContinues' } |
---|
| 6925 | elsif ($dsn_sent==1) { $which_counter = 'InBounces' } |
---|
| 6926 | elsif ($dsn_sent==2) { $which_counter = 'InDiscards' } |
---|
| 6927 | } |
---|
| 6928 | snmp_count($which_counter); |
---|
| 6929 | $snmp_db->register_proc('.') if defined $snmp_db; # content checking done |
---|
| 6930 | |
---|
| 6931 | $MSGINFO = undef; # release global reference to msginfo object |
---|
| 6932 | ($smtp_resp, $exit_code, $preserve_evidence); |
---|
| 6933 | } |
---|
| 6934 | |
---|
| 6935 | # Ensure we have $msginfo->$entity defined when we expect we'll need it, |
---|
| 6936 | # e.g. to construct notifications. While at it, also get us some additional |
---|
| 6937 | # information on sender from the header. |
---|
| 6938 | # |
---|
| 6939 | sub ensure_mime_entity($$$$$) { |
---|
| 6940 | my($msginfo, $fh, $tempdir, $virusname_list, $parts_root) = @_; |
---|
| 6941 | if (!defined($msginfo->mime_entity)) { |
---|
| 6942 | # header may not have been parsed yet, e.g. if the result was cached |
---|
| 6943 | my($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root); |
---|
| 6944 | $msginfo->mime_entity($ent); |
---|
| 6945 | prolong_timer("ensure_mime_entity"); |
---|
| 6946 | } |
---|
| 6947 | } |
---|
| 6948 | |
---|
| 6949 | sub add_forwarding_header_edits_common($$$$$$) { |
---|
| 6950 | my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable, |
---|
| 6951 | $virus_presence_checked, $spam_presence_checked, $mail_defanged) = @_; |
---|
| 6952 | |
---|
| 6953 | $hdr_edits->prepend_header('Received', |
---|
| 6954 | received_line($conn,$msginfo,am_id(),1), 1) |
---|
| 6955 | if $insert_received_line && $msginfo->delivery_method ne ''; |
---|
| 6956 | # discard existing X-Amavis-Hold header field, only allow our own |
---|
| 6957 | $hdr_edits->delete_header('X-Amavis-Hold'); |
---|
| 6958 | if ($hold ne '') { |
---|
| 6959 | $hdr_edits->append_header('X-Amavis-Hold', $hold); |
---|
| 6960 | do_log(-1, "Inserting header field: X-Amavis-Hold: $hold"); |
---|
| 6961 | } |
---|
| 6962 | if ($mail_defanged ne '') { |
---|
| 6963 | # prepend Resent-* header fields, they must precede |
---|
| 6964 | # corresponding Received header field (pushed in reverse order) |
---|
| 6965 | $hdr_edits->prepend_header('Resent-Message-ID', |
---|
| 6966 | sprintf('<RE%s@%s>',am_id(),$myhostname) ); |
---|
| 6967 | $hdr_edits->prepend_header('Resent-Date', |
---|
| 6968 | rfc2822_timestamp($msginfo->rx_time)); |
---|
| 6969 | $hdr_edits->prepend_header('Resent-From', c('hdrfrom_notify_recip')); |
---|
| 6970 | # append X-Amavis-Modified |
---|
| 6971 | my($msg) = "$mail_defanged by $myhostname"; |
---|
| 6972 | $hdr_edits->append_header('X-Amavis-Modified', $msg); |
---|
| 6973 | do_log(1, "Inserting header field: X-Amavis-Modified: $msg"); |
---|
| 6974 | } |
---|
| 6975 | if ($extra_code_antivirus) { |
---|
| 6976 | $hdr_edits->delete_header('X-Amavis-Alert'); |
---|
| 6977 | $hdr_edits->delete_header(c('X_HEADER_TAG')) |
---|
| 6978 | if c('remove_existing_x_scanned_headers') && |
---|
| 6979 | (c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/); |
---|
| 6980 | } |
---|
| 6981 | if ($extra_code_antispam) { |
---|
| 6982 | if (c('remove_existing_spam_headers')) { |
---|
| 6983 | my(@which_headers) = qw( |
---|
| 6984 | X-Spam-Status X-Spam-Level X-Spam-Flag X-Spam-Score |
---|
| 6985 | X-Spam-Report X-Spam-Checker-Version X-Spam-Tests); |
---|
| 6986 | push(@which_headers, qw(X-DSPAM-Result X-DSPAM-Signature X-DSPAM-User |
---|
| 6987 | X-DSPAM-Probability) ) if defined $dspam; |
---|
| 6988 | for my $h (@which_headers) { $hdr_edits->delete_header($h) } |
---|
| 6989 | } |
---|
| 6990 | # $hdr_edits->append_header('X-Spam-Checker-Version', |
---|
| 6991 | # sprintf("SpamAssassin %s (%s) on %s", Mail::SpamAssassin::Version(), |
---|
| 6992 | # $Mail::SpamAssassin::SUB_VERSION, $myhostname)); |
---|
| 6993 | } |
---|
| 6994 | $hdr_edits; |
---|
| 6995 | } |
---|
| 6996 | |
---|
| 6997 | # Prepare header edits for the first not-yet-done recipient. |
---|
| 6998 | # Inspect remaining recipients, returning the list of recipient objects |
---|
| 6999 | # that are receiving the same set of header edits (so the message may be |
---|
| 7000 | # delivered to them in one SMTP transaction). |
---|
| 7001 | # |
---|
| 7002 | sub add_forwarding_header_edits_per_recip($$$$$$$$$) { |
---|
| 7003 | my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable, |
---|
| 7004 | $virus_presence_checked, $spam_presence_checked, |
---|
| 7005 | $mail_defanged, $filter) = @_; |
---|
| 7006 | my(@recip_cluster); |
---|
| 7007 | my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
| 7008 | @{$msginfo->per_recip_data}; |
---|
| 7009 | my($per_recip_data_len) = scalar(@per_recip_data); |
---|
| 7010 | my($first) = 1; my($cluster_key); my($cluster_full_spam_status); |
---|
| 7011 | for my $r (@per_recip_data) { |
---|
| 7012 | my($recip) = $r->recip_addr; |
---|
| 7013 | my($is_local,$blacklisted,$whitelisted,$boost,$tag_level,$tag2_level, |
---|
| 7014 | $do_tag_virus_checked,$do_tag_virus,$do_tag_banned,$do_tag_badh, |
---|
| 7015 | $do_tag,$do_tag2,$do_subj,$do_subj_u,$subject_tag,$subject_tag2); |
---|
| 7016 | $is_local = lookup(0,$recip, @{ca('local_domains_maps')}); |
---|
| 7017 | $do_tag_badh = @bad_headers && |
---|
| 7018 | !lookup(0,$recip,@{ca('bypass_header_checks_maps')}); |
---|
| 7019 | $do_tag_banned= @banned_filename && |
---|
| 7020 | !lookup(0,$recip,@{ca('bypass_banned_checks_maps')}); |
---|
| 7021 | $do_tag_virus = @virusname && |
---|
| 7022 | !lookup(0,$recip,@{ca('bypass_virus_checks_maps')}); |
---|
| 7023 | $do_tag_virus_checked = |
---|
| 7024 | $virus_presence_checked && |
---|
| 7025 | (c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/) && |
---|
| 7026 | !lookup(0,$recip,@{ca('bypass_virus_checks_maps')}); |
---|
| 7027 | if ($extra_code_antispam) { |
---|
| 7028 | my($bypassed); |
---|
| 7029 | $blacklisted = $r->recip_blacklisted_sender; |
---|
| 7030 | $whitelisted = $r->recip_whitelisted_sender; |
---|
| 7031 | $boost = $r->recip_score_boost; |
---|
| 7032 | $bypassed = lookup(0,$recip, @{ca('bypass_spam_checks_maps')}); |
---|
| 7033 | $tag_level = lookup(0,$recip, @{ca('spam_tag_level_maps')}); |
---|
| 7034 | $tag2_level = lookup(0,$recip, @{ca('spam_tag2_level_maps')}); |
---|
| 7035 | # spam-related headers should _not_ be inserted for: |
---|
| 7036 | # - nonlocal recipients (outgoing mail), as a matter of courtesy |
---|
| 7037 | # to our users; |
---|
| 7038 | # - recipients matching bypass_spam_checks: even though spam checking |
---|
| 7039 | # may have been done for other reasons, these recipients do not |
---|
| 7040 | # expect such headers, so let's pretend the check has not been done |
---|
| 7041 | # and not insert spam-related headers for them |
---|
| 7042 | $do_tag = $is_local && !$bypassed && |
---|
| 7043 | ( $blacklisted || !defined $tag_level || |
---|
| 7044 | (defined $spam_level ? $spam_level+$boost >= $tag_level |
---|
| 7045 | : $whitelisted ? (-10 >= $tag_level) : 0) ); |
---|
| 7046 | $do_tag2 = $is_local && !$bypassed && !$whitelisted && |
---|
| 7047 | ( $blacklisted || |
---|
| 7048 | (defined $spam_level && defined $tag2_level ? |
---|
| 7049 | $spam_level+$boost >= $tag2_level : 0) ); |
---|
| 7050 | $subject_tag2 = !$do_tag2 ? undef |
---|
| 7051 | : lookup(0,$recip, @{ca('spam_subject_tag2_maps')}); |
---|
| 7052 | $subject_tag = !($do_tag||$do_tag2) ? undef |
---|
| 7053 | : lookup(0,$recip, @{ca('spam_subject_tag_maps')}); |
---|
| 7054 | $do_subj = ($subject_tag2 ne '' || $subject_tag ne '') && |
---|
| 7055 | lookup(0,$recip, @{ca('spam_modifies_subj_maps')}); |
---|
| 7056 | } |
---|
| 7057 | if ($hold ne '' || $any_undecipherable) { # adding *UNCHECKED* subject tag? |
---|
| 7058 | $do_subj_u = $is_local && c('undecipherable_subject_tag') ne '' && |
---|
| 7059 | !(@virusname && !lookup(0,$recip, |
---|
| 7060 | @{ca('bypass_virus_checks_maps')}) ); |
---|
| 7061 | } |
---|
| 7062 | # normalize |
---|
| 7063 | for ($do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh, |
---|
| 7064 | $do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local) { $_ = $_?1:0 } |
---|
| 7065 | my($spam_level_bar, $full_spam_status); |
---|
| 7066 | if ($do_tag || $do_tag2) { |
---|
| 7067 | my($slc) = c('sa_spam_level_char'); |
---|
| 7068 | $spam_level_bar = |
---|
| 7069 | $slc x min($blacklisted ? 64 : $spam_level+$boost, 64) if $slc ne ''; |
---|
| 7070 | my($s) = $spam_status; $s =~ s/,/,\n /g; # allow header field wrapping |
---|
| 7071 | $full_spam_status = sprintf("%s,\n hits=%s\n%s%s %s%s", |
---|
| 7072 | $do_tag2 ? 'Yes' : 'No', |
---|
| 7073 | !defined $spam_level ? 'x' : 0+sprintf("%.3f",$spam_level+$boost), |
---|
| 7074 | !defined $tag_level ? '' : sprintf(" tagged_above=%s\n",$tag_level), |
---|
| 7075 | !defined $tag2_level ? '' : sprintf(" required=%s\n", $tag2_level), |
---|
| 7076 | join('', $blacklisted ? "BLACKLISTED\n " : (), |
---|
| 7077 | $whitelisted ? "WHITELISTED\n " : ()), |
---|
| 7078 | $s); |
---|
| 7079 | } |
---|
| 7080 | my($key) = join("\000", |
---|
| 7081 | $do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh, |
---|
| 7082 | $do_tag, $do_tag2, $do_subj, $do_subj_u, $spam_level_bar, |
---|
| 7083 | $full_spam_status); |
---|
| 7084 | if ($first) { |
---|
| 7085 | ll(4) && do_log(4,sprintf( |
---|
| 7086 | "headers CLUSTERING: NEW CLUSTER <%s>: ". |
---|
| 7087 | "hits=%s, tag=%s, tag2=%s, subj=%s, subj_u=%s, local=%s, bl=%s", |
---|
| 7088 | $recip, |
---|
| 7089 | (!defined $spam_level ? 'x' |
---|
| 7090 | : !defined $boost ? $spam_level |
---|
| 7091 | : $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost), |
---|
| 7092 | $do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local, $blacklisted)); |
---|
| 7093 | $cluster_key = $key; $cluster_full_spam_status = $full_spam_status; |
---|
| 7094 | } elsif ($key eq $cluster_key) { |
---|
| 7095 | do_log(5,"headers CLUSTERING: <$recip> joining cluster"); |
---|
| 7096 | } else { |
---|
| 7097 | do_log(5,"headers CLUSTERING: skipping <$recip> (tag=$do_tag, tag2=$do_tag2)"); |
---|
| 7098 | next; # this recipient will be handled in some later pass |
---|
| 7099 | } |
---|
| 7100 | |
---|
| 7101 | if ($first) { # insert headers required for the new cluster |
---|
| 7102 | if ($do_tag_virus_checked) { |
---|
| 7103 | $hdr_edits->append_header(c('X_HEADER_TAG'), c('X_HEADER_LINE')); |
---|
| 7104 | } |
---|
| 7105 | if ($do_tag_virus) { |
---|
| 7106 | $hdr_edits->append_header('X-Amavis-Alert', |
---|
| 7107 | "INFECTED, message contains virus:\n " . join(",\n ",@virusname), 1); |
---|
| 7108 | } |
---|
| 7109 | if ($do_tag_banned) { |
---|
| 7110 | my(@b) = @banned_filename>3 ? @banned_filename[0..2] :@banned_filename; |
---|
| 7111 | my($msg) = "BANNED, message contains " |
---|
| 7112 | . (@banned_filename==1 ? 'part' : 'parts') . ":\n " |
---|
| 7113 | . join(",\n ", @b) . (@banned_filename > @b ? ", ..." : ""); |
---|
| 7114 | $hdr_edits->append_header('X-Amavis-Alert', $msg, 1); |
---|
| 7115 | } |
---|
| 7116 | if ($do_tag_badh) { |
---|
| 7117 | $hdr_edits->append_header('X-Amavis-Alert', |
---|
| 7118 | 'BAD HEADER '.$bad_headers[0], 1); |
---|
| 7119 | } |
---|
| 7120 | if ($do_tag) { |
---|
| 7121 | $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1); |
---|
| 7122 | # $hdr_edits->append_header('X-Spam-Score', |
---|
| 7123 | # !defined $spam_level ? '-' : 0+sprintf("%.3f",$spam_level+$boost) ); |
---|
| 7124 | $hdr_edits->append_header('X-Spam-Level', |
---|
| 7125 | $spam_level_bar) if defined $spam_level_bar; |
---|
| 7126 | } |
---|
| 7127 | if ($do_tag2) { |
---|
| 7128 | $hdr_edits->append_header('X-Spam-Flag', 'YES'); |
---|
| 7129 | $hdr_edits->append_header('X-Spam-Report', $spam_report,1) |
---|
| 7130 | if $spam_report ne '' && c('sa_spam_report_header'); |
---|
| 7131 | } |
---|
| 7132 | if ($do_subj || $do_subj_u) { |
---|
| 7133 | my($s) = ''; |
---|
| 7134 | if ($do_subj_u) { |
---|
| 7135 | $s = c('undecipherable_subject_tag'); |
---|
| 7136 | do_log(3,"adding $s, $any_undecipherable, $hold"); |
---|
| 7137 | } |
---|
| 7138 | if ($do_subj) { |
---|
| 7139 | $s .= $do_tag2 && $subject_tag2 ne '' ? $subject_tag2 : $subject_tag; |
---|
| 7140 | } |
---|
| 7141 | my($entity) = $msginfo->mime_entity; |
---|
| 7142 | if (defined $entity && defined $entity->head->get('Subject',0)) { |
---|
| 7143 | $hdr_edits->edit_header('Subject', |
---|
| 7144 | sub { $_[1]=~/^([ \t]?)(.*)\z/s; ' '.$s.$2 }); |
---|
| 7145 | } else { # no Subject header field present, insert one |
---|
| 7146 | $s =~ s/[ \t]+\z//; # trim |
---|
| 7147 | $hdr_edits->append_header('Subject', $s); |
---|
| 7148 | if (!defined $entity) { |
---|
| 7149 | do_log(-1,"WARN: no MIME entity!? Inserting 'Subject'"); |
---|
| 7150 | } else { |
---|
| 7151 | do_log(0,"INFO: no existing header field 'Subject', inserting it"); |
---|
| 7152 | } |
---|
| 7153 | } |
---|
| 7154 | } |
---|
| 7155 | } |
---|
| 7156 | push(@recip_cluster,$r); $first = 0; |
---|
| 7157 | |
---|
| 7158 | my($delim) = c('recipient_delimiter'); |
---|
| 7159 | if ($delim ne '' && $is_local) { |
---|
| 7160 | # append address extensions to mailbox names if desired |
---|
| 7161 | my($ext_map) = $do_tag_virus ? ca('addr_extension_virus_maps') |
---|
| 7162 | : $do_tag_banned ? ca('addr_extension_banned_maps') |
---|
| 7163 | : $do_tag2 ? ca('addr_extension_spam_maps') |
---|
| 7164 | : $do_tag_badh ? ca('addr_extension_bad_header_maps') |
---|
| 7165 | : undef; |
---|
| 7166 | my($ext) = !ref($ext_map) ? undef : lookup(0,$recip, @$ext_map); |
---|
| 7167 | if ($ext ne '') { |
---|
| 7168 | my($localpart,$domain) = split_address($recip); |
---|
| 7169 | if (c('replace_existing_extension')) # strip existing address extensions |
---|
| 7170 | { $localpart =~ s/^(.*?)\Q$delim\E.*\z/$1/s } |
---|
| 7171 | do_log(5,"adding address extension $delim$ext to $localpart$domain"); |
---|
| 7172 | $r->recip_addr_modified($localpart.$delim.$ext.$domain); |
---|
| 7173 | } |
---|
| 7174 | } |
---|
| 7175 | } |
---|
| 7176 | my($done_all); |
---|
| 7177 | if (@recip_cluster == $per_recip_data_len) { |
---|
| 7178 | do_log(5,"headers CLUSTERING: " . |
---|
| 7179 | "done all $per_recip_data_len recips in one go"); |
---|
| 7180 | $done_all = 1; |
---|
| 7181 | } else { |
---|
| 7182 | ll(4) && do_log(4,sprintf( |
---|
| 7183 | "headers CLUSTERING: got %d recips out of %d: %s", |
---|
| 7184 | scalar(@recip_cluster), $per_recip_data_len, |
---|
| 7185 | join(", ", map { "<" . $_->recip_addr . ">" } @recip_cluster) )); |
---|
| 7186 | } |
---|
| 7187 | if (defined($cluster_full_spam_status) && @recip_cluster) { |
---|
| 7188 | my($s) = $cluster_full_spam_status; $s =~ s/\n[ \t]/ /g; |
---|
| 7189 | ll(2) && do_log(2,sprintf("SPAM-TAG, %s -> %s, %s", |
---|
| 7190 | qquote_rfc2821_local($msginfo->sender), |
---|
| 7191 | join(',', qquote_rfc2821_local( |
---|
| 7192 | map { $_->recip_addr } @recip_cluster)), $s)); |
---|
| 7193 | } |
---|
| 7194 | ($hdr_edits, \@recip_cluster, $done_all); |
---|
| 7195 | } |
---|
| 7196 | |
---|
| 7197 | sub do_quarantine($$$$$;$) { |
---|
| 7198 | my($conn,$msginfo,$hdr_edits,$recips_ref,$quarantine_method,$snmp_id) = @_; |
---|
| 7199 | if ($quarantine_method eq '') { do_log(5, "quarantine disabled") } |
---|
| 7200 | else { |
---|
| 7201 | # fudge to make %b access the body_digest of $msginfo, not of $quar_msg |
---|
| 7202 | $quarantine_method =~ s/%b/$msginfo->body_digest/eg; |
---|
| 7203 | my($sender) = $msginfo->sender; |
---|
| 7204 | my($quar_msg) = Amavis::In::Message->new; |
---|
| 7205 | $quar_msg->rx_time($msginfo->rx_time); # copy the reception time |
---|
| 7206 | $quar_msg->delivery_method($quarantine_method); |
---|
| 7207 | if ($quarantine_method =~ /^bsmtp:/i) { |
---|
| 7208 | $quar_msg->sender($sender); # original sender & recipients |
---|
| 7209 | $quar_msg->recips($msginfo->recips); |
---|
| 7210 | } else { |
---|
| 7211 | my($mftq) = c('mailfrom_to_quarantine'); |
---|
| 7212 | $quar_msg->sender(defined $mftq ? $mftq : $sender); |
---|
| 7213 | $quar_msg->recips($recips_ref); # e.g. per-recip quarantine |
---|
| 7214 | # NOTE: RFC2821 mentions possible headers X-SMTP-MAIL and X-SMTP-RCPT |
---|
| 7215 | # Exim uses: Envelope-To, Sendmail uses X-Envelope-To; |
---|
| 7216 | # No need with bsmtp, which already carries addresses in the envelope |
---|
| 7217 | $hdr_edits->prepend_header('X-Envelope-To', |
---|
| 7218 | join(",\n ", qquote_rfc2821_local(@{$msginfo->recips})), 1); |
---|
| 7219 | $hdr_edits->prepend_header('X-Envelope-From', |
---|
| 7220 | qquote_rfc2821_local($sender)); |
---|
| 7221 | } |
---|
| 7222 | do_log(5, "DO_QUARANTINE, sender: " . $quar_msg->sender); |
---|
| 7223 | $quar_msg->auth_submitter(qquote_rfc2821_local($quar_msg->sender)); |
---|
| 7224 | $quar_msg->auth_user(c('amavis_auth_user')); |
---|
| 7225 | $quar_msg->auth_pass(c('amavis_auth_pass')); |
---|
| 7226 | $quar_msg->header_edits($hdr_edits); |
---|
| 7227 | $quar_msg->mail_text($msginfo->mail_text); # use the same mail contents |
---|
| 7228 | |
---|
| 7229 | snmp_count('QuarMsgs'); |
---|
| 7230 | mail_dispatch($conn, $quar_msg, 1); |
---|
| 7231 | my($n_smtp_resp, $n_exit_code, $n_dsn_needed) = |
---|
| 7232 | one_response_for_all($quar_msg, 0, am_id()); # check status |
---|
| 7233 | if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok |
---|
| 7234 | snmp_count($snmp_id eq '' ? 'QuarOther' : $snmp_id); |
---|
| 7235 | } elsif ($n_smtp_resp =~ /^4/) { |
---|
| 7236 | snmp_count('QuarAttemptTempFails'); |
---|
| 7237 | die "temporarily unable to quarantine: $n_smtp_resp"; |
---|
| 7238 | } else { # abort if quarantining not successful |
---|
| 7239 | snmp_count('QuarAttemptFails'); |
---|
| 7240 | die "Can not quarantine: $n_smtp_resp"; |
---|
| 7241 | } |
---|
| 7242 | my(@qa); my(%seen); # collect unique quarantine mailboxes or addresses |
---|
| 7243 | for my $r (@{$quar_msg->per_recip_data}) { |
---|
| 7244 | my($mbxname) = $r->recip_mbxname; |
---|
| 7245 | push(@qa,$mbxname) if $mbxname ne '' && !$seen{$mbxname}++; |
---|
| 7246 | } |
---|
| 7247 | $msginfo->quarantined_to(\@qa); # remember where it was quarantined to |
---|
| 7248 | do_log(5, "DO_QUARANTINE done"); |
---|
| 7249 | } |
---|
| 7250 | } |
---|
| 7251 | |
---|
| 7252 | # If virus/banned/bad-header found - quarantine it and send notifications |
---|
| 7253 | sub do_virus($$$) { |
---|
| 7254 | my($conn, $msginfo, $virus_dejavu) = @_; |
---|
| 7255 | my($q_method, $quarantine_to_maps_ref, |
---|
| 7256 | $bypass_checks_maps_ref, $admin_maps_ref) = |
---|
| 7257 | @virusname ? |
---|
| 7258 | (c('virus_quarantine_method'), |
---|
| 7259 | ca('virus_quarantine_to_maps'), |
---|
| 7260 | ca('bypass_virus_checks_maps'), |
---|
| 7261 | ca('virus_admin_maps') ) |
---|
| 7262 | : @banned_filename ? |
---|
| 7263 | (c('banned_files_quarantine_method'), |
---|
| 7264 | ca('banned_quarantine_to_maps'), |
---|
| 7265 | ca('bypass_banned_checks_maps'), |
---|
| 7266 | ca('banned_admin_maps') ) |
---|
| 7267 | : @bad_headers ? |
---|
| 7268 | (c('bad_header_quarantine_method'), |
---|
| 7269 | ca('bad_header_quarantine_to_maps'), |
---|
| 7270 | ca('bypass_header_checks_maps'), |
---|
| 7271 | ca('bad_header_admin_maps') ) |
---|
| 7272 | : (undef, undef, undef, undef); |
---|
| 7273 | # suggest a name to be used as 'X-Quarantine-Id:' or file name |
---|
| 7274 | $VIRUSFILE = $q_method =~ /^(?:local|bsmtp):(.*)\z/si ? $1 : "virus-%i-%n"; |
---|
| 7275 | $VIRUSFILE =~ s{%(.)} |
---|
| 7276 | { $1 eq 'b' ? $msginfo->body_digest |
---|
| 7277 | : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time)) |
---|
| 7278 | : $1 eq 'n' ? am_id() |
---|
| 7279 | : $1 eq '%' ? '%' : '%'.$1 }egs; |
---|
| 7280 | do_log(5, "do_virus: looking for per-recipient quarantine and admins"); |
---|
| 7281 | my($newvirus_admin_maps_ref) = |
---|
| 7282 | @virusname && !$virus_dejavu ? ca('newvirus_admin_maps') : undef; |
---|
| 7283 | my(@q_addr,@a_addr); # get per-recipient quarantine address(es) and admins |
---|
| 7284 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 7285 | my($rec) = $r->recip_addr; |
---|
| 7286 | my($q); # quarantine (pseudo) address associated with the recipient |
---|
| 7287 | my($a); # administrator's e-mail address |
---|
| 7288 | ($q) = lookup(0,$rec,@$quarantine_to_maps_ref) if $quarantine_to_maps_ref; |
---|
| 7289 | $q = $rec if $q ne '' && $q_method =~ /^bsmtp:/i; # orig.recip when BSMTP |
---|
| 7290 | ($a) = lookup(0,$rec,@$admin_maps_ref) if $admin_maps_ref; |
---|
| 7291 | push(@q_addr, $q) if $q ne '' && !grep {$_ eq $q} @q_addr; |
---|
| 7292 | push(@a_addr, $a) if $a ne '' && !grep {$_ eq $a} @a_addr; |
---|
| 7293 | if ($newvirus_admin_maps_ref) { |
---|
| 7294 | ($a) = lookup(0,$rec,@$newvirus_admin_maps_ref); |
---|
| 7295 | push(@a_addr, $a) if $a ne '' && !grep {$_ eq $a} @a_addr; |
---|
| 7296 | } |
---|
| 7297 | } |
---|
| 7298 | if (@q_addr) { # do the quarantining |
---|
| 7299 | # prepare header edits for the quarantined message |
---|
| 7300 | my($hdr_edits) = Amavis::Out::EditHeader->new; |
---|
| 7301 | $hdr_edits->prepend_header('X-Quarantine-Id', "<$VIRUSFILE>"); |
---|
| 7302 | if (@virusname) { |
---|
| 7303 | $hdr_edits->append_header('X-Amavis-Alert', |
---|
| 7304 | "INFECTED, message contains virus:\n " . join(",\n ", @virusname), 1); |
---|
| 7305 | } |
---|
| 7306 | if (@banned_filename) { |
---|
| 7307 | my(@b) = @banned_filename>3 ? @banned_filename[0..2] : @banned_filename; |
---|
| 7308 | my($msg) = "BANNED, message contains " |
---|
| 7309 | . (@banned_filename==1 ? 'part' : 'parts') . ":\n " |
---|
| 7310 | . join(",\n ", @b) . (@banned_filename > @b ? ", ..." : ""); |
---|
| 7311 | $hdr_edits->append_header('X-Amavis-Alert', $msg, 1); |
---|
| 7312 | } |
---|
| 7313 | if (@bad_headers) { |
---|
| 7314 | $hdr_edits->append_header('X-Amavis-Alert', |
---|
| 7315 | 'BAD HEADER '.$bad_headers[0], 1); |
---|
| 7316 | } |
---|
| 7317 | do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method, |
---|
| 7318 | @virusname ? 'QuarVirusMsgs' : |
---|
| 7319 | @banned_filename ? 'QuarBannedMsgs' : |
---|
| 7320 | @bad_headers ? 'QuarBadHMsgs' : 'QuarOther'); |
---|
| 7321 | } |
---|
| 7322 | my($hdr_edits) = Amavis::Out::EditHeader->new; |
---|
| 7323 | if (!@a_addr) { |
---|
| 7324 | do_log(4, "Skip admin notification, no administrators"); |
---|
| 7325 | } else { # notify per-recipient virus administrators |
---|
| 7326 | ll(5) && do_log(5, sprintf("DO_VIRUS - NOTIFICATIONS to %s; sender: %s", |
---|
| 7327 | join(",",qquote_rfc2821_local(@a_addr)), $msginfo->sender)); |
---|
| 7328 | my($notification) = Amavis::In::Message->new; |
---|
| 7329 | $notification->rx_time($msginfo->rx_time); # copy the reception time |
---|
| 7330 | $notification->delivery_method(c('notify_method')); |
---|
| 7331 | $notification->sender(c('mailfrom_notify_admin')); |
---|
| 7332 | $notification->auth_submitter( |
---|
| 7333 | qquote_rfc2821_local(c('mailfrom_notify_admin'))); |
---|
| 7334 | $notification->auth_user(c('amavis_auth_user')); |
---|
| 7335 | $notification->auth_pass(c('amavis_auth_pass')); |
---|
| 7336 | $notification->recips([@a_addr]); |
---|
| 7337 | my(%mybuiltins) = %builtins; # make a local copy |
---|
| 7338 | $mybuiltins{'T'} = \@a_addr; # used in 'To:' |
---|
| 7339 | $mybuiltins{'f'} = c('hdrfrom_notify_admin'); # From: |
---|
| 7340 | $notification->mail_text( |
---|
| 7341 | string_to_mime_entity(expand(cr('notify_virus_admin_templ'), |
---|
| 7342 | \%mybuiltins))); |
---|
| 7343 | $notification->header_edits($hdr_edits); |
---|
| 7344 | mail_dispatch($conn, $notification, 1); |
---|
| 7345 | my($n_smtp_resp, $n_exit_code, $n_dsn_needed) = |
---|
| 7346 | one_response_for_all($notification, 0, am_id()); # check status |
---|
| 7347 | if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok |
---|
| 7348 | } elsif ($n_smtp_resp =~ /^4/) { |
---|
| 7349 | die "temporarily unable to notify virus admin: $n_smtp_resp"; |
---|
| 7350 | } else { |
---|
| 7351 | do_log(-1, "FAILED to notify virus admin: $n_smtp_resp"); |
---|
| 7352 | } |
---|
| 7353 | # $notification->purge; |
---|
| 7354 | } |
---|
| 7355 | |
---|
| 7356 | # if (! defined($msginfo->sender_contact) ) { |
---|
| 7357 | # do_log(5,"do_virus: skip recipient notifications for unknown senders"); |
---|
| 7358 | # } else { # send notification to recipients |
---|
| 7359 | my(@recips_to_notify) = |
---|
| 7360 | grep { |
---|
| 7361 | @virusname && !lookup(0,$_,@{ca('bypass_virus_checks_maps')}) ? |
---|
| 7362 | scalar(lookup(0,$_,@{ca('warnvirusrecip_maps')})) |
---|
| 7363 | : @banned_filename && !lookup(0,$_,@{ca('bypass_banned_checks_maps')}) ? |
---|
| 7364 | scalar(lookup(0,$_,@{ca('warnbannedrecip_maps')})) |
---|
| 7365 | : @bad_headers && !lookup(0,$_,@{ca('bypass_header_checks_maps')}) ? |
---|
| 7366 | scalar(lookup(0,$_,@{ca('warnbadhrecip_maps')})) |
---|
| 7367 | : 0 } |
---|
| 7368 | grep { c('warn_offsite') || lookup(0,$_,@{ca('local_domains_maps')}) } |
---|
| 7369 | @{$msginfo->recips}; |
---|
| 7370 | if (!@recips_to_notify) { |
---|
| 7371 | do_log(5,"do_virus: recipient notifications not required"); |
---|
| 7372 | } else { |
---|
| 7373 | my($notification) = Amavis::In::Message->new; |
---|
| 7374 | $notification->rx_time($msginfo->rx_time); # copy the reception time |
---|
| 7375 | $notification->delivery_method(c('notify_method')); |
---|
| 7376 | $notification->sender(c('mailfrom_notify_recip')); |
---|
| 7377 | $notification->auth_submitter( |
---|
| 7378 | qquote_rfc2821_local(c('mailfrom_notify_recip'))); |
---|
| 7379 | $notification->auth_user(c('amavis_auth_user')); |
---|
| 7380 | $notification->auth_pass(c('amavis_auth_pass')); |
---|
| 7381 | $notification->recips(\@recips_to_notify); |
---|
| 7382 | my(%mybuiltins) = %builtins; # make a local copy |
---|
| 7383 | $mybuiltins{'f'} = c('hdrfrom_notify_recip'); # 'From:' |
---|
| 7384 | $mybuiltins{'T'} = (@recips_to_notify==1 && $recips_to_notify[0] ne '') ? # 'To:' |
---|
| 7385 | [quote_rfc2821_local($recips_to_notify[0])] : undef; |
---|
| 7386 | $notification->mail_text( |
---|
| 7387 | string_to_mime_entity(expand(cr('notify_virus_recips_templ'), |
---|
| 7388 | \%mybuiltins)) ); |
---|
| 7389 | $notification->header_edits($hdr_edits); |
---|
| 7390 | mail_dispatch($conn, $notification, 1); |
---|
| 7391 | my($n_smtp_resp, $n_exit_code, $n_dsn_needed) = |
---|
| 7392 | one_response_for_all($notification, 0, am_id()); # check status |
---|
| 7393 | if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok |
---|
| 7394 | } elsif ($n_smtp_resp =~ /^4/) { |
---|
| 7395 | die "temporarily unable to notify recipients: $n_smtp_resp"; |
---|
| 7396 | } else { |
---|
| 7397 | do_log(-1, "FAILED to notify recipients: $n_smtp_resp"); |
---|
| 7398 | } |
---|
| 7399 | # $notification->purge; |
---|
| 7400 | } |
---|
| 7401 | # } |
---|
| 7402 | do_log(5, "DO_VIRUS - DONE"); |
---|
| 7403 | } |
---|
| 7404 | |
---|
| 7405 | # |
---|
| 7406 | # If Spam found - quarantine it and log report |
---|
| 7407 | sub do_spam($$) { |
---|
| 7408 | my($conn, $msginfo) = @_; |
---|
| 7409 | # suggest a name to be used as 'X-Quarantine-Id:' or file name |
---|
| 7410 | my($q_method) = c('spam_quarantine_method'); |
---|
| 7411 | $VIRUSFILE = $q_method =~ /^(?:local|bsmtp):(.*)\z/si ? $1 : "spam-%b-%i-%n"; |
---|
| 7412 | $VIRUSFILE =~ s{%(.)} |
---|
| 7413 | { $1 eq 'b' ? $msginfo->body_digest |
---|
| 7414 | : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time)) |
---|
| 7415 | : $1 eq 'n' ? am_id() |
---|
| 7416 | : $1 eq '%' ? '%' : '%'.$1 }egs; |
---|
| 7417 | # use the smallest value as the level reported in quarantined headers! |
---|
| 7418 | my($tag_level) = |
---|
| 7419 | min(map { scalar(lookup(0,$_,@{ca('spam_tag_level_maps')})) } @{$msginfo->recips}); |
---|
| 7420 | my($tag2_level) = |
---|
| 7421 | min(map { scalar(lookup(0,$_,@{ca('spam_tag2_level_maps')})) } @{$msginfo->recips}); |
---|
| 7422 | my($kill_level) = |
---|
| 7423 | min(map { scalar(lookup(0,$_,@{ca('spam_kill_level_maps')})) } @{$msginfo->recips}); |
---|
| 7424 | my($blacklisted) = |
---|
| 7425 | scalar(grep { $_->recip_blacklisted_sender } @{$msginfo->per_recip_data}); |
---|
| 7426 | my($whitelisted) = |
---|
| 7427 | scalar(grep { $_->recip_whitelisted_sender } @{$msginfo->per_recip_data}); |
---|
| 7428 | my($s) = $spam_status; $s =~ s/,/,\n /g; # allow header field wrapping |
---|
| 7429 | my($full_spam_status) = sprintf( |
---|
| 7430 | "%s,\n hits=%s\n tag=%s\n tag2=%s\n kill=%s\n %s%s", |
---|
| 7431 | (defined $spam_level && defined $tag2_level && $spam_level>=$tag2_level ? |
---|
| 7432 | 'Yes' : 'No'), |
---|
| 7433 | (map { !defined $_ ? 'x' : 0+sprintf("%.3f",$_) } |
---|
| 7434 | ($spam_level, $tag_level, $tag2_level, $kill_level)), |
---|
| 7435 | join('', $blacklisted ? "BLACKLISTED\n " : (), |
---|
| 7436 | $whitelisted ? "WHITELISTED\n " : ()), |
---|
| 7437 | $s); |
---|
| 7438 | |
---|
| 7439 | do_log(5, "do_spam: looking for a quarantine address"); |
---|
| 7440 | my(@q_addr,@a_addr); # quarantine address(es) and administrators |
---|
| 7441 | my($sqbsm) = ca('spam_quarantine_bysender_to_maps'); |
---|
| 7442 | if (@$sqbsm) { # by-sender quarantine |
---|
| 7443 | my($a); $a = lookup(0,$msginfo->sender, @$sqbsm); |
---|
| 7444 | push(@q_addr, $a) if $a ne ''; |
---|
| 7445 | } |
---|
| 7446 | # get per-recipient quarantine address(es) and admins |
---|
| 7447 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 7448 | my($rec) = $r->recip_addr; |
---|
| 7449 | my($q); # quarantine (pseudo) address associated with the recipient |
---|
| 7450 | ($q) = lookup(0,$rec, @{ca('spam_quarantine_to_maps')}); |
---|
| 7451 | $q = $rec if $q ne '' && $q_method =~ /^bsmtp:/i; # orig.recip when BSMTP |
---|
| 7452 | my($a) = lookup(0,$rec, @{ca('spam_admin_maps')}); |
---|
| 7453 | push(@q_addr, $q) if $q ne '' && !grep {$_ eq $q} @q_addr; |
---|
| 7454 | push(@a_addr, $a) if $a ne '' && !grep {$_ eq $a} @a_addr; |
---|
| 7455 | } |
---|
| 7456 | if (@q_addr) { # do the quarantining |
---|
| 7457 | # prepare header edits for the quarantined message |
---|
| 7458 | my($hdr_edits) = Amavis::Out::EditHeader->new; |
---|
| 7459 | $hdr_edits->prepend_header('X-Quarantine-Id', "<$VIRUSFILE>"); |
---|
| 7460 | $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1); |
---|
| 7461 | # $hdr_edits->append_header('X-Spam-Score', |
---|
| 7462 | # !defined $spam_level ? 'x' : 0+sprintf("%.3f",$spam_level) ); |
---|
| 7463 | my($slc) = c('sa_spam_level_char'); |
---|
| 7464 | $hdr_edits->append_header('X-Spam-Level', |
---|
| 7465 | $slc x min(0+$spam_level,64)) if $slc ne ''; |
---|
| 7466 | $hdr_edits->append_header('X-Spam-Report', $spam_report,1) |
---|
| 7467 | if c('sa_spam_report_header') && $spam_report ne ''; |
---|
| 7468 | do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method,'QuarSpamMsgs'); |
---|
| 7469 | } |
---|
| 7470 | $s = $full_spam_status; $s =~ s/\n[ \t]/ /g; |
---|
| 7471 | do_log(2,sprintf("SPAM, <%s> -> %s, %s%s", $msginfo->sender_source, |
---|
| 7472 | join(',', qquote_rfc2821_local(@{$msginfo->recips})), $s, |
---|
| 7473 | !@q_addr ? '' : sprintf(", quarantine %s (%s)", |
---|
| 7474 | $VIRUSFILE, join(',', @q_addr)) )); |
---|
| 7475 | if (!@a_addr) { |
---|
| 7476 | do_log(4, "Skip spam admin notification, no administrators"); |
---|
| 7477 | } else { # notify per-recipient spam administrators |
---|
| 7478 | ll(5) && do_log(5, sprintf("DO_SPAM - NOTIFICATIONS to %s; sender: %s", |
---|
| 7479 | join(",",qquote_rfc2821_local(@a_addr)), $msginfo->sender)); |
---|
| 7480 | my($notification) = Amavis::In::Message->new; |
---|
| 7481 | $notification->rx_time($msginfo->rx_time); # copy the reception time |
---|
| 7482 | $notification->delivery_method(c('notify_method')); |
---|
| 7483 | $notification->sender(c('mailfrom_notify_spamadmin')); |
---|
| 7484 | $notification->auth_submitter( |
---|
| 7485 | qquote_rfc2821_local(c('mailfrom_notify_spamadmin'))); |
---|
| 7486 | $notification->auth_user(c('amavis_auth_user')); |
---|
| 7487 | $notification->auth_pass(c('amavis_auth_pass')); |
---|
| 7488 | $notification->recips([@a_addr]); |
---|
| 7489 | my(%mybuiltins) = %builtins; # make a local copy |
---|
| 7490 | $mybuiltins{'T'} = \@a_addr; # used in 'To:' |
---|
| 7491 | $mybuiltins{'f'} = c('hdrfrom_notify_spamadmin'); |
---|
| 7492 | $notification->mail_text( |
---|
| 7493 | string_to_mime_entity(expand(cr('notify_spam_admin_templ'), |
---|
| 7494 | \%mybuiltins))); |
---|
| 7495 | my($hdr_edits) = Amavis::Out::EditHeader->new; |
---|
| 7496 | $notification->header_edits($hdr_edits); |
---|
| 7497 | mail_dispatch($conn, $notification, 1); |
---|
| 7498 | my($n_smtp_resp, $n_exit_code, $n_dsn_needed) = |
---|
| 7499 | one_response_for_all($notification, 0, am_id()); # check status |
---|
| 7500 | if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok |
---|
| 7501 | } elsif ($n_smtp_resp =~ /^4/) { |
---|
| 7502 | die "temporarily unable to notify spam admin: $n_smtp_resp"; |
---|
| 7503 | } else { |
---|
| 7504 | do_log(-1, "FAILED to notify spam admin: $n_smtp_resp"); |
---|
| 7505 | } |
---|
| 7506 | # $notification->purge; |
---|
| 7507 | } |
---|
| 7508 | do_log(5, "DO_SPAM DONE"); |
---|
| 7509 | } |
---|
| 7510 | |
---|
| 7511 | # Calculate message digest; |
---|
| 7512 | # While at it, also get the message size and store original header, |
---|
| 7513 | # since we need it for the %H macro, and MIME::Tools may modify it. |
---|
| 7514 | |
---|
| 7515 | sub get_body_digest($$) { |
---|
| 7516 | my($fh, $msginfo) = @_; |
---|
| 7517 | $fh->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
| 7518 | local($_); |
---|
| 7519 | |
---|
| 7520 | # choose message digest method: |
---|
| 7521 | my($ctx) = Digest::MD5->new; # 128 bits (32 hex digits) |
---|
| 7522 | # my($ctx) = Digest::SHA1->new; # 160 bits (40 hex digits), slightly slower |
---|
| 7523 | |
---|
| 7524 | my(@orig_header); |
---|
| 7525 | my($header_size) = 0; |
---|
| 7526 | my($body_size) = 0; |
---|
| 7527 | while (<$fh>) { # skip mail header |
---|
| 7528 | last if $_ eq $eol; |
---|
| 7529 | $header_size += length($_); |
---|
| 7530 | push(@orig_header, $_); # with trailing EOL |
---|
| 7531 | } |
---|
| 7532 | my($len); |
---|
| 7533 | while (($len = read($fh,$_,16384)) > 0) |
---|
| 7534 | { $ctx->add($_); $body_size += $len } |
---|
| 7535 | my($signature) = $ctx->hexdigest; |
---|
| 7536 | # my($signature) = $ctx->b64digest; |
---|
| 7537 | $signature = untaint($signature) # checked (either 32 or 40 char) |
---|
| 7538 | if $signature =~ /^ [0-9a-fA-F]{32} (?: [0-9a-fA-F]{8} )? \z/x; |
---|
| 7539 | # store information obtained |
---|
| 7540 | $msginfo->orig_header(\@orig_header); |
---|
| 7541 | $msginfo->orig_header_size($header_size); |
---|
| 7542 | $msginfo->orig_body_size($body_size); |
---|
| 7543 | $msginfo->body_digest($signature); |
---|
| 7544 | |
---|
| 7545 | section_time('body_hash'); |
---|
| 7546 | do_log(3, "body hash: $signature"); |
---|
| 7547 | $signature; |
---|
| 7548 | } |
---|
| 7549 | |
---|
| 7550 | sub find_program_path($$$) { |
---|
| 7551 | my($fv_list, $path_list_ref, $may_log) = @_; |
---|
| 7552 | $fv_list = [$fv_list] if !ref $fv_list; |
---|
| 7553 | my($found); |
---|
| 7554 | for my $fv (@$fv_list) { |
---|
| 7555 | my(@fv_cmd) = split(' ',$fv); |
---|
| 7556 | if (!@fv_cmd) { # empty, not available |
---|
| 7557 | } elsif ($fv_cmd[0] =~ /^\//) { # absolute path |
---|
| 7558 | my($errn) = stat($fv_cmd[0]) ? 0 : 0+$!; |
---|
| 7559 | if ($errn == ENOENT) { } |
---|
| 7560 | elsif ($errn) { |
---|
| 7561 | do_log(-1, "find_program_path: " . "$fv_cmd[0] inaccessible: $!") |
---|
| 7562 | if $may_log; |
---|
| 7563 | } elsif (-x _ && !-d _) { $found = join(' ', @fv_cmd) } |
---|
| 7564 | } elsif ($fv_cmd[0] =~ /\//) { # relative path |
---|
| 7565 | die "find_program_path: relative paths not implemented: @fv_cmd\n"; |
---|
| 7566 | } else { # walk through the specified PATH |
---|
| 7567 | for my $p (@$path_list_ref) { |
---|
| 7568 | my($errn) = stat("$p/$fv_cmd[0]") ? 0 : 0+$!; |
---|
| 7569 | if ($errn == ENOENT) { } |
---|
| 7570 | elsif ($errn) { |
---|
| 7571 | do_log(-1, "find_program_path: " . "$p/$fv_cmd[0] inaccessible: $!") |
---|
| 7572 | if $may_log; |
---|
| 7573 | } elsif (-x _ && !-d _) { |
---|
| 7574 | $found = $p . '/' . join(' ', @fv_cmd); |
---|
| 7575 | last; |
---|
| 7576 | } |
---|
| 7577 | } |
---|
| 7578 | } |
---|
| 7579 | last if defined $found; |
---|
| 7580 | } |
---|
| 7581 | $found; |
---|
| 7582 | } |
---|
| 7583 | |
---|
| 7584 | sub find_external_programs($) { |
---|
| 7585 | my($path_list_ref) = @_; |
---|
| 7586 | for my $f (qw($file $arc $gzip $bzip2 $lzop $lha $unarj $uncompress |
---|
| 7587 | $unfreeze $unrar $zoo $pax $cpio $ar $rpm2cpio $cabextract |
---|
| 7588 | $ripole $dspam)) |
---|
| 7589 | { |
---|
| 7590 | my($g) = $f; |
---|
| 7591 | $g =~ s/\$/Amavis::Conf::/; |
---|
| 7592 | my($fv_list) = eval('$' . $g); |
---|
| 7593 | my($found) = find_program_path($fv_list, $path_list_ref, 1); |
---|
| 7594 | { no strict 'refs'; $$g = $found } # NOTE: a symbolic reference |
---|
| 7595 | if (!defined $found) { |
---|
| 7596 | do_log(-1, sprintf("No %-14s not using it", "$f,")); |
---|
| 7597 | } else { |
---|
| 7598 | do_log(0,sprintf("Found %-11s at %s%s", $f, |
---|
| 7599 | $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '', |
---|
| 7600 | $found)); |
---|
| 7601 | } |
---|
| 7602 | } |
---|
| 7603 | # map program name hints to full paths |
---|
| 7604 | my($tier) = 'primary'; # primary, secondary, ... av scanners |
---|
| 7605 | for my $f (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) { |
---|
| 7606 | if ($f eq "\000") { # next tier |
---|
| 7607 | $tier = 'secondary'; |
---|
| 7608 | } elsif (!defined $f || !ref $f) { # empty, skip |
---|
| 7609 | } elsif (ref($f->[1]) eq 'CODE') { |
---|
| 7610 | do_log(0, "Using internal av scanner code for ($tier) " . $f->[0]); |
---|
| 7611 | } else { |
---|
| 7612 | my($found) = $f->[1] = find_program_path($f->[1], $path_list_ref, 1); |
---|
| 7613 | if (!defined $found) { |
---|
| 7614 | do_log(3, "No $tier av scanner: " . $f->[0]); |
---|
| 7615 | $f = undef; # release its storage |
---|
| 7616 | } else { |
---|
| 7617 | do_log(0,sprintf("Found $tier av scanner %-11s at %s%s", $f->[0], |
---|
| 7618 | $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '', |
---|
| 7619 | $found)); |
---|
| 7620 | } |
---|
| 7621 | } |
---|
| 7622 | } |
---|
| 7623 | } |
---|
| 7624 | |
---|
| 7625 | # Fetch all remaining modules. |
---|
| 7626 | sub fetch_modules_extra() { |
---|
| 7627 | my(@modules); |
---|
| 7628 | if ($extra_code_sql) { |
---|
| 7629 | push(@modules, 'DBI'); |
---|
| 7630 | for (@lookup_sql_dsn) { |
---|
| 7631 | my(@dsn) = split(/:/,$_->[0],-1); |
---|
| 7632 | push(@modules, 'DBD::'.$dsn[1]) if uc($dsn[0]) eq 'DBI'; |
---|
| 7633 | } |
---|
| 7634 | } |
---|
| 7635 | push(@modules, qw(Net::LDAP Net::LDAP::Util Net::LDAP::Search)) |
---|
| 7636 | if $extra_code_ldap; |
---|
| 7637 | if (c('bypass_decode_parts') && |
---|
| 7638 | !grep {exists $policy_bank{$_}{'bypass_decode_parts'} && |
---|
| 7639 | !$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) { |
---|
| 7640 | } else { |
---|
| 7641 | push(@modules, qw(Compress::Zlib Convert::TNEF Convert::UUlib |
---|
| 7642 | Archive::Zip Archive::Tar)); |
---|
| 7643 | } |
---|
| 7644 | push(@modules, 'Mail::SpamAssassin') if $extra_code_antispam; |
---|
| 7645 | push(@modules, 'Authen::SASL') if c('auth_required_out'); |
---|
| 7646 | Amavis::Boot::fetch_modules('REQUIRED ADDITIONAL MODULES', 1, @modules); |
---|
| 7647 | my($sa_version); |
---|
| 7648 | $sa_version = Mail::SpamAssassin::Version() if $extra_code_antispam; |
---|
| 7649 | @modules = (); # now start collecting optional modules |
---|
| 7650 | if ($unicode_aware) { |
---|
| 7651 | push(@modules, qw( |
---|
| 7652 | bytes bytes_heavy.pl utf8 utf8_heavy.pl Encode Encode::Byte |
---|
| 7653 | unicore::Canonical.pl unicore::Exact.pl unicore::PVA.pl |
---|
| 7654 | unicore::To::Fold.pl unicore::To::Title.pl |
---|
| 7655 | unicore::To::Lower.pl unicore::To::Upper.pl |
---|
| 7656 | )); |
---|
| 7657 | } |
---|
| 7658 | if ($extra_code_antispam) { # must be loaded before chroot takes place |
---|
| 7659 | push(@modules, qw( |
---|
| 7660 | Mail::SpamAssassin::Locker::Flock |
---|
| 7661 | Mail::SpamAssassin::Locker::UnixNFSSafe |
---|
| 7662 | Mail::SpamAssassin::DBBasedAddrList |
---|
| 7663 | Mail::SpamAssassin::SQLBasedAddrList |
---|
| 7664 | Mail::SpamAssassin::BayesStore::DBM |
---|
| 7665 | Mail::SpamAssassin::BayesStore::SQL |
---|
| 7666 | Mail::SpamAssassin::Plugin::SPF |
---|
| 7667 | Mail::SpamAssassin::Plugin::URIDNSBL |
---|
| 7668 | Mail::SpamAssassin::Plugin::Hashcash |
---|
| 7669 | Mail::SpamAssassin::PerMsgLearner |
---|
| 7670 | |
---|
| 7671 | DBD::mysql Sys::Hostname::Long |
---|
| 7672 | Mail::SPF::Query Razor2::Client Net::CIDR::Lite |
---|
| 7673 | Net::DNS::RR::SOA Net::DNS::RR::NS Net::DNS::RR::MX |
---|
| 7674 | Net::DNS::RR::A Net::DNS::RR::AAAA Net::DNS::RR::PTR |
---|
| 7675 | Net::DNS::RR::CNAME Net::DNS::RR::TXT Net::Ping |
---|
| 7676 | )); |
---|
| 7677 | # ??? ArchiveIterator Reporter Plugin::RelayCountry |
---|
| 7678 | # ??? Mail::SpamAssassin::Plugin::Razor2 |
---|
| 7679 | } |
---|
| 7680 | # *** note that $sa_version could be 3.0.1, which is not really numeric! |
---|
| 7681 | if ($extra_code_antispam && defined $sa_version && $sa_version < 3) { |
---|
| 7682 | push(@modules, qw( |
---|
| 7683 | Mail::SpamAssassin::UnixLocker Mail::SpamAssassin::BayesStoreDBM |
---|
| 7684 | Mail::SpamAssassin::SpamCopURI |
---|
| 7685 | URI URI::Escape URI::Heuristic URI::QueryParam URI::Split URI::URL |
---|
| 7686 | URI::WithBase URI::_foreign URI::_generic URI::_ldap URI::_login |
---|
| 7687 | URI::_query URI::_segment URI::_server URI::_userpass URI::data URI::ftp |
---|
| 7688 | URI::gopher URI::http URI::https URI::ldap URI::ldapi URI::ldaps |
---|
| 7689 | URI::mailto URI::mms URI::news URI::nntp URI::pop URI::rlogin URI::rsync |
---|
| 7690 | URI::rtsp URI::rtspu URI::sip URI::sips URI::snews URI::ssh URI::telnet |
---|
| 7691 | URI::tn3270 URI::urn URI::urn::isbn URI::urn::oid |
---|
| 7692 | URI::file URI::file::Base URI::file::Unix URI::file::Win32 |
---|
| 7693 | )); |
---|
| 7694 | } |
---|
| 7695 | my($missing); |
---|
| 7696 | $missing = Amavis::Boot::fetch_modules('PRE-COMPILE OPTIONAL MODULES', 0, |
---|
| 7697 | @modules) if @modules; |
---|
| 7698 | do_log(2, 'INFO: no optional modules: '.join(' ',@$missing)) |
---|
| 7699 | if ref $missing && @$missing; |
---|
| 7700 | # load optional modules SAVI and Mail::ClamAV if available and requested |
---|
| 7701 | if ($extra_code_antivirus) { |
---|
| 7702 | my($savi_obj, $savi_module_ok, $clamav_module_ok); |
---|
| 7703 | for my $entry (@{ca('av_scanners')}, @{ca('av_scanners_backup')}) { |
---|
| 7704 | if (ref($entry) ne 'ARRAY') { # none |
---|
| 7705 | } elsif ($entry->[1] eq \&ask_sophos_savi || |
---|
| 7706 | $entry->[1] eq \&sophos_savi || |
---|
| 7707 | $entry->[0] eq 'Sophos SAVI') { |
---|
| 7708 | if (!defined($savi_module_ok)) { |
---|
| 7709 | $savi_module_ok = eval { require SAVI }; |
---|
| 7710 | $savi_module_ok = 0 if !defined $savi_module_ok; |
---|
| 7711 | # |
---|
| 7712 | # if the Amavis::AV::sophos_savi_init() in the following line _is_ |
---|
| 7713 | # called from here, SAVI-Perl will only initialize once during startup; |
---|
| 7714 | # otherwise (if left uninitialized here) each child process will do the |
---|
| 7715 | # SAVI-Perl initialization at its birth, which costs some time, but avoids |
---|
| 7716 | # the need to restart amavisd when IDE database changes: |
---|
| 7717 | ### $savi_obj = Amavis::AV::sophos_savi_init(@$entry) if $savi_module_ok; |
---|
| 7718 | |
---|
| 7719 | } |
---|
| 7720 | if (!$savi_module_ok) { $entry->[1] = undef } # disable entry |
---|
| 7721 | else { $entry->[2] = $savi_obj if defined $savi_obj } # save as args |
---|
| 7722 | } elsif ($entry->[1] eq \&ask_clamav || |
---|
| 7723 | $entry->[0] =~ /^Mail::ClamAV/) { |
---|
| 7724 | if (!defined($clamav_module_ok)) { |
---|
| 7725 | $clamav_module_ok = eval { require Mail::ClamAV }; |
---|
| 7726 | $clamav_module_ok = 0 if !defined $clamav_module_ok; |
---|
| 7727 | } |
---|
| 7728 | $entry->[1] = undef if !$clamav_module_ok; # disable entry |
---|
| 7729 | } |
---|
| 7730 | } |
---|
| 7731 | } |
---|
| 7732 | } |
---|
| 7733 | |
---|
| 7734 | # |
---|
| 7735 | # Main program starts here |
---|
| 7736 | # |
---|
| 7737 | |
---|
| 7738 | # Read dynamic source code, and logging and notification message templates |
---|
| 7739 | # from the end of this file (pseudo file handle DATA) |
---|
| 7740 | # |
---|
| 7741 | $Amavis::Conf::notify_spam_admin_templ = ''; # not used |
---|
| 7742 | $Amavis::Conf::notify_spam_recips_templ = ''; # not used |
---|
| 7743 | do { local($/) = "__DATA__\n"; # set line terminator to this string |
---|
| 7744 | chomp($_ = <Amavis::DATA>) for ( |
---|
| 7745 | $extra_code_db, $extra_code_cache, |
---|
| 7746 | $extra_code_sql, $extra_code_ldap, |
---|
| 7747 | $extra_code_in_amcl, $extra_code_in_smtp, $extra_code_in_qmqpqq, |
---|
| 7748 | $extra_code_antivirus, $extra_code_antispam, $extra_code_unpackers, |
---|
| 7749 | $Amavis::Conf::log_templ, $Amavis::Conf::log_recip_templ); |
---|
| 7750 | if ($unicode_aware) { |
---|
| 7751 | # binmode(\*Amavis::DATA, ":encoding(utf8)") # :encoding(iso-8859-1) |
---|
| 7752 | # or die "Can't set \*DATA encoding: $!"; |
---|
| 7753 | } |
---|
| 7754 | chomp($_ = <Amavis::DATA>) for ( |
---|
| 7755 | $Amavis::Conf::notify_sender_templ, |
---|
| 7756 | $Amavis::Conf::notify_virus_sender_templ, |
---|
| 7757 | $Amavis::Conf::notify_virus_admin_templ, |
---|
| 7758 | $Amavis::Conf::notify_virus_recips_templ, |
---|
| 7759 | $Amavis::Conf::notify_spam_sender_templ, |
---|
| 7760 | $Amavis::Conf::notify_spam_admin_templ ); |
---|
| 7761 | }; # restore line terminator |
---|
| 7762 | close(\*Amavis::DATA) or die "Can't close *Amavis::DATA: $!"; |
---|
| 7763 | # close(STDIN) or die "Can't close STDIN: $!"; |
---|
| 7764 | # note: don't close STDIN just yet to prevent some other file taking up fd 0 |
---|
| 7765 | |
---|
| 7766 | # discard trailing NL |
---|
| 7767 | $Amavis::Conf::log_templ = $1 |
---|
| 7768 | if $Amavis::Conf::log_templ=~/^(.*?)[\r\n]+\z/s; |
---|
| 7769 | $Amavis::Conf::log_recip_templ = $1 |
---|
| 7770 | if $Amavis::Conf::log_recip_templ=~/^(.*?)[\r\n]+\z/s; |
---|
| 7771 | |
---|
| 7772 | # Consider droping privileges early, before reading config file. |
---|
| 7773 | # This is only possible if running under chroot will not be needed. |
---|
| 7774 | # |
---|
| 7775 | my($desired_group); # defaults to $desired_user's group |
---|
| 7776 | my($desired_user); # username or UID |
---|
| 7777 | if ($> != 0) { $desired_user = $> } # use effective UID if not root |
---|
| 7778 | #else { |
---|
| 7779 | # for my $u ('amavis', 'vscan') { # try to guess a good default username |
---|
| 7780 | # my($username,$passwd,$uid,$gid) = getpwnam($u); |
---|
| 7781 | # if (defined $uid && $uid != 0) { $desired_user = $u; last } |
---|
| 7782 | # } |
---|
| 7783 | #} |
---|
| 7784 | |
---|
| 7785 | # collect and parse command line options |
---|
| 7786 | while (@ARGV >= 2 && $ARGV[0] =~ /^-[ugc]\z/) { |
---|
| 7787 | my($opt) = shift @ARGV; |
---|
| 7788 | if ($opt eq '-u') { # -u username |
---|
| 7789 | my($val) = shift @ARGV; |
---|
| 7790 | if ($> == 0) { $desired_user = $val } |
---|
| 7791 | else { print STDERR "Ignoring option -u when not running as root\n" } |
---|
| 7792 | } elsif ($opt eq '-g') { # -g group |
---|
| 7793 | my($val) = shift @ARGV; |
---|
| 7794 | if ($> == 0) { $desired_group = $val } |
---|
| 7795 | else { print STDERR "Ignoring option -g when not running as root\n" } |
---|
| 7796 | } elsif ($opt eq '-c') { # -c config_file |
---|
| 7797 | push(@config_files, untaint(shift @ARGV)); |
---|
| 7798 | } |
---|
| 7799 | } |
---|
| 7800 | |
---|
| 7801 | if (defined $desired_user && ($> == 0 || $< == 0)) { # drop privileges early |
---|
| 7802 | my($username,$passwd,$uid,$gid) = |
---|
| 7803 | $desired_user=~/^(\d+)$/ ? (undef,undef,$1,undef) :getpwnam($desired_user); |
---|
| 7804 | defined $uid or die "No such username: $desired_user\n"; |
---|
| 7805 | if ($desired_group eq '') { $desired_group = $gid } # for logging purposes |
---|
| 7806 | else { $gid = $desired_group=~/^(\d+)$/ ? $1 : getgrnam($desired_group) } |
---|
| 7807 | defined $gid or die "No such group: $desired_group\n"; |
---|
| 7808 | $( = $gid; # real GID |
---|
| 7809 | $) = "$gid $gid"; # effective GID |
---|
| 7810 | POSIX::setuid($uid) or die "Can't setuid to $uid: $!"; |
---|
| 7811 | $> = $uid; $< = $uid; # just in case |
---|
| 7812 | # print STDERR "desired user=$desired_user ($uid), current: EUID: $> ($<)\n"; |
---|
| 7813 | # print STDERR "desired group=$desired_group, current: EGID: $) ($()\n"; |
---|
| 7814 | $> != 0 or die "Still running as root, aborting\n"; |
---|
| 7815 | $< != 0 or die "Effective UID changed, but Real UID is 0\n"; |
---|
| 7816 | } |
---|
| 7817 | |
---|
| 7818 | umask(0027); |
---|
| 7819 | |
---|
| 7820 | # default location of the config file if none specified |
---|
| 7821 | push(@config_files, '/etc/amavisd.conf') if !@config_files; |
---|
| 7822 | |
---|
| 7823 | # Read config file, which may override default settings |
---|
| 7824 | Amavis::Conf::build_default_maps(); |
---|
| 7825 | Amavis::Conf::read_config(@config_files); |
---|
| 7826 | # chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
| 7827 | |
---|
| 7828 | if (defined $desired_user && $daemon_user ne '') { |
---|
| 7829 | # compare the config file settings to current UID |
---|
| 7830 | my($username,$passwd,$uid,$gid) = |
---|
| 7831 | $daemon_user=~/^(\d+)$/ ? (undef,undef,$1,undef) : getpwnam($daemon_user); |
---|
| 7832 | $uid == $> or warn sprintf( |
---|
| 7833 | "WARN: running under user '%s' (UID=%s), the config file". |
---|
| 7834 | " specifies \$daemon_user='%s' (UID=%s)\n", |
---|
| 7835 | $desired_user, $>, $daemon_user, defined $uid ? $uid : '?'); |
---|
| 7836 | } |
---|
| 7837 | |
---|
| 7838 | # compile optional modules if needed |
---|
| 7839 | # %modules_basic = %INC; # helps to track missing modules in chroot |
---|
| 7840 | if (!$enable_db) { $extra_code_db = undef } |
---|
| 7841 | else { |
---|
| 7842 | eval $extra_code_db or die "Problem in Amavis::DB or Amavis::DB::SNMP code: $@"; |
---|
| 7843 | $extra_code_db = 1; # release memory occupied by the source code |
---|
| 7844 | } |
---|
| 7845 | if (!$enable_global_cache || !$extra_code_db) { $extra_code_cache = undef } |
---|
| 7846 | else { |
---|
| 7847 | eval $extra_code_cache or die "Problem in the Amavis::Cache code: $@"; |
---|
| 7848 | $extra_code_cache = 1; # release memory occupied by the source code |
---|
| 7849 | } |
---|
| 7850 | if (!@lookup_sql_dsn) { $extra_code_sql = undef } |
---|
| 7851 | else { |
---|
| 7852 | eval $extra_code_sql or die "Problem in the Lookup::SQL code: $@"; |
---|
| 7853 | $extra_code_sql = 1; # release memory occupied by the source code |
---|
| 7854 | } |
---|
| 7855 | if (!$enable_ldap) { $extra_code_ldap = undef } |
---|
| 7856 | else { |
---|
| 7857 | eval $extra_code_ldap or die "Problem in the Lookup::LDAP code: $@"; |
---|
| 7858 | $extra_code_ldap = 1; # release memory occupied by the source code |
---|
| 7859 | } |
---|
| 7860 | |
---|
| 7861 | if (c('protocol') eq 'COURIER') { |
---|
| 7862 | die "In::Courier code not available"; |
---|
| 7863 | } elsif (c('protocol') eq 'AM.PDP' || $unix_socketname ne '') { |
---|
| 7864 | eval $extra_code_in_amcl or die "Problem in the In::AMCL code: $@"; |
---|
| 7865 | $extra_code_in_amcl = 1; # release memory occupied by the source code |
---|
| 7866 | } else { |
---|
| 7867 | $extra_code_in_amcl = undef; |
---|
| 7868 | } |
---|
| 7869 | |
---|
| 7870 | if (c('protocol') eq 'QMQPqq') { # simpleminded, not checking all policy banks |
---|
| 7871 | eval $extra_code_in_qmqpqq or die "Problem in the In::QMQPqq code: $@"; |
---|
| 7872 | $extra_code_in_qmqpqq = 1; # release memory occupied by the source code |
---|
| 7873 | $extra_code_in_smtp = undef; |
---|
| 7874 | } elsif (c('protocol') =~ /^(SMTP|LMTP)\z/ || |
---|
| 7875 | $inet_socket_port ne '' && |
---|
| 7876 | (!ref $inet_socket_port || @$inet_socket_port)) { # assume SMTP/LMTP |
---|
| 7877 | eval $extra_code_in_smtp or die "Problem in the In::SMTP code: $@"; |
---|
| 7878 | $extra_code_in_smtp = 1; # release memory occupied by the source code |
---|
| 7879 | $extra_code_in_qmqpqq = undef; |
---|
| 7880 | } else { |
---|
| 7881 | $extra_code_in_smtp = undef; |
---|
| 7882 | $extra_code_in_qmqpqq = undef; |
---|
| 7883 | } |
---|
| 7884 | |
---|
| 7885 | my($bpvcm) = ca('bypass_virus_checks_maps'); |
---|
| 7886 | if (!@{ca('av_scanners')} && !@{ca('av_scanners_backup')}) { |
---|
| 7887 | $extra_code_antivirus = undef; |
---|
| 7888 | } elsif (@$bpvcm && !ref($bpvcm->[0]) && $bpvcm->[0]) { |
---|
| 7889 | # do a simple-minded test to make it easy to turn off virus checks |
---|
| 7890 | $extra_code_antivirus = undef; |
---|
| 7891 | } else { |
---|
| 7892 | eval $extra_code_antivirus or die "Problem in the antivirus code: $@"; |
---|
| 7893 | $extra_code_antivirus = 1; # release memory occupied by the source code |
---|
| 7894 | } |
---|
| 7895 | if (!$extra_code_antivirus) # release storage |
---|
| 7896 | { @Amavis::Conf::av_scanners = @Amavis::Conf::av_scanners_backup = () } |
---|
| 7897 | |
---|
| 7898 | my($bpscm) = ca('bypass_spam_checks_maps'); |
---|
| 7899 | if (@$bpscm && !ref($bpscm->[0]) && $bpscm->[0]) { |
---|
| 7900 | # do a simple-minded test to make it easy to turn off spam checks |
---|
| 7901 | $extra_code_antispam = undef; |
---|
| 7902 | } else { |
---|
| 7903 | eval $extra_code_antispam or die "Problem in the antispam code: $@"; |
---|
| 7904 | $extra_code_antispam = 1; # release memory occupied by the source code |
---|
| 7905 | } |
---|
| 7906 | |
---|
| 7907 | if (c('bypass_decode_parts') && |
---|
| 7908 | !grep {exists $policy_bank{$_}{'bypass_decode_parts'} && |
---|
| 7909 | !$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) { |
---|
| 7910 | $extra_code_unpackers = undef; |
---|
| 7911 | } else { |
---|
| 7912 | eval $extra_code_unpackers or die "Problem in the Amavis::Unpackers code: $@"; |
---|
| 7913 | $extra_code_unpackers = 1; # release memory occupied by the source code |
---|
| 7914 | } |
---|
| 7915 | |
---|
| 7916 | # act on command line parameters |
---|
| 7917 | my($cmd) = lc($ARGV[0]); |
---|
| 7918 | if ($cmd =~ /^(start|debug|debug-sa|foreground)?\z/) { |
---|
| 7919 | $DEBUG=1 if $cmd eq 'debug'; |
---|
| 7920 | $daemonize=0 if $cmd eq 'foreground'; |
---|
| 7921 | $daemonize=0, $sa_debug=1 if $cmd eq 'debug-sa'; |
---|
| 7922 | } elsif ($cmd !~ /^(reload|stop)\z/) { |
---|
| 7923 | die "$myversion: Unknown argument. Usage:\n $0 [-u user] [-g group] [-c config-file] ( [start] | stop | reload | debug | debug-sa | foreground )\n"; |
---|
| 7924 | } else { # stop or reload |
---|
| 7925 | eval { # first stop a running daemon |
---|
| 7926 | $pid_file ne '' or die "Config parameter \$pid_file not defined"; |
---|
| 7927 | my($errn) = stat($pid_file) ? 0 : 0+$!; |
---|
| 7928 | $errn != ENOENT or die "No PID file $pid_file\n"; |
---|
| 7929 | $errn == 0 or die "PID file $pid_file inaccessible: $!"; |
---|
| 7930 | my($amavisd_pid); |
---|
| 7931 | open(PID_FILE, "< $pid_file\0") or die "Can't read file $pid_file: $!"; |
---|
| 7932 | while (<PID_FILE>) { chomp; $amavisd_pid = $_ if /^\d+\z/ } |
---|
| 7933 | close(PID_FILE) or die "Can't close file $pid_file: $!"; |
---|
| 7934 | defined($amavisd_pid) or die "Invalid PID in the $pid_file"; |
---|
| 7935 | $amavisd_pid = untaint($amavisd_pid); |
---|
| 7936 | kill('TERM',$amavisd_pid) or die "Can't SIGTERM amavisd[$amavisd_pid]: $!"; |
---|
| 7937 | my($delay) = 1; # seconds |
---|
| 7938 | for (;;) { |
---|
| 7939 | sleep($delay); $delay = 5; |
---|
| 7940 | last if !kill(0,$amavisd_pid); # is the old daemon still there? |
---|
| 7941 | print STDERR "Waiting for the process $amavisd_pid to terminate\n"; |
---|
| 7942 | } |
---|
| 7943 | }; |
---|
| 7944 | if ($@ ne '') { chomp($@); die "$@, can't $cmd the process\n" } |
---|
| 7945 | exit 0 if $cmd eq 'stop'; |
---|
| 7946 | print STDERR "daemon terminated, waiting for the dust to settle...\n"; |
---|
| 7947 | sleep 5; # wait for the TCP socket to be released |
---|
| 7948 | print STDERR "becoming a new daemon...\n"; |
---|
| 7949 | } |
---|
| 7950 | $daemonize = 0 if $DEBUG; |
---|
| 7951 | |
---|
| 7952 | # Set path, home and term explictly. Don't trust environment |
---|
| 7953 | $ENV{PATH} = $path if $path ne ''; |
---|
| 7954 | $ENV{HOME} = $helpers_home if $helpers_home ne ''; |
---|
| 7955 | $ENV{TERM} = 'dumb'; $ENV{COLUMNS} = '80'; $ENV{LINES} = '100'; |
---|
| 7956 | |
---|
| 7957 | Amavis::Log::init("amavis", !$daemonize, $DO_SYSLOG, $SYSLOG_LEVEL, $LOGFILE); |
---|
| 7958 | |
---|
| 7959 | # report version of Perl and process UID |
---|
| 7960 | do_log(1, "user=$desired_user, EUID: $> ($<); group=$desired_group, EGID: $) ($()"); |
---|
| 7961 | do_log(0, "Perl version $]"); |
---|
| 7962 | |
---|
| 7963 | # $SIG{USR2} = sub { |
---|
| 7964 | # my($msg) = Carp::longmess("SIG$_[0] received, backtrace:"); |
---|
| 7965 | # print STDERR "\n",$msg,"\n"; do_log(-1,$msg); |
---|
| 7966 | # }; |
---|
| 7967 | |
---|
| 7968 | fetch_modules_extra(); # bring additional modules into memory and compile them |
---|
| 7969 | |
---|
| 7970 | # set up Net::Server configuration |
---|
| 7971 | my $server = bless { |
---|
| 7972 | server => { |
---|
| 7973 | # command args to be used after HUP must be untainted, deflt: [$0,@ARGV] |
---|
| 7974 | # commandline => ['/usr/local/sbin/amavisd','-c',$config_file[0] ], |
---|
| 7975 | commandline => [], # disable |
---|
| 7976 | |
---|
| 7977 | # listen on the following sockets (one or more): |
---|
| 7978 | port => [ ($unix_socketname eq '' ? () : "$unix_socketname|unix"), # helper |
---|
| 7979 | map { "$_/tcp" } # accept SMTP on this port(s) |
---|
| 7980 | (ref $inet_socket_port ? @$inet_socket_port |
---|
| 7981 | : $inet_socket_port ne '' ? $inet_socket_port : () ), |
---|
| 7982 | ], |
---|
| 7983 | # limit socket bind (e.g. to the loopback interface) |
---|
| 7984 | host => ($inet_socket_bind eq '' ? '*' : $inet_socket_bind), |
---|
| 7985 | |
---|
| 7986 | max_servers => $max_servers, # number of pre-forked children |
---|
| 7987 | max_requests => $max_requests, # restart child after that many accept's |
---|
| 7988 | user => (($> == 0 || $< == 0) ? $daemon_user : undef), |
---|
| 7989 | group => (($> == 0 || $< == 0) ? $daemon_group : undef), |
---|
| 7990 | pid_file => $pid_file, |
---|
| 7991 | lock_file => $lock_file, # serialization lockfile |
---|
| 7992 | # serialize => 'flock', # flock, semaphore, pipe |
---|
| 7993 | background => $daemonize ? 1 : undef, |
---|
| 7994 | setsid => $daemonize ? 1 : undef, |
---|
| 7995 | chroot => $daemon_chroot_dir ne '' ? $daemon_chroot_dir : undef, |
---|
| 7996 | no_close_by_child => 1, |
---|
| 7997 | |
---|
| 7998 | # controls log level for Net::Server internal log messages: |
---|
| 7999 | # 0=err, 1=warning, 2=notice, 3=info, 4=debug |
---|
| 8000 | log_level => ($DEBUG ? 4 : 2), |
---|
| 8001 | log_file => undef, # will be overridden to call do_log() |
---|
| 8002 | }, |
---|
| 8003 | }, 'Amavis'; |
---|
| 8004 | |
---|
| 8005 | $0 = 'amavisd (master)'; |
---|
| 8006 | $server->run; # transfer control to Net::Server |
---|
| 8007 | |
---|
| 8008 | # shouldn't get here |
---|
| 8009 | exit 1; |
---|
| 8010 | |
---|
| 8011 | # we read text (especially notification templates) from DATA sections |
---|
| 8012 | # to avoid any interpretations of special characters (e.g. \ or ') by Perl |
---|
| 8013 | # |
---|
| 8014 | |
---|
| 8015 | __DATA__ |
---|
| 8016 | # |
---|
| 8017 | package Amavis::DB::SNMP; |
---|
| 8018 | use strict; |
---|
| 8019 | use re 'taint'; |
---|
| 8020 | |
---|
| 8021 | BEGIN { |
---|
| 8022 | import Amavis::Conf qw($myversion $myhostname); |
---|
| 8023 | import Amavis::Util qw(ll do_log snmp_counters_get); |
---|
| 8024 | } |
---|
| 8025 | |
---|
| 8026 | use BerkeleyDB; |
---|
| 8027 | |
---|
| 8028 | BEGIN { |
---|
| 8029 | use Exporter (); |
---|
| 8030 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 8031 | $VERSION = '2.034'; |
---|
| 8032 | @ISA = qw(Exporter); |
---|
| 8033 | } |
---|
| 8034 | |
---|
| 8035 | # open existing databases (called by each child process) |
---|
| 8036 | sub new { |
---|
| 8037 | my($class,$db_env) = @_; my($env) = $db_env->get_db_env; |
---|
| 8038 | defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!."; |
---|
| 8039 | my($dbs) = BerkeleyDB::Hash->new(-Filename=>'snmp.db', -Env=>$env); |
---|
| 8040 | defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!."; |
---|
| 8041 | my($dbn) = BerkeleyDB::Hash->new(-Filename=>'nanny.db', -Env=>$env); |
---|
| 8042 | defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!."; |
---|
| 8043 | bless { 'db_snmp'=>$dbs, 'db_nanny'=>$dbn }, $class; |
---|
| 8044 | } |
---|
| 8045 | |
---|
| 8046 | sub DESTROY { |
---|
| 8047 | my($self) = shift; |
---|
| 8048 | eval { do_log(5,"Amavis::DB::SNMP called") }; |
---|
| 8049 | for my $db ($self->{'db_snmp'}, $self->{'db_nanny'}) { |
---|
| 8050 | if (defined $db) { |
---|
| 8051 | eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." }; |
---|
| 8052 | if ($@ ne '') { warn "BDB S+N DESTROY $@" } |
---|
| 8053 | $db = undef; |
---|
| 8054 | } |
---|
| 8055 | } |
---|
| 8056 | } |
---|
| 8057 | |
---|
| 8058 | #sub lock_stat($) { |
---|
| 8059 | # my($label) = @_; |
---|
| 8060 | # my($s) = qx'/usr/local/bin/db_stat-4.2 -c -h /var/amavis/db | /usr/local/bin/perl -ne \'$a{$2}=$1 if /^(\d+)\s+Total number of locks (requested|released)/; END {printf("%d, %d\n",$a{requested}, $a{requested}-$a{released})}\''; |
---|
| 8061 | # do_log(0, "lock_stat $label: $s"); |
---|
| 8062 | #} |
---|
| 8063 | |
---|
| 8064 | # insert startup time SNMP entry, called from the master process at startup |
---|
| 8065 | # (a classical subroutine, not a method) |
---|
| 8066 | sub put_initial_snmp_data($) { |
---|
| 8067 | my($db) = @_; |
---|
| 8068 | my($cursor) = $db->db_cursor(DB_WRITECURSOR); |
---|
| 8069 | defined $cursor or die "BDB S db_cursor: $BerkeleyDB::Error, $!."; |
---|
| 8070 | for my $obj (['sysDescr', 'STR', $myversion], |
---|
| 8071 | ['sysObjectID', 'OID', '1.3.6.1.4.1.15312.2.1'], |
---|
| 8072 | # iso.org.dod.internet.private.enterprise.ijs.amavisd-new.snmp |
---|
| 8073 | ['sysUpTime', 'INT', int(time)], |
---|
| 8074 | # later it must be converted to timeticks (10ms since start) |
---|
| 8075 | ['sysContact', 'STR', ''], |
---|
| 8076 | ['sysName', 'STR', $myhostname], |
---|
| 8077 | ['sysLocation', 'STR', ''], |
---|
| 8078 | ['sysServices', 'INT', 64], # application |
---|
| 8079 | ) { |
---|
| 8080 | my($key,$type,$val) = @$obj; |
---|
| 8081 | $cursor->c_put($key, sprintf("%s %s",$type,$val), DB_KEYLAST) == 0 |
---|
| 8082 | or die "BDB S c_put: $BerkeleyDB::Error, $!."; |
---|
| 8083 | }; |
---|
| 8084 | $cursor->c_close==0 or die "BDB S c_close: $BerkeleyDB::Error, $!."; |
---|
| 8085 | } |
---|
| 8086 | |
---|
| 8087 | sub update_counters { |
---|
| 8088 | my($self) = @_; |
---|
| 8089 | my($counter_names_ref) = snmp_counters_get(); |
---|
| 8090 | my($eval_stat,$interrupt); $interrupt = ''; |
---|
| 8091 | if (defined $counter_names_ref && @$counter_names_ref) { |
---|
| 8092 | my($db) = $self->{'db_snmp'}; my($cursor); |
---|
| 8093 | my($h1) = sub { $interrupt = $_[0] }; |
---|
| 8094 | local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8; |
---|
| 8095 | eval { # ensure cursor will be unlocked even in case of errors or signals |
---|
| 8096 | $cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock |
---|
| 8097 | defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!."; |
---|
| 8098 | for my $key (@$counter_names_ref) { |
---|
| 8099 | my($counter_name,$counter_incr) = ref($key) ? @$key : ($key,1); |
---|
| 8100 | my($val,$flags); my($type) = 'C32'; |
---|
| 8101 | my($stat) = $cursor->c_get($counter_name,$val,DB_SET); |
---|
| 8102 | if ($stat==0) { # exists, update it |
---|
| 8103 | if ($val =~ /^\Q$type\E (\d+)\z/o) { $val = $1 } |
---|
| 8104 | else { do_log(-2,"WARN: counter syntax? $val, clearing"); $val = 0 } |
---|
| 8105 | $flags = DB_CURRENT; $val = $val+$counter_incr; |
---|
| 8106 | } else { # create new entry |
---|
| 8107 | $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!."; |
---|
| 8108 | $flags = DB_KEYLAST; $val = $counter_incr; |
---|
| 8109 | } |
---|
| 8110 | $cursor->c_put($counter_name, sprintf("%s %010d",$type,$val),$flags)==0 |
---|
| 8111 | or die "c_put: $BerkeleyDB::Error, $!."; |
---|
| 8112 | } |
---|
| 8113 | $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!."; |
---|
| 8114 | $cursor = undef; |
---|
| 8115 | }; |
---|
| 8116 | $eval_stat = $@; |
---|
| 8117 | if (defined $db) { |
---|
| 8118 | $cursor->c_close if defined $cursor; # unlock, ignoring status |
---|
| 8119 | $cursor = undef; |
---|
| 8120 | if ($eval_stat eq '') { |
---|
| 8121 | # my($stat); $db->db_sync(); # not really needed |
---|
| 8122 | # $stat==0 or warn "BDB S db_sync, status $stat: $BerkeleyDB::Error, $!."; |
---|
| 8123 | } |
---|
| 8124 | } |
---|
| 8125 | } |
---|
| 8126 | delete $self->{'cnt'}; |
---|
| 8127 | if ($interrupt ne '') { kill($interrupt,$$) } # resignal |
---|
| 8128 | elsif ($eval_stat ne '') |
---|
| 8129 | { chomp($eval_stat); die "update_counters: BDB S $eval_stat\n" } |
---|
| 8130 | } |
---|
| 8131 | |
---|
| 8132 | sub read_counters { |
---|
| 8133 | my($self,@counter_names) = @_; |
---|
| 8134 | my($eval_stat,$interrupt); $interrupt = ''; |
---|
| 8135 | my($db) = $self->{'db_snmp'}; my($cursor); my(@values); |
---|
| 8136 | my($h1) = sub { $interrupt = $_[0] }; |
---|
| 8137 | local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8; |
---|
| 8138 | eval { # ensure cursor will be unlocked even in case of errors or signals |
---|
| 8139 | $cursor = $db->db_cursor; # obtain read lock |
---|
| 8140 | defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!."; |
---|
| 8141 | for my $cname (@counter_names) { |
---|
| 8142 | my($val); my($stat) = $cursor->c_get($cname,$val,DB_SET); |
---|
| 8143 | push(@values, $stat==0 ? $val : undef); |
---|
| 8144 | $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!."; |
---|
| 8145 | } |
---|
| 8146 | $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!."; |
---|
| 8147 | $cursor = undef; |
---|
| 8148 | }; |
---|
| 8149 | $eval_stat = $@; |
---|
| 8150 | if (defined $db) { |
---|
| 8151 | $cursor->c_close if defined $cursor; # unlock, ignoring status |
---|
| 8152 | $cursor = undef; |
---|
| 8153 | } |
---|
| 8154 | if ($interrupt ne '') { kill($interrupt,$$) } # resignal |
---|
| 8155 | elsif ($eval_stat ne '') |
---|
| 8156 | { chomp($eval_stat); die "read_counters: BDB S $eval_stat\n" } |
---|
| 8157 | my($type) = 'C32'; |
---|
| 8158 | for my $val (@values) { |
---|
| 8159 | if (!defined($val)) {} # keep undefined |
---|
| 8160 | elsif ($val =~ /^\Q$type\E (\d+)\z/o) { $val = 0+$1 } |
---|
| 8161 | else { do_log(-2,"WARN: counter syntax? $val"); $val = undef } |
---|
| 8162 | } |
---|
| 8163 | \@values; |
---|
| 8164 | } |
---|
| 8165 | |
---|
| 8166 | sub register_proc { |
---|
| 8167 | my($self,$task_id) = @_; |
---|
| 8168 | my($db) = $self->{'db_nanny'}; my($cursor); |
---|
| 8169 | my($val,$new_val); my($key) = sprintf("%05d",$$); |
---|
| 8170 | $new_val = sprintf("%010d %-12s", time, $task_id) if defined $task_id; |
---|
| 8171 | my($eval_stat,$interrupt); $interrupt = ''; |
---|
| 8172 | my($h1) = sub { $interrupt = $_[0] }; |
---|
| 8173 | local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8; |
---|
| 8174 | eval { # ensure cursor will be unlocked even in case of errors or signals |
---|
| 8175 | $cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock |
---|
| 8176 | defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!."; |
---|
| 8177 | my($stat) = $cursor->c_get($key,$val,DB_SET); |
---|
| 8178 | $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!."; |
---|
| 8179 | if ($stat==0 && !defined $task_id) { # remove existing entry |
---|
| 8180 | $cursor->c_del==0 or die "c_del: $BerkeleyDB::Error, $!."; |
---|
| 8181 | } elsif (defined $task_id && !($stat==0 && $new_val eq $val)) { |
---|
| 8182 | # add new, or update existing entry if different |
---|
| 8183 | $cursor->c_put($key, $new_val, |
---|
| 8184 | $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0 |
---|
| 8185 | or die "c_put: $BerkeleyDB::Error, $!."; |
---|
| 8186 | } |
---|
| 8187 | $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!."; |
---|
| 8188 | $cursor = undef; |
---|
| 8189 | }; |
---|
| 8190 | $eval_stat = $@; |
---|
| 8191 | if (defined $db) { |
---|
| 8192 | $cursor->c_close if defined $cursor; # unlock, ignoring status |
---|
| 8193 | $cursor = undef; |
---|
| 8194 | if ($eval_stat eq '') { |
---|
| 8195 | # my($stat) = $db->db_sync(); # not really needed |
---|
| 8196 | # $stat==0 or warn "BDB N db_sync, status $stat: $BerkeleyDB::Error, $!."; |
---|
| 8197 | } |
---|
| 8198 | } |
---|
| 8199 | if ($interrupt ne '') { kill($interrupt,$$) } # resignal |
---|
| 8200 | elsif ($eval_stat ne '') |
---|
| 8201 | { chomp($eval_stat); die "register_proc: BDB N $eval_stat\n" } |
---|
| 8202 | } |
---|
| 8203 | |
---|
| 8204 | 1; |
---|
| 8205 | |
---|
| 8206 | # |
---|
| 8207 | package Amavis::DB; |
---|
| 8208 | use strict; |
---|
| 8209 | use re 'taint'; |
---|
| 8210 | |
---|
| 8211 | BEGIN { |
---|
| 8212 | import Amavis::Conf qw($db_home $daemon_chroot_dir); |
---|
| 8213 | import Amavis::Util qw(untaint ll do_log); |
---|
| 8214 | } |
---|
| 8215 | |
---|
| 8216 | use BerkeleyDB; |
---|
| 8217 | |
---|
| 8218 | BEGIN { |
---|
| 8219 | use Exporter (); |
---|
| 8220 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 8221 | $VERSION = '2.034'; |
---|
| 8222 | @ISA = qw(Exporter); |
---|
| 8223 | } |
---|
| 8224 | |
---|
| 8225 | # create new databases, then close them (called by the parent process) |
---|
| 8226 | # (called only if $db_home is nonempty) |
---|
| 8227 | sub init($) { |
---|
| 8228 | my($predelete) = @_; # delete existing db files first? |
---|
| 8229 | my($name) = $db_home; |
---|
| 8230 | $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne ''; |
---|
| 8231 | if ($predelete) { # delete old database files |
---|
| 8232 | local(*DIR); my($f); |
---|
| 8233 | opendir(DIR,$db_home) or die "Can't open directory $name: $!"; |
---|
| 8234 | while (defined($f = readdir(DIR))) { |
---|
| 8235 | next if ($f eq '.' || $f eq '..') && -d _; |
---|
| 8236 | if ($f =~ /^(__db\.\d+|(cache-expiry|cache|snmp|nanny)\.db)\z/s) { |
---|
| 8237 | $f = untaint($f); |
---|
| 8238 | unlink("$db_home/$f") or die "Can't delete file $name/$f: $!"; |
---|
| 8239 | } |
---|
| 8240 | } |
---|
| 8241 | closedir(DIR) or die "Can't close directory $name: $!"; |
---|
| 8242 | } |
---|
| 8243 | my($env) = BerkeleyDB::Env->new(-Home=>$db_home, -Mode=>0640, |
---|
| 8244 | -Flags=> DB_CREATE | DB_INIT_CDB | DB_INIT_MPOOL); |
---|
| 8245 | defined $env or die "BDB bad db env. at $db_home: $BerkeleyDB::Error, $!."; |
---|
| 8246 | do_log(0, sprintf("Creating db in %s/; BerkeleyDB %s, libdb %s", |
---|
| 8247 | $name, BerkeleyDB->VERSION, $BerkeleyDB::db_version)); |
---|
| 8248 | my($dbc) = BerkeleyDB::Hash->new( |
---|
| 8249 | -Filename=>'cache.db', -Flags=>DB_CREATE, -Env=>$env ); |
---|
| 8250 | defined $dbc or die "BDB no dbC: $BerkeleyDB::Error, $!."; |
---|
| 8251 | my($dbq) = BerkeleyDB::Queue->new( |
---|
| 8252 | -Filename=>'cache-expiry.db', -Flags=>DB_CREATE, -Env=>$env, |
---|
| 8253 | -Len=>15+1+32 ); # '-ExtentSize' needs DB 3.2.x, e.g. -ExtentSize=>2 |
---|
| 8254 | defined $dbq or die "BDB no dbQ: $BerkeleyDB::Error, $!."; |
---|
| 8255 | my($dbs) = BerkeleyDB::Hash->new( |
---|
| 8256 | -Filename=>'snmp.db', -Flags=>DB_CREATE, -Env=>$env ); |
---|
| 8257 | defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!."; |
---|
| 8258 | my($dbn) = BerkeleyDB::Hash->new( |
---|
| 8259 | -Filename=>'nanny.db', -Flags=>DB_CREATE, -Env=>$env ); |
---|
| 8260 | defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!."; |
---|
| 8261 | |
---|
| 8262 | Amavis::DB::SNMP::put_initial_snmp_data($dbs); |
---|
| 8263 | for my $db ($dbc, $dbq, $dbs, $dbn) |
---|
| 8264 | { $db->db_close==0 or die "BDB db_close: $BerkeleyDB::Error, $!." } |
---|
| 8265 | } |
---|
| 8266 | |
---|
| 8267 | # open an existing databases environment (called by each child process) |
---|
| 8268 | sub new { |
---|
| 8269 | my($class) = @_; my($env); |
---|
| 8270 | if (defined $db_home) { |
---|
| 8271 | $env = BerkeleyDB::Env->new( |
---|
| 8272 | -Home=>$db_home, -Mode=>0640, -Flags=> DB_INIT_CDB | DB_INIT_MPOOL); |
---|
| 8273 | defined $env or die "BDB bad db env. at $db_home: $BerkeleyDB::Error, $!."; |
---|
| 8274 | } |
---|
| 8275 | bless \$env, $class; |
---|
| 8276 | } |
---|
| 8277 | sub get_db_env { my($self) = shift; $$self } |
---|
| 8278 | |
---|
| 8279 | 1; |
---|
| 8280 | |
---|
| 8281 | __DATA__ |
---|
| 8282 | # |
---|
| 8283 | package Amavis::Cache; |
---|
| 8284 | # offer an 'IPC::Cache'-compatible interface to a BerkeleyDB-based cache. |
---|
| 8285 | # Replaces methods new,get,set of the memory-based cache. |
---|
| 8286 | use strict; |
---|
| 8287 | use re 'taint'; |
---|
| 8288 | |
---|
| 8289 | BEGIN { |
---|
| 8290 | import Amavis::Util qw(ll do_log); |
---|
| 8291 | } |
---|
| 8292 | |
---|
| 8293 | use BerkeleyDB; |
---|
| 8294 | |
---|
| 8295 | BEGIN { |
---|
| 8296 | use Exporter (); |
---|
| 8297 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 8298 | $VERSION = '2.0342'; |
---|
| 8299 | @ISA = qw(Exporter); |
---|
| 8300 | } |
---|
| 8301 | |
---|
| 8302 | # open existing databases (called by each child process); |
---|
| 8303 | # if $db_env is undef a memory-based cache is created, otherwise use BerkeleyDB |
---|
| 8304 | sub new { |
---|
| 8305 | my($class,$db_env) = @_; |
---|
| 8306 | my($dbc,$dbq,$mem_cache); |
---|
| 8307 | if (!defined($db_env)) { |
---|
| 8308 | do_log(1,"BerkeleyDB not available, using memory-based local cache"); |
---|
| 8309 | $mem_cache = {}; |
---|
| 8310 | } else { |
---|
| 8311 | my($env) = $db_env->get_db_env; |
---|
| 8312 | defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!."; |
---|
| 8313 | $dbc = BerkeleyDB::Hash->new(-Filename=>'cache.db', -Env=>$env); |
---|
| 8314 | defined $dbc or die "BDB no dbC: $BerkeleyDB::Error, $!."; |
---|
| 8315 | $dbq = BerkeleyDB::Queue->new(-Filename=>'cache-expiry.db', -Env=>$env, |
---|
| 8316 | -Len=>15+1+32); # '-ExtentSize' needs DB 3.2.x, e.g. -ExtentSize=>2 |
---|
| 8317 | defined $dbq or die "BDB no dbQ: $BerkeleyDB::Error, $!."; |
---|
| 8318 | } |
---|
| 8319 | bless {'db_cache'=>$dbc, 'db_queue'=>$dbq, 'mem_cache'=>$mem_cache}, $class; |
---|
| 8320 | } |
---|
| 8321 | |
---|
| 8322 | sub DESTROY { |
---|
| 8323 | my($self) = shift; |
---|
| 8324 | eval { do_log(5,"Amavis::Cache called") }; |
---|
| 8325 | for my $db ($self->{'db_cache'}, $self->{'db_queue'}) { |
---|
| 8326 | if (defined $db) { |
---|
| 8327 | eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." }; |
---|
| 8328 | if ($@ ne '') { warn "BDB C+Q DESTROY $@" } |
---|
| 8329 | $db = undef; |
---|
| 8330 | } |
---|
| 8331 | } |
---|
| 8332 | } |
---|
| 8333 | |
---|
| 8334 | # purge expired entries from the queue head and enqueue new entry at the tail |
---|
| 8335 | sub enqueue { |
---|
| 8336 | my($self,$str,$now_utc_iso8601,$expires_utc_iso8601) = @_; |
---|
| 8337 | my($db) = $self->{'db_cache'}; my($dbq) = $self->{'db_queue'}; |
---|
| 8338 | local($1,$2); my($stat,$key,$val); $key = ''; |
---|
| 8339 | my($qcursor) = $dbq->db_cursor(DB_WRITECURSOR); |
---|
| 8340 | defined $qcursor or die "BDB Q db_cursor: $BerkeleyDB::Error, $!."; |
---|
| 8341 | while ( ($stat=$qcursor->c_get($key,$val,DB_NEXT)) == 0 ) { |
---|
| 8342 | if ($val !~ /^([^ ]+) (.*)\z/s) { |
---|
| 8343 | do_log(-2,"WARN: queue head invalid, deleting: $val"); |
---|
| 8344 | } else { |
---|
| 8345 | my($t,$digest) = ($1,$2); |
---|
| 8346 | last if $t ge $now_utc_iso8601; |
---|
| 8347 | my($cursor) = $db->db_cursor(DB_WRITECURSOR); |
---|
| 8348 | defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!."; |
---|
| 8349 | my($v); my($st1) = $cursor->c_get($digest,$v,DB_SET); |
---|
| 8350 | $st1==0 || $st1==DB_NOTFOUND or die "BDB C c_get: $BerkeleyDB::Error, $!."; |
---|
| 8351 | if ($st1==0 && $v=~/^([^ ]+) /s) { # record exists and appears valid |
---|
| 8352 | if ($1 ne $t) { |
---|
| 8353 | do_log(5,"enqueue: not deleting: $digest, was refreshed since"); |
---|
| 8354 | } else { # its expiration time correspond to timestamp in the queue |
---|
| 8355 | do_log(5,"enqueue: deleting: $digest"); |
---|
| 8356 | my($st2) = $cursor->c_del; # delete expired entry from the cache |
---|
| 8357 | $st2==0 || $st2==DB_KEYEMPTY |
---|
| 8358 | or die "BDB C c_del: $BerkeleyDB::Error, $!."; |
---|
| 8359 | } |
---|
| 8360 | } |
---|
| 8361 | $cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!."; |
---|
| 8362 | } |
---|
| 8363 | my($st3) = $qcursor->c_del; |
---|
| 8364 | $st3==0 || $st3==DB_KEYEMPTY or die "BDB Q c_del: $BerkeleyDB::Error, $!."; |
---|
| 8365 | } |
---|
| 8366 | $stat==0 || $stat==DB_NOTFOUND or die "BDB Q c_get: $BerkeleyDB::Error, $!."; |
---|
| 8367 | $qcursor->c_close==0 or die "BDB Q c_close: $BerkeleyDB::Error, $!."; |
---|
| 8368 | # insert new expiration request in the queue |
---|
| 8369 | $dbq->db_put($key, "$expires_utc_iso8601 $str", DB_APPEND) == 0 |
---|
| 8370 | or die "BDB Q db_put: $BerkeleyDB::Error, $!."; |
---|
| 8371 | # syncing would only be worth doing if we would want the cache to persist |
---|
| 8372 | # across restarts - but we scratch the databases to avoid rebuild worries |
---|
| 8373 | # $stat = $dbq->db_sync(); |
---|
| 8374 | # $stat==0 or warn "BDB Q db_sync, status $stat: $BerkeleyDB::Error, $!."; |
---|
| 8375 | # $stat = $db->db_sync(); |
---|
| 8376 | # $stat==0 or warn "BDB C db_sync, status $stat: $BerkeleyDB::Error, $!."; |
---|
| 8377 | } |
---|
| 8378 | |
---|
| 8379 | sub get { |
---|
| 8380 | my($self,$key) = @_; |
---|
| 8381 | my($val); my($db) = $self->{'db_cache'}; |
---|
| 8382 | if (!defined($db)) { |
---|
| 8383 | $val = $self->{'mem_cache'}{$key}; # simple local memory-based cache |
---|
| 8384 | } else { |
---|
| 8385 | my($stat) = $db->db_get($key,$val); |
---|
| 8386 | $stat==0 || $stat==DB_NOTFOUND |
---|
| 8387 | or die "BDB C c_get: $BerkeleyDB::Error, $!."; |
---|
| 8388 | local($1,$2); |
---|
| 8389 | if ($stat==0 && $val=~/^([^ ]+) (.*)/s) { $val = $2 } else { $val = undef } |
---|
| 8390 | } |
---|
| 8391 | thaw($val); |
---|
| 8392 | } |
---|
| 8393 | |
---|
| 8394 | sub set { |
---|
| 8395 | my($self,$key,$obj,$now_utc_iso8601,$expires_utc_iso8601) = @_; |
---|
| 8396 | my($db) = $self->{'db_cache'}; |
---|
| 8397 | if (!defined($db)) { |
---|
| 8398 | $self->{'mem_cache'}{$key} = freeze($obj); |
---|
| 8399 | } else { |
---|
| 8400 | my($cursor) = $db->db_cursor(DB_WRITECURSOR); |
---|
| 8401 | defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!."; |
---|
| 8402 | my($val); my($stat) = $cursor->c_get($key,$val,DB_SET); |
---|
| 8403 | $stat==0 || $stat==DB_NOTFOUND |
---|
| 8404 | or die "BDB C c_get: $BerkeleyDB::Error, $!."; |
---|
| 8405 | $cursor->c_put($key, $expires_utc_iso8601.' '.freeze($obj), |
---|
| 8406 | $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0 |
---|
| 8407 | or die "BDB C c_put: $BerkeleyDB::Error, $!."; |
---|
| 8408 | $cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!."; |
---|
| 8409 | # $stat = $db->db_sync(); # only worth doing if cache were persistent |
---|
| 8410 | # $stat==0 or warn "BDB C db_sync, status $stat: $BerkeleyDB::Error, $!."; |
---|
| 8411 | $self->enqueue($key,$now_utc_iso8601,$expires_utc_iso8601); |
---|
| 8412 | } |
---|
| 8413 | $obj; |
---|
| 8414 | } |
---|
| 8415 | |
---|
| 8416 | 1; |
---|
| 8417 | |
---|
| 8418 | __DATA__ |
---|
| 8419 | # |
---|
| 8420 | package Amavis::Lookup::SQLfield; |
---|
| 8421 | use strict; |
---|
| 8422 | use re 'taint'; |
---|
| 8423 | |
---|
| 8424 | BEGIN { |
---|
| 8425 | use Exporter (); |
---|
| 8426 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 8427 | $VERSION = '2.034'; |
---|
| 8428 | @ISA = qw(Exporter); |
---|
| 8429 | } |
---|
| 8430 | BEGIN { import Amavis::Util qw(ll do_log) } |
---|
| 8431 | |
---|
| 8432 | sub new($$$;$$) { |
---|
| 8433 | my($class, $sql_query,$fieldname, $fieldtype,$implied_args) = @_; |
---|
| 8434 | # fieldtype: B=boolean, N=numeric, S=string, |
---|
| 8435 | # N-: numeric, nonexistent field returns undef without complaint |
---|
| 8436 | # S-: string, nonexistent field returns undef without complaint |
---|
| 8437 | # B-: boolean, nonexistent field returns undef without complaint |
---|
| 8438 | # B0: boolean, nonexistent field treated as false |
---|
| 8439 | # B1: boolean, nonexistent field treated as true |
---|
| 8440 | return undef if !defined($sql_query); |
---|
| 8441 | my($self) = bless {}, $class; |
---|
| 8442 | $self->{sql_query} = $sql_query; |
---|
| 8443 | $self->{fieldname} = lc($fieldname); |
---|
| 8444 | $self->{fieldtype} = uc($fieldtype); |
---|
| 8445 | $self->{args} = ref($implied_args) eq 'ARRAY' ? [@$implied_args] # copy |
---|
| 8446 | : [$implied_args] if defined $implied_args; |
---|
| 8447 | $self; |
---|
| 8448 | } |
---|
| 8449 | |
---|
| 8450 | sub lookup_sql_field($$$) { |
---|
| 8451 | my($self,$addr,$get_all) = @_; |
---|
| 8452 | my(@result,@matchingkey); |
---|
| 8453 | if (!defined($self)) { |
---|
| 8454 | do_log(5, "lookup_sql_field - undefined, \"$addr\" no match"); |
---|
| 8455 | } elsif (!defined($self->{sql_query})) { |
---|
| 8456 | do_log(5, sprintf("lookup_sql_field(%s) - null query, \"%s\" no match", |
---|
| 8457 | $self->{fieldname}, $addr)); |
---|
| 8458 | } else { |
---|
| 8459 | my($field) = $self->{fieldname}; |
---|
| 8460 | my($res_ref,$mk_ref) = $self->{sql_query}->lookup_sql($addr,1, |
---|
| 8461 | !exists($self->{args}) ? () : $self->{args}); |
---|
| 8462 | do_log(5, "lookup_sql_field($field), \"$addr\" no matching record") |
---|
| 8463 | if !@$res_ref; |
---|
| 8464 | for my $ind (0..$#$res_ref) { |
---|
| 8465 | my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind]; |
---|
| 8466 | if (!exists($h_ref->{$field})) { |
---|
| 8467 | # record found, but no field with that name in the table |
---|
| 8468 | # fieldtype: B0: boolean, nonexistent field treated as false, |
---|
| 8469 | # B1: boolean, nonexistent field treated as true |
---|
| 8470 | if ( $self->{fieldtype} =~ /^B0/) { # boolean, defaults to false |
---|
| 8471 | $match = 0; # nonexistent field treated as 0 |
---|
| 8472 | do_log(5, "lookup_sql_field($field), no field, \"$addr\" result=$match"); |
---|
| 8473 | } elsif ($self->{fieldtype} =~ /^B1/) { # defaults to true |
---|
| 8474 | $match = 1; # nonexistent field treated as 1 |
---|
| 8475 | do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=$match"); |
---|
| 8476 | } elsif ($self->{fieldtype}=~/^.-/s) { # allowed to not exist |
---|
| 8477 | do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=undef"); |
---|
| 8478 | } else { # treated as 'no match', issue a warning |
---|
| 8479 | do_log(1,"lookup_sql_field($field) ". |
---|
| 8480 | "(WARN: no such field in the SQL table), ". |
---|
| 8481 | "\"$addr\" result=undef"); |
---|
| 8482 | } |
---|
| 8483 | } else { # field exists |
---|
| 8484 | # fieldtype: B=boolean, N=numeric, S=string |
---|
| 8485 | $match = $h_ref->{$field}; |
---|
| 8486 | if (!defined($match)) { # NULL field values represented as undef |
---|
| 8487 | } elsif ($self->{fieldtype} =~ /^B/) { # boolean |
---|
| 8488 | # convert values 'N', 'F', '0', ' ' and "\000" to 0 |
---|
| 8489 | # to allow value to be used directly as a Perl boolean |
---|
| 8490 | $match = 0 if $match =~ /^([NnFf ]|0+|\000+)[ ]*\z/; |
---|
| 8491 | } elsif ($self->{fieldtype} =~ /^N/) { # numeric |
---|
| 8492 | $match = $match + 0; # unify different numeric forms |
---|
| 8493 | } elsif ($self->{fieldtype} =~ /^S/) { # string |
---|
| 8494 | $match =~ s/ +\z//; # trim trailing spaces |
---|
| 8495 | } |
---|
| 8496 | do_log(5, "lookup_sql_field($field) \"$addr\" result=" . |
---|
| 8497 | (defined $match ? $match : 'undef') ); |
---|
| 8498 | } |
---|
| 8499 | if (defined $match) { |
---|
| 8500 | push(@result,$match); push(@matchingkey,$mk); |
---|
| 8501 | last if !$get_all; |
---|
| 8502 | } |
---|
| 8503 | } |
---|
| 8504 | } |
---|
| 8505 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
| 8506 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
| 8507 | } |
---|
| 8508 | |
---|
| 8509 | 1; |
---|
| 8510 | |
---|
| 8511 | # |
---|
| 8512 | package Amavis::Lookup::SQL; |
---|
| 8513 | use strict; |
---|
| 8514 | use re 'taint'; |
---|
| 8515 | |
---|
| 8516 | BEGIN { |
---|
| 8517 | use Exporter (); |
---|
| 8518 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 8519 | $VERSION = '2.034'; |
---|
| 8520 | @ISA = qw(Exporter); |
---|
| 8521 | } |
---|
| 8522 | |
---|
| 8523 | use DBI; |
---|
| 8524 | |
---|
| 8525 | BEGIN { |
---|
| 8526 | import Amavis::Conf qw(:platform :confvars c cr ca); |
---|
| 8527 | import Amavis::Timing qw(section_time); |
---|
| 8528 | import Amavis::Util qw(untaint snmp_count ll do_log); |
---|
| 8529 | import Amavis::rfc2821_2822_Tools qw(make_query_keys); |
---|
| 8530 | } |
---|
| 8531 | |
---|
| 8532 | use vars qw($sql_connected); |
---|
| 8533 | |
---|
| 8534 | # Connect to a database. Take a list of database connection |
---|
| 8535 | # parameters and try each until one succeeds. |
---|
| 8536 | # -- based on code from Ben Ransford <amavis@uce.ransford.org> 2002-09-22 |
---|
| 8537 | sub connect_to_sql(@) { |
---|
| 8538 | my(@dsns) = @_; # a list of DSNs to try connecting to sequentially |
---|
| 8539 | my($dbh); |
---|
| 8540 | do_log(3,"Connecting to SQL database server"); |
---|
| 8541 | for my $tmpdsn (@dsns) { |
---|
| 8542 | my($dsn, $username, $password) = @$tmpdsn; |
---|
| 8543 | do_log(4, "connect_to_sql: trying '$dsn'"); |
---|
| 8544 | $dbh = DBI->connect($dsn, $username, $password, |
---|
| 8545 | {PrintError => 0, RaiseError => 0, Taint => 1} ); |
---|
| 8546 | if ($dbh) { do_log(3,"connect_to_sql: '$dsn' succeeded"); last } |
---|
| 8547 | do_log(-1,"connect_to_sql: unable to connect to DSN '$dsn': ".$DBI::errstr); |
---|
| 8548 | } |
---|
| 8549 | do_log(-2,"connect_to_sql: unable to connect to any DSN at all!") |
---|
| 8550 | if !$dbh && @dsns > 1; |
---|
| 8551 | $sql_connected = 1 if $dbh; |
---|
| 8552 | $dbh; |
---|
| 8553 | } |
---|
| 8554 | |
---|
| 8555 | # return a new Lookup::SQL object to contain DBI handle and prepared selects |
---|
| 8556 | sub new { |
---|
| 8557 | my($class) = @_; bless {}, $class; |
---|
| 8558 | } |
---|
| 8559 | |
---|
| 8560 | # explicitly disconnect from SQL server |
---|
| 8561 | sub DESTROY { |
---|
| 8562 | my($self) = shift; |
---|
| 8563 | eval { do_log(5,"Amavis::Lookup::SQL called") }; |
---|
| 8564 | if (defined $self && $self->{dbh} && $sql_connected) { |
---|
| 8565 | $sql_connected = 0; |
---|
| 8566 | eval { $self->{dbh}->disconnect }; $self->{dbh} = undef; |
---|
| 8567 | } |
---|
| 8568 | } |
---|
| 8569 | |
---|
| 8570 | # store DBI handle and prepared selects into existing Lookup::SQL obj |
---|
| 8571 | sub store_dbh($$$) { |
---|
| 8572 | my($self, $dbh, $select_clause) = @_; |
---|
| 8573 | $self->{dbh} = $dbh; # save DBI handle |
---|
| 8574 | $self->{select_clause} = $select_clause; |
---|
| 8575 | $self->clear_cache; # let's start afresh just in case |
---|
| 8576 | $self; |
---|
| 8577 | } |
---|
| 8578 | |
---|
| 8579 | sub clear_cache { |
---|
| 8580 | my($self) = @_; |
---|
| 8581 | delete $self->{cache}; |
---|
| 8582 | } |
---|
| 8583 | |
---|
| 8584 | # lookup_sql() performs a lookup for an e-mail address against a SQL map. |
---|
| 8585 | # If a match is found it returns whatever the map returns (a reference |
---|
| 8586 | # to a hash containing values of requested fields), otherwise returns undef. |
---|
| 8587 | # A match aborts further fetching sequence, unless $get_all is true. |
---|
| 8588 | # |
---|
| 8589 | # SQL lookups (e.g. for user+foo@example.com) are performed in order |
---|
| 8590 | # which can be requested by 'ORDER BY' in the SELECT statement, otherwise |
---|
| 8591 | # the order is unspecified, which is only useful if only specific entries |
---|
| 8592 | # exist in a database (e.g. only full addresses, not domains). |
---|
| 8593 | # |
---|
| 8594 | # The following order is recommended, going from specific to more general: |
---|
| 8595 | # - lookup for user+foo@example.com |
---|
| 8596 | # - lookup for user@example.com (only if $recipient_delimiter nonempty) |
---|
| 8597 | # - lookup for user+foo ('naked lookup': only if local) |
---|
| 8598 | # - lookup for user ('naked lookup': local and $recipient_delimiter nonempty) |
---|
| 8599 | # - lookup for @sub.example.com |
---|
| 8600 | # - lookup for @.sub.example.com |
---|
| 8601 | # - lookup for @.example.com |
---|
| 8602 | # - lookup for @.com |
---|
| 8603 | # - lookup for @. (catchall) |
---|
| 8604 | # NOTE: |
---|
| 8605 | # this is different from hash and ACL lookups in two important aspects: |
---|
| 8606 | # - a key without '@' implies mailbox (=user) name, not domain name; |
---|
| 8607 | # - the naked mailbox name lookups are only performed when the e-mail addr |
---|
| 8608 | # (usually its domain part) matches the static local_domains* lookups. |
---|
| 8609 | # |
---|
| 8610 | # The domain part is always lowercased when constructing a key, |
---|
| 8611 | # the localpart is lowercased unless $localpart_is_case_sensitive is true. |
---|
| 8612 | # |
---|
| 8613 | sub lookup_sql($$$;$) { |
---|
| 8614 | my($self, $addr,$get_all,$extra_args) = @_; |
---|
| 8615 | my(@matchingkey,@result); |
---|
| 8616 | if (!defined $extra_args && |
---|
| 8617 | exists $self->{cache} && exists $self->{cache}->{$addr}) |
---|
| 8618 | { # cached ? |
---|
| 8619 | my($c) = $self->{cache}->{$addr}; @result = @$c if ref $c; |
---|
| 8620 | @matchingkey = map {'/cached/'} @result; #will do for now, improve some day |
---|
| 8621 | if (!ll(5)) { |
---|
| 8622 | # don't bother preparing log report which will not be printed |
---|
| 8623 | } elsif (!@result) { |
---|
| 8624 | do_log(5,"lookup_sql (cached): \"$addr\" no match"); |
---|
| 8625 | } else { |
---|
| 8626 | for my $m (@result) { |
---|
| 8627 | do_log(5, sprintf("lookup_sql (cached): \"%s\" matches, result=(%s)", |
---|
| 8628 | $addr, join(", ", map { sprintf("%s=>%s", $_, |
---|
| 8629 | !defined($m->{$_})?'-':'"'.$m->{$_}.'"' |
---|
| 8630 | ) } sort keys(%$m) ) )); |
---|
| 8631 | } |
---|
| 8632 | } |
---|
| 8633 | if (!$get_all) { |
---|
| 8634 | return(!wantarray ? $result[0] : ($result[0], $matchingkey[0])); |
---|
| 8635 | } else { |
---|
| 8636 | return(!wantarray ? \@result : (\@result, \@matchingkey)); |
---|
| 8637 | } |
---|
| 8638 | } |
---|
| 8639 | if (!$sql_connected) { |
---|
| 8640 | my($sql_dbh) = connect_to_sql(@lookup_sql_dsn); |
---|
| 8641 | section_time('sql-connect'); |
---|
| 8642 | defined($sql_dbh) or die "SQL server(s) not reachable"; |
---|
| 8643 | $sql_dbh->{'RaiseError'} = 1; |
---|
| 8644 | $Amavis::sql_policy->store_dbh($sql_dbh, $sql_select_policy) |
---|
| 8645 | if defined $sql_select_policy; |
---|
| 8646 | $Amavis::sql_wblist->store_dbh($sql_dbh, $sql_select_white_black_list) |
---|
| 8647 | if defined $sql_select_white_black_list; |
---|
| 8648 | } |
---|
| 8649 | my($is_local); # $local_domains_sql is not looked up to avoid recursion! |
---|
| 8650 | $is_local = Amavis::Lookup::lookup(0,$addr, |
---|
| 8651 | grep {ref ne 'Amavis::Lookup::SQL' && |
---|
| 8652 | ref ne 'Amavis::Lookup::SQLfield' && |
---|
| 8653 | ref ne 'Amavis::Lookup::LDAP' && |
---|
| 8654 | ref ne 'Amavis::Lookup::LDAPattr'} |
---|
| 8655 | @{ca('local_domains_maps')}); |
---|
| 8656 | my($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local); |
---|
| 8657 | my($n) = sprintf("%d",scalar(@$keys_ref)); # number of keys |
---|
| 8658 | my($sel) = $self->{select_clause}; |
---|
| 8659 | my(@pos_args); my(@extras_tmp) = !ref $extra_args ? () : @$extra_args; |
---|
| 8660 | $sel =~ s{ ( %k | \? ) } # substitute %k for keys and ? for each extra arg |
---|
| 8661 | { push(@pos_args, map { untaint($_) } |
---|
| 8662 | $1 eq '%k' ? @$keys_ref : shift @extras_tmp), |
---|
| 8663 | $1 eq '%k' ? join(',', ('?') x $n) : '?' }gxe; |
---|
| 8664 | if (!exists $self->{"sth$n"}) { |
---|
| 8665 | # 'prepare' appropriate query only when needed, and save it for reuse |
---|
| 8666 | do_log(5,"SQL prepare($n): $sel"); |
---|
| 8667 | $self->{"sth$n"} = $self->{dbh}->prepare($sel); |
---|
| 8668 | } |
---|
| 8669 | my($sth) = $self->{"sth$n"}; |
---|
| 8670 | do_log(4,"lookup_sql \"$addr\", query args: ". |
---|
| 8671 | join(', ', map{"\"$_\""} @pos_args)); |
---|
| 8672 | do_log(4,"lookup_sql select: $sel"); |
---|
| 8673 | my($a_ref,$found); my($match) = {}; |
---|
| 8674 | eval { |
---|
| 8675 | snmp_count('OpsSqlSelect'); |
---|
| 8676 | $sth->execute(@pos_args); # do the query |
---|
| 8677 | while ( defined($a_ref=$sth->fetchrow_arrayref) ) { # fetch query results |
---|
| 8678 | my(@names) = @{$sth->{NAME_lc}}; |
---|
| 8679 | $match = {}; @$match{@names} = @$a_ref; |
---|
| 8680 | if (!exists $match->{'local'} && $match->{'email'} eq '@.') { |
---|
| 8681 | # UGLY HACK to let a catchall (@.) imply that field 'local' has |
---|
| 8682 | # a value undef (NULL) when that field is not present in the |
---|
| 8683 | # database. This overrides B1 fieldtype default by an explicit |
---|
| 8684 | # undef for '@.', causing a fallback to static lookup tables. |
---|
| 8685 | # The purpose is to provide a useful default for local_domains |
---|
| 8686 | # lookup if the field 'local' is not present in the SQL table. |
---|
| 8687 | # NOTE: field names 'local' and 'email' are hardwired here!!! |
---|
| 8688 | push(@names,'local'); $match->{'local'} = undef; |
---|
| 8689 | do_log(5, "lookup_sql: \"$addr\" matches catchall, local=>undef"); |
---|
| 8690 | } |
---|
| 8691 | push(@result, {%$match}); # copy hash |
---|
| 8692 | push(@matchingkey, join(", ", map { sprintf("%s=>%s", $_, |
---|
| 8693 | !defined($match->{$_})?'-':'"'.$match->{$_}.'"' |
---|
| 8694 | ) } @names)); |
---|
| 8695 | last if !$get_all; |
---|
| 8696 | } |
---|
| 8697 | $sth->finish(); |
---|
| 8698 | }; # eval |
---|
| 8699 | if ($@ ne '') { |
---|
| 8700 | my($err) = $@; |
---|
| 8701 | do_log(-1, "lookup_sql: $DBI::err, $DBI::errstr"); |
---|
| 8702 | if (!$sth) {} |
---|
| 8703 | elsif ($sth->err eq '2006' || $sth->errstr =~ /\bserver has gone away\b/ || |
---|
| 8704 | $sth->err eq '2013' || $sth->errstr =~ /\bLost connection to\b/) { |
---|
| 8705 | do_log(-1,"NOTICE: Disconnected from SQL server"); |
---|
| 8706 | $sql_connected = 0; $self->{dbh}->disconnect; $self->{dbh} = undef; |
---|
| 8707 | } |
---|
| 8708 | die $err; |
---|
| 8709 | } |
---|
| 8710 | if (!ll(4)) { |
---|
| 8711 | # don't bother preparing log report which will not be printed |
---|
| 8712 | } elsif (!@result) { |
---|
| 8713 | do_log(4, "lookup_sql, \"$addr\" no match") |
---|
| 8714 | } else { |
---|
| 8715 | do_log(4, "lookup_sql($addr) matches, result=($_)") for @matchingkey; |
---|
| 8716 | } |
---|
| 8717 | # save for future use, but only within processing of this message |
---|
| 8718 | $self->{cache}->{$addr} = \@result; |
---|
| 8719 | section_time('lookup_sql'); |
---|
| 8720 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
| 8721 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
| 8722 | } |
---|
| 8723 | |
---|
| 8724 | 1; |
---|
| 8725 | |
---|
| 8726 | __DATA__ |
---|
| 8727 | #^L |
---|
| 8728 | package Amavis::Lookup::LDAPattr; |
---|
| 8729 | |
---|
| 8730 | use strict; |
---|
| 8731 | use re 'taint'; |
---|
| 8732 | |
---|
| 8733 | BEGIN { |
---|
| 8734 | use Exporter (); |
---|
| 8735 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 8736 | $VERSION = '2.034'; |
---|
| 8737 | @ISA = qw(Exporter); |
---|
| 8738 | |
---|
| 8739 | import Amavis::Util qw(ll do_log) |
---|
| 8740 | } |
---|
| 8741 | |
---|
| 8742 | # attrtype: B=boolean, N=numeric, S=string, L=list |
---|
| 8743 | # N-: numeric, nonexistent field returns undef without complaint |
---|
| 8744 | # S-: string, nonexistent field returns undef without complaint |
---|
| 8745 | # L-: list, nonexistent field returns undef without complaint |
---|
| 8746 | # B-: boolean, nonexistent field returns undef without complaint |
---|
| 8747 | # B0: boolean, nonexistent field treated as false |
---|
| 8748 | # B1: boolean, nonexistent field treated as true |
---|
| 8749 | |
---|
| 8750 | sub new($$$;$) { |
---|
| 8751 | my($class,$ldap_query,$attrname,$attrtype) = @_; |
---|
| 8752 | return undef if !defined($ldap_query); |
---|
| 8753 | my($self) = bless {}, $class; |
---|
| 8754 | $self->{ldap_query} = $ldap_query; |
---|
| 8755 | $self->{attrname} = lc($attrname); |
---|
| 8756 | $self->{attrtype} = uc($attrtype); |
---|
| 8757 | $self; |
---|
| 8758 | } |
---|
| 8759 | |
---|
| 8760 | sub lookup_ldap_attr($$$) { |
---|
| 8761 | my($self,$addr,$get_all) = @_; |
---|
| 8762 | my(@result,@matchingkey); |
---|
| 8763 | if (!defined($self)) { |
---|
| 8764 | do_log(5,"lookup_ldap_attr - undefined, \"$addr\" no match"); |
---|
| 8765 | } elsif (!defined($self->{ldap_query})) { |
---|
| 8766 | do_log(5,sprintf("lookup_ldap_attr(%s) - null query, \"%s\" no match", |
---|
| 8767 | $self->{attrname}, $addr)); |
---|
| 8768 | } else { |
---|
| 8769 | my($attr) = $self->{attrname}; |
---|
| 8770 | my($res_ref,$mk_ref) = $self->{ldap_query}->lookup_ldap($addr,1); |
---|
| 8771 | do_log(5,"lookup_ldap_attr($attr), \"$addr\" no matching record") |
---|
| 8772 | if !@$res_ref; |
---|
| 8773 | for my $ind (0..$#$res_ref) { |
---|
| 8774 | my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind]; |
---|
| 8775 | if (!exists($h_ref->{$attr})) { |
---|
| 8776 | # record found, but no attribute with that name in the table |
---|
| 8777 | if ( $self->{attrtype} =~ /^B0/) { # boolean, defaults to false |
---|
| 8778 | $match = 0; # nonexistent attribute treated as 0 |
---|
| 8779 | do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match"); |
---|
| 8780 | } elsif ($self->{attrtype} =~ /^B1/) { # boolean, defaults to true |
---|
| 8781 | $match = 1; # nonexistent attribute treated as 1 |
---|
| 8782 | do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match"); |
---|
| 8783 | } elsif ($self->{attrtype}=~/^.-/s) { # allowed to not exist |
---|
| 8784 | do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=undef"); |
---|
| 8785 | } else { # treated as 'no match', issue a warning |
---|
| 8786 | do_log(1,"lookup_ldap_attr($attr) ". |
---|
| 8787 | "(WARN: no such attribute in LDAP entry), ". |
---|
| 8788 | "\"$addr\" result=undef"); |
---|
| 8789 | } |
---|
| 8790 | } else { # attribute exists |
---|
| 8791 | $match = $h_ref->{$attr}; |
---|
| 8792 | if (!defined($match)) { # NULL attribute values represented as undef |
---|
| 8793 | } elsif ($self->{attrtype} =~ /^B/) { # boolean |
---|
| 8794 | $match = $match eq "TRUE" ? 1 : 0; # convert TRUE|FALSE to 1|0 |
---|
| 8795 | } elsif ($self->{attrtype} =~ /^N/) { # numeric |
---|
| 8796 | $match = $match + 0; # unify different numeric forms |
---|
| 8797 | } elsif ($self->{attrtype} =~ /^S/) { # string |
---|
| 8798 | $match =~ s/ +\z//; # trim trailing spaces |
---|
| 8799 | } elsif ($self->{attrtype} =~ /^L/) { # list |
---|
| 8800 | #$match = join(", ",@$match); |
---|
| 8801 | } |
---|
| 8802 | do_log(5,sprintf("lookup_ldap_attr(%s) \"%s\" result=(%s)", |
---|
| 8803 | $attr, $addr, defined($match) ? $match : 'undef')); |
---|
| 8804 | } |
---|
| 8805 | if (defined $match) { |
---|
| 8806 | push(@result,$match); push(@matchingkey,$mk); |
---|
| 8807 | last if !$get_all; |
---|
| 8808 | } |
---|
| 8809 | } |
---|
| 8810 | } |
---|
| 8811 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
| 8812 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
| 8813 | } |
---|
| 8814 | |
---|
| 8815 | 1; |
---|
| 8816 | |
---|
| 8817 | # |
---|
| 8818 | package Amavis::Lookup::LDAP; |
---|
| 8819 | |
---|
| 8820 | use strict; |
---|
| 8821 | use re 'taint'; |
---|
| 8822 | |
---|
| 8823 | BEGIN { |
---|
| 8824 | use Exporter (); |
---|
| 8825 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION |
---|
| 8826 | $ldap_sys_default @ldap_attrs); |
---|
| 8827 | $VERSION = '2.034'; |
---|
| 8828 | @ISA = qw(Exporter); |
---|
| 8829 | |
---|
| 8830 | import Amavis::Conf qw(:platform :confvars c cr ca); |
---|
| 8831 | import Amavis::Timing qw(section_time); |
---|
| 8832 | import Amavis::Util qw(untaint snmp_count ll do_log); |
---|
| 8833 | import Amavis::rfc2821_2822_Tools qw(make_query_keys); |
---|
| 8834 | |
---|
| 8835 | $ldap_sys_default = { |
---|
| 8836 | hostname => 'localhost', |
---|
| 8837 | port => 389, |
---|
| 8838 | version => 3, |
---|
| 8839 | timeout => 120, |
---|
| 8840 | tls => 0, |
---|
| 8841 | base => undef, |
---|
| 8842 | scope => 'sub', |
---|
| 8843 | query_filter => '(&(objectClass=amavisAccount)(mail=%m))', |
---|
| 8844 | bind_dn => undef, |
---|
| 8845 | bind_password => undef, |
---|
| 8846 | }; |
---|
| 8847 | |
---|
| 8848 | @ldap_attrs = qw(amavisVirusLover amavisSpamLover amavisBannedFilesLover |
---|
| 8849 | amavisBadHeaderLover amavisBypassVirusChecks amavisBypassSpamChecks |
---|
| 8850 | amavisBypassBannedChecks amavisBypassHeaderChecks amavisSpamTagLevel |
---|
| 8851 | amavisSpamTag2Level amavisSpamKillLevel amavisSpamModifiesSubj |
---|
| 8852 | amavisVirusQuarantineTo amavisSpamQuarantineTo amavisBannedQuarantineTo |
---|
| 8853 | amavisBadHeaderQuarantineTo amavisBlacklistSender amavisWhitelistSender |
---|
| 8854 | amavisLocal amavisMessageSizeLimit mail |
---|
| 8855 | amavisWarnVirusRecip amavisWarnBannedRecip amavisWarnBadHeaderRecip |
---|
| 8856 | ); |
---|
| 8857 | } |
---|
| 8858 | |
---|
| 8859 | use vars qw($ldap_connected); |
---|
| 8860 | |
---|
| 8861 | sub new { |
---|
| 8862 | my($class,$default) = @_; |
---|
| 8863 | my($self) = bless {}, $class; |
---|
| 8864 | for (qw(hostname port timeout tls base scope query_filter |
---|
| 8865 | bind_dn bind_password)) { |
---|
| 8866 | # replace undefined attributes with defaults |
---|
| 8867 | $self->{$_} = $default->{$_} unless defined($self->{$_}); |
---|
| 8868 | $self->{$_} = $ldap_sys_default->{$_} unless defined($self->{$_}); |
---|
| 8869 | } |
---|
| 8870 | $self; |
---|
| 8871 | } |
---|
| 8872 | |
---|
| 8873 | # explicitly disconnect from LDAP server |
---|
| 8874 | sub DESTROY { |
---|
| 8875 | my($self) = shift; |
---|
| 8876 | eval { do_log(5,"Amavis::Lookup::LDAP called") }; |
---|
| 8877 | if (defined $self && $self->{ldap} && $ldap_connected) { |
---|
| 8878 | $ldap_connected = 0; |
---|
| 8879 | eval { $self->{ldap}->disconnect }; $self->{ldap} = undef; |
---|
| 8880 | } |
---|
| 8881 | } |
---|
| 8882 | |
---|
| 8883 | sub connect_to_ldap { |
---|
| 8884 | my($self) = @_; |
---|
| 8885 | my($ldap); |
---|
| 8886 | do_log(3,"Connecting to LDAP host"); |
---|
| 8887 | my $hostlist = ref $self->{hostname} eq 'ARRAY' ? |
---|
| 8888 | join(", ",@{$self->{hostname}}) : $self->{hostname}; |
---|
| 8889 | do_log(4,"connect_to_ldap: trying $hostlist"); |
---|
| 8890 | $ldap = Net::LDAP->new($self->{hostname}, |
---|
| 8891 | port => $self->{port}, |
---|
| 8892 | version => $self->{version}, |
---|
| 8893 | timeout => $self->{timeout}, |
---|
| 8894 | onerror => 'undef'); |
---|
| 8895 | if ($ldap) { |
---|
| 8896 | do_log(3,"connect_to_ldap: connected to $hostlist"); |
---|
| 8897 | } else { |
---|
| 8898 | do_log(-1,"connect_to_ldap: unable to connect to host $hostlist"); |
---|
| 8899 | return undef; |
---|
| 8900 | } |
---|
| 8901 | if ($self->{tls}) { # TLS required |
---|
| 8902 | my $tlsVer = $ldap->start_tls(verify=>'none'); |
---|
| 8903 | do_log(3,"connect_to_ldap: TLS version $tlsVer enabled"); |
---|
| 8904 | } |
---|
| 8905 | if ($self->{bind_dn}) { # binding required |
---|
| 8906 | if ($ldap->bind($self->{bind_dn}, password => $self->{bind_password})) { |
---|
| 8907 | do_log(3,"connect_to_ldap: bind $self->{bind_dn} succeeded"); |
---|
| 8908 | } else { |
---|
| 8909 | do_log(-1,"connect_to_ldap: bind $self->{bind_dn} failed"); |
---|
| 8910 | return undef; |
---|
| 8911 | } |
---|
| 8912 | } |
---|
| 8913 | $ldap_connected = 1 if $ldap; |
---|
| 8914 | $ldap; |
---|
| 8915 | } |
---|
| 8916 | |
---|
| 8917 | sub store_ldap($$) { |
---|
| 8918 | my($self,$ldap) = @_; |
---|
| 8919 | $self->{ldap} = $ldap; # save LDAP handle |
---|
| 8920 | $self->clear_cache; # let's start afresh just in case |
---|
| 8921 | $self; |
---|
| 8922 | } |
---|
| 8923 | |
---|
| 8924 | sub clear_cache { |
---|
| 8925 | my($self) = @_; |
---|
| 8926 | delete $self->{cache}; |
---|
| 8927 | } |
---|
| 8928 | |
---|
| 8929 | sub lookup_ldap($$$) { |
---|
| 8930 | my($self,$addr,$get_all) = @_; |
---|
| 8931 | my(@matchingkey,@result); |
---|
| 8932 | if (exists $self->{cache} && exists $self->{cache}->{$addr}) { # cached? |
---|
| 8933 | my($c) = $self->{cache}->{$addr}; @result = @$c if ref $c; |
---|
| 8934 | @matchingkey = map {'/cached/'} @result; # will do for now, improve some day |
---|
| 8935 | if (!ll(5)) { |
---|
| 8936 | # don't bother preparing log report which will not be printed |
---|
| 8937 | } elsif (!@result) { |
---|
| 8938 | do_log(5,"lookup_ldap (cached): \"$addr\" no match"); |
---|
| 8939 | } else { |
---|
| 8940 | for my $m (@result) { |
---|
| 8941 | do_log(5, sprintf("lookup_ldap (cached): \"%s\" matches, result=(%s)", |
---|
| 8942 | $addr, join(", ", map { sprintf("%s=>%s", $_, |
---|
| 8943 | !defined($m->{$_})?'-':'"'.$m->{$_}.'"' |
---|
| 8944 | ) } sort keys(%$m) ) )); |
---|
| 8945 | } |
---|
| 8946 | } |
---|
| 8947 | if (!$get_all) { |
---|
| 8948 | return(!wantarray ? $result[0] : ($result[0], $matchingkey[0])); |
---|
| 8949 | } else { |
---|
| 8950 | return(!wantarray ? \@result : (\@result, \@matchingkey)); |
---|
| 8951 | } |
---|
| 8952 | } |
---|
| 8953 | if (!$ldap_connected) { |
---|
| 8954 | my($ldap) = $self->connect_to_ldap; |
---|
| 8955 | defined($ldap) or die "LDAP server(s) not reachable"; |
---|
| 8956 | $self->store_ldap($ldap); |
---|
| 8957 | section_time('ldap-connect'); |
---|
| 8958 | } |
---|
| 8959 | my($is_local); # LDAP is not looked up to avoid recursion! |
---|
| 8960 | $is_local = Amavis::Lookup::lookup(0,$addr, |
---|
| 8961 | grep {ref ne 'Amavis::Lookup::SQL' && |
---|
| 8962 | ref ne 'Amavis::Lookup::SQLfield' && |
---|
| 8963 | ref ne 'Amavis::Lookup::LDAP' && |
---|
| 8964 | ref ne 'Amavis::Lookup::LDAPattr'} |
---|
| 8965 | @{ca('local_domains_maps')}); |
---|
| 8966 | my($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local); |
---|
| 8967 | my(@keys) = @$keys_ref; |
---|
| 8968 | $_ = untaint($_) for @keys; # untaint keys |
---|
| 8969 | do_log(4,sprintf("lookup_ldap \"%s\", query keys: %s, %s", |
---|
| 8970 | $addr, join(', ',map{"\"$_\""}@keys), $self->{query_filter})); |
---|
| 8971 | my($result); |
---|
| 8972 | eval { |
---|
| 8973 | snmp_count('OpsLDAPSearch'); |
---|
| 8974 | for my $key (@keys) { |
---|
| 8975 | my($match) = {}; |
---|
| 8976 | (my $filter = $self->{query_filter}) =~ s/%m/$key/g; |
---|
| 8977 | do_log(9,sprintf( |
---|
| 8978 | "lookup_ldap: searching base=\"%s\", scope=\"%s\", filter=\"%s\"", |
---|
| 8979 | $self->{base}, $self->{scope}, $filter)); |
---|
| 8980 | $result = $self->{ldap}->search(base => $self->{base}, |
---|
| 8981 | scope => $self->{scope}, |
---|
| 8982 | filter => $filter, |
---|
| 8983 | attrs => [@ldap_attrs],); |
---|
| 8984 | my $entry = $result->entry(0); # only use first record returned |
---|
| 8985 | next unless $entry; # no entry found, try next key |
---|
| 8986 | for my $attr (@ldap_attrs) { |
---|
| 8987 | my($value); |
---|
| 8988 | $attr = lc($attr); |
---|
| 8989 | do_log(9,"lookup_ldap: reading attribute \"$attr\" from object"); |
---|
| 8990 | if ($attr =~ /^amavis(white|black)listsender\z/) { # multivalued (list) |
---|
| 8991 | $value = $entry->get_value($attr, asref => 1); |
---|
| 8992 | } else { |
---|
| 8993 | $value = $entry->get_value($attr); |
---|
| 8994 | } |
---|
| 8995 | $match->{$attr} = $value if $value; |
---|
| 8996 | } |
---|
| 8997 | if (!exists $match->{'amavislocal'} && $match->{'mail'} eq '@.') { |
---|
| 8998 | # NOTE: see lookup_sql |
---|
| 8999 | $match->{'amavislocal'} = undef; |
---|
| 9000 | do_log(5,"lookup_ldap: \"$addr\" matches catchall, amavislocal=>undef"); |
---|
| 9001 | } |
---|
| 9002 | push(@result, {%$match}); # copy hash |
---|
| 9003 | push(@matchingkey, join(", ", map { sprintf("%s=>%s", $_, |
---|
| 9004 | !defined($match->{$_})?'-':'"'.$match->{$_}.'"' ) } keys(%$match))); |
---|
| 9005 | last if !$get_all; |
---|
| 9006 | } |
---|
| 9007 | }; # eval |
---|
| 9008 | if ($@ ne '') { |
---|
| 9009 | my($err) = $@; |
---|
| 9010 | do_log(-1,"lookup_ldap: $err"); |
---|
| 9011 | if (!$result || $result->code() != 'LDAP_SUCCESS') { |
---|
| 9012 | my $code = $result?$result->code():-1; |
---|
| 9013 | my $errname = Net::LDAP::Util::ldap_error_name->($code); |
---|
| 9014 | if ($errname eq 'LDAP_PARAM_ERROR') { |
---|
| 9015 | do_log(-1,"NOTICE: LDAP error - LDAP_PARAM_ERROR"); |
---|
| 9016 | } |
---|
| 9017 | else { # probably lost connection to server |
---|
| 9018 | do_log(-1,"NOTICE: Check LDAP server, lost connection?"); |
---|
| 9019 | $ldap_connected = 0; $self->{ldap}->disconnect; $self->{ldap} = undef; |
---|
| 9020 | } |
---|
| 9021 | } |
---|
| 9022 | die $err; |
---|
| 9023 | } |
---|
| 9024 | if (!ll(4)) { |
---|
| 9025 | # don't bother preparing log report which will not be printed |
---|
| 9026 | } elsif (!@result) { |
---|
| 9027 | do_log(4,"lookup_ldap, \"$addr\" no match") |
---|
| 9028 | } else { |
---|
| 9029 | do_log(4,"lookup_ldap($addr) matches, result=($_)") for @matchingkey; |
---|
| 9030 | } |
---|
| 9031 | # save for future use, but only within processing of this message |
---|
| 9032 | $self->{cache}->{$addr} = \@result; |
---|
| 9033 | section_time('lookup_ldap'); |
---|
| 9034 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
| 9035 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
| 9036 | } |
---|
| 9037 | |
---|
| 9038 | 1; |
---|
| 9039 | |
---|
| 9040 | __DATA__ |
---|
| 9041 | # |
---|
| 9042 | package Amavis::In::AMCL; |
---|
| 9043 | use strict; |
---|
| 9044 | use re 'taint'; |
---|
| 9045 | |
---|
| 9046 | BEGIN { |
---|
| 9047 | use Exporter (); |
---|
| 9048 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 9049 | $VERSION = '2.034'; |
---|
| 9050 | @ISA = qw(Exporter); |
---|
| 9051 | } |
---|
| 9052 | |
---|
| 9053 | use subs @EXPORT; |
---|
| 9054 | use Errno qw(ENOENT EACCES); |
---|
| 9055 | use IO::File (); |
---|
| 9056 | |
---|
| 9057 | BEGIN { |
---|
| 9058 | import Amavis::Conf qw(:platform :confvars c cr ca); |
---|
| 9059 | import Amavis::Util qw(ll do_log debug_oneshot snmp_counters_init snmp_count |
---|
| 9060 | am_id new_am_id untaint rmdir_recursively); |
---|
| 9061 | import Amavis::Lookup qw(lookup); |
---|
| 9062 | import Amavis::Timing qw(section_time); |
---|
| 9063 | import Amavis::rfc2821_2822_Tools; |
---|
| 9064 | import Amavis::In::Message; |
---|
| 9065 | import Amavis::In::Connection; |
---|
| 9066 | import Amavis::Out::EditHeader qw(hdr); |
---|
| 9067 | import Amavis::rfc2821_2822_Tools qw(/^EX_/); |
---|
| 9068 | } |
---|
| 9069 | |
---|
| 9070 | sub new($) { my($class) = @_; bless {}, $class } |
---|
| 9071 | |
---|
| 9072 | # (used with sendmail milter and traditional (non-SMTP) MTA interface) |
---|
| 9073 | # |
---|
| 9074 | sub process_policy_request($$$$) { |
---|
| 9075 | my($self, $sock, $conn, $check_mail, $old_amcl) = @_; |
---|
| 9076 | # $sock: connected socket from Net::Server |
---|
| 9077 | # $conn: information about client connection |
---|
| 9078 | # $check_mail: subroutine ref to be called with file handle |
---|
| 9079 | |
---|
| 9080 | my(%attr); |
---|
| 9081 | $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count); |
---|
| 9082 | if ($old_amcl) { |
---|
| 9083 | # Accept a single request from traditional amavis helper program. |
---|
| 9084 | # Receive TEMPDIR/SENDER/RCPTS/LDA/LDAARGS from client |
---|
| 9085 | # Simple protocol: \2 means LDA follows; \3 means EOT (end of transmission) |
---|
| 9086 | my($state) = 0; $attr{'request'} = 'AM.CL'; my($response) = "\001"; |
---|
| 9087 | my($rv,@recips,@ldaargs,$inbuff); local($1); |
---|
| 9088 | my(@attr_names) = qw(tempdir sender recipient ldaargs); |
---|
| 9089 | while (defined($rv = recv($sock, $inbuff, 8192, 0))) { |
---|
| 9090 | $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count); |
---|
| 9091 | if ($state < 2) { |
---|
| 9092 | $attr{$attr_names[$state]} = $inbuff; $state++; |
---|
| 9093 | } elsif ($state == 2 && $inbuff eq "\002") { |
---|
| 9094 | $state++; |
---|
| 9095 | } elsif ($state >= 2 && $inbuff eq "\003") { |
---|
| 9096 | section_time('got data'); |
---|
| 9097 | $attr{'recipient'} = \@recips; $attr{'ldaargs'} = \@ldaargs; |
---|
| 9098 | $attr{'delivery_care_of'} = @ldaargs ? 'client' : 'server'; |
---|
| 9099 | eval { |
---|
| 9100 | my($msginfo) = preprocess_policy_query(\%attr); |
---|
| 9101 | $response = (map { /^exit_code=(\d+)\z/ ? $1 : () } |
---|
| 9102 | check_amcl_policy($conn,$msginfo,$check_mail,1))[0]; |
---|
| 9103 | }; |
---|
| 9104 | if ($@ ne '') { |
---|
| 9105 | chomp($@); do_log(-2,"policy_server FAILED: $@"); |
---|
| 9106 | $response = EX_TEMPFAIL; |
---|
| 9107 | } |
---|
| 9108 | $state = 4; |
---|
| 9109 | } elsif ($state == 2) { |
---|
| 9110 | push(@recips, $inbuff); |
---|
| 9111 | } else { |
---|
| 9112 | push(@ldaargs, $inbuff); |
---|
| 9113 | } |
---|
| 9114 | defined(send($sock,$response,0)) |
---|
| 9115 | or die "send failed in state $state: $!"; |
---|
| 9116 | last if $state >= 4; |
---|
| 9117 | $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count); |
---|
| 9118 | } |
---|
| 9119 | if ($state==4 && defined($rv)) { |
---|
| 9120 | # normal termination |
---|
| 9121 | } elsif (!defined($rv) && $! != 0) { |
---|
| 9122 | die "recv failed in state $state: $!"; |
---|
| 9123 | } else { # eof or a runaway state |
---|
| 9124 | die "helper client session terminated unexpectedly, state: $state"; |
---|
| 9125 | } |
---|
| 9126 | do_log(2, Amavis::Timing::report()); # report elapsed times |
---|
| 9127 | |
---|
| 9128 | } else { # new amavis helper protocol AM.PDP or a Postfix policy server |
---|
| 9129 | # for Postfix policy server see Postfix docs SMTPD_POLICY_README |
---|
| 9130 | my(@response); local($1,$2,$3); |
---|
| 9131 | local($/) = "\012"; # set line terminator to LF (Postfix idiosyncrasy) |
---|
| 9132 | $! = undef; |
---|
| 9133 | while(<$sock>) { # can accept multiple tasks |
---|
| 9134 | $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count); |
---|
| 9135 | Amavis::Timing::init(); snmp_counters_init(); |
---|
| 9136 | # must not use \r and \n, not \015 and \012 on certain platforms |
---|
| 9137 | if (/^\015?\012\z/) { # end of request |
---|
| 9138 | section_time('got data'); |
---|
| 9139 | eval { |
---|
| 9140 | my($msginfo) = preprocess_policy_query(\%attr); |
---|
| 9141 | @response = $attr{'request'} eq 'smtpd_access_policy' |
---|
| 9142 | ? postfix_policy($conn,$msginfo,\%attr) |
---|
| 9143 | : check_amcl_policy($conn,$msginfo,$check_mail,0); |
---|
| 9144 | }; |
---|
| 9145 | if ($@ ne '') { |
---|
| 9146 | chomp($@); do_log(-2, "policy_server FAILED: $@"); |
---|
| 9147 | @response = (proto_encode('setreply','450','4.5.0',"Failure: $@"), |
---|
| 9148 | proto_encode('return_value','tempfail'), |
---|
| 9149 | proto_encode('exit_code',sprintf("%d",EX_TEMPFAIL))); |
---|
| 9150 | # last; |
---|
| 9151 | } |
---|
| 9152 | $sock->print( map { $_."\015\012" } (@response,'') ) |
---|
| 9153 | or die "Can't write response to socket: $!"; |
---|
| 9154 | %attr = (); @response = (); |
---|
| 9155 | do_log(2, Amavis::Timing::report()); |
---|
| 9156 | } elsif (/^ ([^=\000\012]*?) (=|:[ \t]*) |
---|
| 9157 | ([^\012]*?) \015?\012 \z/xsi) { |
---|
| 9158 | my($attr_name) = Amavis::tcp_lookup_decode($1); |
---|
| 9159 | my($attr_val) = Amavis::tcp_lookup_decode($3); |
---|
| 9160 | if (!exists $attr{$attr_name}) { |
---|
| 9161 | $attr{$attr_name} = $attr_val; |
---|
| 9162 | } else { |
---|
| 9163 | if (!ref($attr{$attr_name})) |
---|
| 9164 | { $attr{$attr_name} = [ $attr{$attr_name} ] } |
---|
| 9165 | push(@{$attr{$attr_name}}, $attr_val); |
---|
| 9166 | } |
---|
| 9167 | my($known_attr) = scalar(grep {$_ eq $attr_name} qw( |
---|
| 9168 | request helo_name protocol_state protocol_name queue_id |
---|
| 9169 | client_name client_address sender recipient) ); |
---|
| 9170 | do_log(!$known_attr?-1:1, "policy protocol: $attr_name=$attr_val"); |
---|
| 9171 | } else { |
---|
| 9172 | do_log(-1, "policy protocol: INVALID ATTRIBUTE LINE: $_"); |
---|
| 9173 | } |
---|
| 9174 | $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count); |
---|
| 9175 | $! = undef; # we'll be interested in the errno from the I/O op in while |
---|
| 9176 | } |
---|
| 9177 | if (!defined($_) && $! != 0) { die "read from client socket FAILED: $!" } |
---|
| 9178 | }; |
---|
| 9179 | $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count); |
---|
| 9180 | } |
---|
| 9181 | |
---|
| 9182 | # Return a new Amavis::In::Message object |
---|
| 9183 | # based on given policy query attributes |
---|
| 9184 | # |
---|
| 9185 | sub preprocess_policy_query($) { |
---|
| 9186 | my($attr_ref) = @_; |
---|
| 9187 | |
---|
| 9188 | my($msginfo) = Amavis::In::Message->new; |
---|
| 9189 | $msginfo->rx_time(time); # now |
---|
| 9190 | |
---|
| 9191 | # amavisd -> amavis-helper protocol query consists of any number of |
---|
| 9192 | # the following lines, the response is terminated by an empty line. |
---|
| 9193 | # The 'request=AM.PDP' is a required first field, the order of |
---|
| 9194 | # remaining fields is arbitrary. |
---|
| 9195 | # Required fields are: request, tempdir, sender, recipient (one or more) |
---|
| 9196 | # request=AM.PDP |
---|
| 9197 | # tempdir=/var/amavis/amavis-milter-MWZmu9Di |
---|
| 9198 | # tempdir_removed_by=client (tempdir_removed_by=server is a default) |
---|
| 9199 | # mail_file=/var/amavis/am.../email.txt (defaults to tempdir/email.txt) |
---|
| 9200 | # sender=<foo@example.com> |
---|
| 9201 | # recipient=<bar1@example.net> |
---|
| 9202 | # recipient=<bar2@example.net> |
---|
| 9203 | # recipient=<bar3@example.net> |
---|
| 9204 | # delivery_care_of=server (client or server, client is a default) |
---|
| 9205 | # queue_id=qid |
---|
| 9206 | # protocol_name=ESMTP |
---|
| 9207 | # helo_name=b.example.com |
---|
| 9208 | # client_address=10.2.3.4 |
---|
| 9209 | |
---|
| 9210 | my($sender,@recips); |
---|
| 9211 | $msginfo->delivery_method( |
---|
| 9212 | lc($attr_ref->{'delivery_care_of'}) eq 'server' ? c('forward_method') :''); |
---|
| 9213 | $msginfo->client_delete(lc($attr_ref->{'tempdir_removed_by'}) eq 'client' |
---|
| 9214 | ? 1 : 0); |
---|
| 9215 | $msginfo->queue_id($attr_ref->{'queue_id'}) |
---|
| 9216 | if exists $attr_ref->{'queue_id'}; |
---|
| 9217 | $msginfo->client_addr($attr_ref->{'client_address'}) |
---|
| 9218 | if exists $attr_ref->{'client_address'}; |
---|
| 9219 | $msginfo->client_name($attr_ref->{'client_name'}) |
---|
| 9220 | if exists $attr_ref->{'client_name'}; |
---|
| 9221 | $msginfo->client_proto($attr_ref->{'protocol_name'}) |
---|
| 9222 | if exists $attr_ref->{'protocol_name'}; |
---|
| 9223 | $msginfo->client_helo($attr_ref->{'helo_name'}) |
---|
| 9224 | if exists $attr_ref->{'helo_name'}; |
---|
| 9225 | if (exists $attr_ref->{'sender'}) { |
---|
| 9226 | $sender = $attr_ref->{'sender'}; |
---|
| 9227 | $sender = unquote_rfc2821_local($sender); |
---|
| 9228 | $msginfo->sender($sender); |
---|
| 9229 | } |
---|
| 9230 | if (exists $attr_ref->{'recipient'}) { |
---|
| 9231 | my($r) = $attr_ref->{'recipient'}; |
---|
| 9232 | @recips = !ref($r) ? $r : @$r; |
---|
| 9233 | $_ = unquote_rfc2821_local($_) for @recips; |
---|
| 9234 | $msginfo->recips(\@recips); |
---|
| 9235 | } |
---|
| 9236 | if (!exists $attr_ref->{'tempdir'}) { |
---|
| 9237 | $msginfo->mail_tempdir($TEMPBASE); # defaults to $TEMPBASE |
---|
| 9238 | } else { |
---|
| 9239 | local($1,$2); my($tempdir) = $attr_ref->{tempdir}; |
---|
| 9240 | $tempdir =~ /^ (?: \Q$TEMPBASE\E | \Q$MYHOME\E ) |
---|
| 9241 | \/ (?! \.{1,2} \z) [A-Za-z0-9_.-]+ \z/xso |
---|
| 9242 | or die "Invalid/unexpected temporary directory name '$tempdir'"; |
---|
| 9243 | $msginfo->mail_tempdir(untaint($tempdir)); |
---|
| 9244 | } |
---|
| 9245 | if (!exists $attr_ref->{'mail_file'}) { |
---|
| 9246 | $msginfo->mail_text_fn($msginfo->mail_tempdir . '/email.txt'); |
---|
| 9247 | } else { |
---|
| 9248 | # SECURITY: just believe the supplied file name, blindly untainting it |
---|
| 9249 | $msginfo->mail_text_fn(untaint($attr_ref->{'mail_file'})); |
---|
| 9250 | } |
---|
| 9251 | if ($msginfo->mail_text_fn ne '') { |
---|
| 9252 | my($fname) = $msginfo->mail_text_fn; |
---|
| 9253 | do_log(5, "preprocess_policy_query: opening mail file '$fname'"); |
---|
| 9254 | # set new amavis message id |
---|
| 9255 | new_am_id( ($fname =~ m{amavis-(milter-)?([^/]+?)\z}s ? $2 : undef) ); |
---|
| 9256 | # file created by amavis helper program, just open it |
---|
| 9257 | my($fh) = IO::File->new; |
---|
| 9258 | $fh->open($fname,'<') or die "Can't open file $fname: $!"; |
---|
| 9259 | binmode($fh,":bytes") or die "Can't cancel :utf8 mode: $!" |
---|
| 9260 | if $unicode_aware; |
---|
| 9261 | $msginfo->mail_text($fh); # save file handle to object |
---|
| 9262 | } |
---|
| 9263 | if ($attr_ref->{'request'} =~ /^AM\.(CL|PDP)\z/) { |
---|
| 9264 | do_log(1, sprintf("%s %s: <%s> -> %s", |
---|
| 9265 | $attr_ref->{'request'}, $msginfo->mail_tempdir, $sender, |
---|
| 9266 | join(',', qquote_rfc2821_local(@recips)) )); |
---|
| 9267 | } else { |
---|
| 9268 | do_log(1, sprintf("%s(%s): %s %s: %s[%s] <%s> -> <%s>", |
---|
| 9269 | @$attr_ref{qw(request protocol_state protocol_name queue_id |
---|
| 9270 | client_name client_address sender recipient)})); |
---|
| 9271 | } |
---|
| 9272 | $msginfo; |
---|
| 9273 | } |
---|
| 9274 | |
---|
| 9275 | sub check_amcl_policy($$$$) { |
---|
| 9276 | my($conn,$msginfo,$check_mail,$old_amcl) = @_; |
---|
| 9277 | |
---|
| 9278 | my($smtp_resp, $exit_code, $preserve_evidence); |
---|
| 9279 | my(%baseline_policy_bank); my($policy_changed) = 0; |
---|
| 9280 | %baseline_policy_bank = %current_policy_bank; |
---|
| 9281 | # do some sanity checks before deciding to call check_mail() |
---|
| 9282 | if (!ref($msginfo->per_recip_data) || !defined($msginfo->mail_text)) { |
---|
| 9283 | $smtp_resp = '450 4.5.0 Incomplete request'; $exit_code = EX_TEMPFAIL; |
---|
| 9284 | } else { |
---|
| 9285 | my($cl_ip) = $msginfo->client_addr; |
---|
| 9286 | if ($cl_ip ne '' && defined $policy_bank{'MYNETS'} |
---|
| 9287 | && lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) { |
---|
| 9288 | Amavis::load_policy_bank('MYNETS'); $policy_changed = 1; |
---|
| 9289 | } |
---|
| 9290 | debug_oneshot(1) if lookup(0,$msginfo->sender,@{ca('debug_sender_maps')}); |
---|
| 9291 | # check_mail() expects open file on $fh, need not be rewound |
---|
| 9292 | my($fh) = $msginfo->mail_text; my($tempdir) = $msginfo->mail_tempdir; |
---|
| 9293 | ($smtp_resp, $exit_code, $preserve_evidence) = |
---|
| 9294 | &$check_mail($conn,$msginfo,0,$tempdir); |
---|
| 9295 | $fh->close or die "Can't close temp file: $!" if $fh; |
---|
| 9296 | $fh = undef; $msginfo->mail_text(undef); |
---|
| 9297 | my($errn) = $tempdir eq '' ? ENOENT : (stat($tempdir) ? 0 : 0+$!); |
---|
| 9298 | if ($tempdir eq '' || $errn == ENOENT) { |
---|
| 9299 | # do nothing |
---|
| 9300 | } elsif ($msginfo->client_delete) { |
---|
| 9301 | do_log(4, "AM.PDP: deletion of $tempdir is client's responsibility"); |
---|
| 9302 | } elsif ($preserve_evidence) { |
---|
| 9303 | do_log(-1,"AM.PDP: tempdir is to be PRESERVED: $tempdir"); |
---|
| 9304 | } else { |
---|
| 9305 | do_log(4, "AM.PDP: tempdir being removed: $tempdir"); |
---|
| 9306 | my($fname) = $msginfo->mail_text_fn; |
---|
| 9307 | unlink($fname) or die "Can't remove file $fname: $!" if $fname ne ''; |
---|
| 9308 | rmdir_recursively($tempdir); |
---|
| 9309 | } |
---|
| 9310 | } |
---|
| 9311 | # amavisd -> amavis-helper protocol response consists of any number of |
---|
| 9312 | # the following lines, the response is terminated by an empty line |
---|
| 9313 | # addrcpt=recipient |
---|
| 9314 | # delrcpt=recipient |
---|
| 9315 | # addheader=hdr_head hdr_body |
---|
| 9316 | # chgheader=index hdr_head hdr_body |
---|
| 9317 | # delheader=index hdr_head |
---|
| 9318 | # replacebody=new_body (not implemented) |
---|
| 9319 | # return_value=continue|reject|discard|accept|tempfail |
---|
| 9320 | # setreply=rcode xcode message |
---|
| 9321 | # exit_code=n (old amavis helper, not applicable to milter) |
---|
| 9322 | |
---|
| 9323 | my(@response); my($rcpt_deletes,$rcpt_count)=(0,0); |
---|
| 9324 | if (ref($msginfo->per_recip_data)) { |
---|
| 9325 | for my $r (@{$msginfo->per_recip_data}) |
---|
| 9326 | { $rcpt_count++; if ($r->recip_done) { $rcpt_deletes++ } } |
---|
| 9327 | } |
---|
| 9328 | if ($smtp_resp=~/^([1-5]\d\d) ([245]\.\d{1,3}\.\d{1,3})(?: |\z)(.*)\z/s) |
---|
| 9329 | { push(@response, proto_encode('setreply', $1,$2,$3)) } |
---|
| 9330 | if ( $exit_code == EX_TEMPFAIL) { |
---|
| 9331 | push(@response, proto_encode('return_value','tempfail')); |
---|
| 9332 | } elsif ($exit_code == EX_NOUSER) { # reject the whole message |
---|
| 9333 | push(@response, proto_encode('return_value','reject')); |
---|
| 9334 | } elsif ($exit_code == EX_UNAVAILABLE) { # reject the whole message |
---|
| 9335 | push(@response, proto_encode('return_value','reject')); |
---|
| 9336 | } elsif ($exit_code == 99) { # discard the whole message |
---|
| 9337 | push(@response, proto_encode('return_value','discard')); |
---|
| 9338 | } elsif ($msginfo->delivery_method ne '') { # explicit forwarding by server |
---|
| 9339 | $rcpt_count==$rcpt_deletes or die "Not all recips done"; # just in case |
---|
| 9340 | # MTA is relieved of duty to deliver a message, amavisd did the forwarding |
---|
| 9341 | $exit_code = EX_OK; # *** 99 or EX_OK; ??? (doesn't really matter with |
---|
| 9342 | # helper client programs which can't do the delivery) |
---|
| 9343 | push(@response, proto_encode('return_value','continue')); # 'discard' ??? |
---|
| 9344 | } elsif ($rcpt_count-$rcpt_deletes <= 0) { # none left, should be discarded |
---|
| 9345 | # discarding could have been requested (?) |
---|
| 9346 | do_log(-1, "WARN: no recips left (forgot to set ". |
---|
| 9347 | "\$forward_method=undef using milter?), $smtp_resp"); |
---|
| 9348 | $exit_code = 99; |
---|
| 9349 | push(@response, proto_encode('return_value','discard')); |
---|
| 9350 | } else { # EX_OK |
---|
| 9351 | for my $r (@{$msginfo->per_recip_data}) { # modified recipient addresses? |
---|
| 9352 | my($addr,$newaddr) = ($r->recip_addr, $r->recip_final_addr); |
---|
| 9353 | if ($r->recip_done) { # delete |
---|
| 9354 | push(@response, proto_encode('delrcpt', |
---|
| 9355 | quote_rfc2821_local($addr))); |
---|
| 9356 | } elsif ($newaddr ne $addr) { # modify, e.g. adding extension |
---|
| 9357 | push(@response, proto_encode('delrcpt', |
---|
| 9358 | quote_rfc2821_local($addr))); |
---|
| 9359 | push(@response, proto_encode('addrcpt', |
---|
| 9360 | quote_rfc2821_local($newaddr))); |
---|
| 9361 | } |
---|
| 9362 | } |
---|
| 9363 | my($hdr_edits) = $msginfo->header_edits; |
---|
| 9364 | if ($hdr_edits) { # any added or modified header fields? |
---|
| 9365 | local($1,$2); |
---|
| 9366 | # Inserting. Not posible to specify placement of header fields in milter! |
---|
| 9367 | for my $hf (@{$hdr_edits->{prepend}}, @{$hdr_edits->{append}}) { |
---|
| 9368 | if ($hf =~ /^([^:]+):[ \t]*(.*?)$/s) |
---|
| 9369 | { push(@response, proto_encode('addheader',$1,$2)) } |
---|
| 9370 | } |
---|
| 9371 | my($field_name,$edit,$field_body); |
---|
| 9372 | while ( ($field_name,$edit) = each %{$hdr_edits->{edit}} ) { |
---|
| 9373 | $field_body = $msginfo->mime_entity->head->get($field_name,0); |
---|
| 9374 | if (!defined($field_body)) { |
---|
| 9375 | # such header field does not exist, do nothing |
---|
| 9376 | } elsif (!defined($edit)) { # delete existing header field |
---|
| 9377 | push(@response, proto_encode('delheader',"1",$field_name)); |
---|
| 9378 | } else { # edit the first occurrence |
---|
| 9379 | chomp($field_body); |
---|
| 9380 | $field_body = hdr($field_name, &$edit($field_name,$field_body)); |
---|
| 9381 | $field_body = $1 if $field_body =~ /^[^:]+:[ \t]*(.*?)$/s; |
---|
| 9382 | push(@response, proto_encode('chgheader', "1", |
---|
| 9383 | $field_name, $field_body)); |
---|
| 9384 | } |
---|
| 9385 | } |
---|
| 9386 | } |
---|
| 9387 | if ($old_amcl) { # milter via old amavis helper program |
---|
| 9388 | # warn if there is anything that should be done but MTA is not capable of |
---|
| 9389 | # (or a helper program can not pass the request) |
---|
| 9390 | for (grep { /^(delrcpt|addrcpt)=/ } @response) |
---|
| 9391 | { do_log(-1, "WARN: MTA can't do: $_") } |
---|
| 9392 | if ($rcpt_deletes && $rcpt_count-$rcpt_deletes > 0) { |
---|
| 9393 | do_log(-1, "WARN: ACCEPT THE WHOLE MESSAGE, ". |
---|
| 9394 | "MTA-in can't do selective recips deletion"); |
---|
| 9395 | } |
---|
| 9396 | } |
---|
| 9397 | push(@response, proto_encode('return_value','continue')); |
---|
| 9398 | } |
---|
| 9399 | push(@response, proto_encode('exit_code',"$exit_code")); |
---|
| 9400 | do_log(2, "mail checking ended: ".join("\n",@response)); |
---|
| 9401 | if ($policy_changed) { |
---|
| 9402 | %current_policy_bank = %baseline_policy_bank; $policy_changed = 0; |
---|
| 9403 | } |
---|
| 9404 | @response; |
---|
| 9405 | } |
---|
| 9406 | |
---|
| 9407 | sub postfix_policy($$$) { |
---|
| 9408 | my($conn,$msginfo,$attr_ref) = @_; |
---|
| 9409 | my(@response); |
---|
| 9410 | if (!exists($attr_ref->{'request'})) { |
---|
| 9411 | die "no 'request' attribute"; |
---|
| 9412 | } elsif ($attr_ref->{'request'} ne 'smtpd_access_policy') { |
---|
| 9413 | die ("unknown 'request' value: " . $attr_ref->{'request'}); |
---|
| 9414 | } else { |
---|
| 9415 | @response = 'action=DUNNO'; |
---|
| 9416 | } |
---|
| 9417 | @response; |
---|
| 9418 | } |
---|
| 9419 | |
---|
| 9420 | sub proto_encode($@) { |
---|
| 9421 | my($attribute_name,@strings) = @_; local($1); |
---|
| 9422 | $attribute_name =~ # encode all but alfanumerics, '_' and '-' |
---|
| 9423 | s/([^0-9a-zA-Z_-])/sprintf("%%%02x",ord($1))/eg; |
---|
| 9424 | for (@strings) { # encode % and nonprintables |
---|
| 9425 | s/([^\041-\044\046-\176])/sprintf("%%%02x",ord($1))/eg; |
---|
| 9426 | } |
---|
| 9427 | ## encode % and nonprintables, but leave SP and TAB as-is |
---|
| 9428 | # $str =~ s/[^\011\040-\044\046-\176]/sprintf("%%%02x",ord($&))/eg; |
---|
| 9429 | $attribute_name . '=' . join(' ',@strings); |
---|
| 9430 | } |
---|
| 9431 | |
---|
| 9432 | 1; |
---|
| 9433 | |
---|
| 9434 | __DATA__ |
---|
| 9435 | # |
---|
| 9436 | package Amavis::In::SMTP; |
---|
| 9437 | use strict; |
---|
| 9438 | use re 'taint'; |
---|
| 9439 | |
---|
| 9440 | BEGIN { |
---|
| 9441 | use Exporter (); |
---|
| 9442 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 9443 | $VERSION = '2.034'; |
---|
| 9444 | @ISA = qw(Exporter); |
---|
| 9445 | } |
---|
| 9446 | use Errno qw(ENOENT EACCES); |
---|
| 9447 | use MIME::Base64; |
---|
| 9448 | |
---|
| 9449 | BEGIN { |
---|
| 9450 | import Amavis::Conf qw(:platform :confvars c cr ca); |
---|
| 9451 | import Amavis::Util qw(ll do_log am_id new_am_id snmp_counters_init |
---|
| 9452 | prolong_timer debug_oneshot sanitize_str |
---|
| 9453 | strip_tempdir rmdir_recursively); |
---|
| 9454 | import Amavis::Lookup qw(lookup lookup_ip_acl); |
---|
| 9455 | import Amavis::Timing qw(section_time); |
---|
| 9456 | import Amavis::rfc2821_2822_Tools; |
---|
| 9457 | import Amavis::In::Message; |
---|
| 9458 | import Amavis::In::Connection; |
---|
| 9459 | } |
---|
| 9460 | |
---|
| 9461 | sub new($) { |
---|
| 9462 | my($class) = @_; |
---|
| 9463 | my($self) = bless {}, $class; |
---|
| 9464 | $self->{sock} = undef; # SMTP socket |
---|
| 9465 | $self->{proto} = undef; # SMTP / ((ESMTP / LMTP) (A | S | SA)? ) |
---|
| 9466 | $self->{pipelining} = undef; # may we buffer responses? |
---|
| 9467 | $self->{smtp_outbuf} = undef; # SMTP responses buffer for PIPELINING |
---|
| 9468 | $self->{fh_pers} = undef; # persistent file handle for email.txt |
---|
| 9469 | $self->{tempdir_persistent} = undef;# temporary directory for check_mail |
---|
| 9470 | $self->{preserve} = undef; # don't delete tempdir on exit |
---|
| 9471 | $self->{tempdir_empty} = 1; # anything of interest in tempdir? |
---|
| 9472 | $self->{session_closed_normally} = undef; # closed properly with QUIT |
---|
| 9473 | $self; |
---|
| 9474 | } |
---|
| 9475 | |
---|
| 9476 | sub preserve_evidence # try to preserve temporary files etc in case of trouble |
---|
| 9477 | { my($self)=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift) } |
---|
| 9478 | |
---|
| 9479 | sub DESTROY { |
---|
| 9480 | my($self) = shift; |
---|
| 9481 | eval { do_log(5,"Amavis::In::SMTP::DESTROY called") }; |
---|
| 9482 | eval { |
---|
| 9483 | $self->{fh_pers}->close |
---|
| 9484 | or die "Can't close temp file: $!" if $self->{fh_pers}; |
---|
| 9485 | my($errn) = $self->{tempdir_pers} eq '' ? ENOENT |
---|
| 9486 | : (stat($self->{tempdir_pers}) ? 0 : 0+$!); |
---|
| 9487 | if (defined $self->{tempdir_pers} && $errn != ENOENT) { |
---|
| 9488 | # this will not be included in the TIMING report, |
---|
| 9489 | # but it only occurs infrequently and doesn't take that long |
---|
| 9490 | if ($self->preserve_evidence && !$self->{tempdir_empty}) { |
---|
| 9491 | do_log(-1,"SMTP shutdown: tempdir is to be PRESERVED: ". |
---|
| 9492 | $self->{tempdir_pers}); |
---|
| 9493 | } else { |
---|
| 9494 | do_log(3, sprintf("SMTP shutdown: %s is being removed: %s%s", |
---|
| 9495 | $self->{tempdir_empty} ? 'empty tempdir' : 'tempdir', |
---|
| 9496 | $self->{tempdir_pers}, |
---|
| 9497 | $self->preserve_evidence ? ', nothing to preserve' : '')); |
---|
| 9498 | rmdir_recursively($self->{tempdir_pers}); |
---|
| 9499 | } |
---|
| 9500 | } |
---|
| 9501 | if (! $self->{session_closed_normally}) { |
---|
| 9502 | $self->smtp_resp(1,"421 4.3.2 Service shutting down, closing channel"); |
---|
| 9503 | } |
---|
| 9504 | }; |
---|
| 9505 | if ($@ ne '') |
---|
| 9506 | { my($eval_stat) = $@; eval { do_log(1,"SMTP shutdown: $eval_stat") } } |
---|
| 9507 | } |
---|
| 9508 | |
---|
| 9509 | sub prepare_tempdir($) { |
---|
| 9510 | my($self) = @_; |
---|
| 9511 | if (! defined $self->{tempdir_pers} ) { |
---|
| 9512 | # invent a name for a temporary directory for this child, and create it |
---|
| 9513 | my($now_iso8601) = iso8601_timestamp(time,1); # or: iso8601_utc_timestamp |
---|
| 9514 | $self->{tempdir_pers} = sprintf("%s/amavis-%s-%05d", |
---|
| 9515 | $TEMPBASE, $now_iso8601, $$); |
---|
| 9516 | } |
---|
| 9517 | my($dname) = $self->{tempdir_pers}; |
---|
| 9518 | my(@stat_list) = lstat($dname); my($errn) = @stat_list ? 0 : 0+$!; |
---|
| 9519 | if (!$errn && ! -d _) { # exists, but is not a directory !? |
---|
| 9520 | die "prepare_tempdir: $dname is not a directory!!!"; |
---|
| 9521 | } elsif (!$errn) { |
---|
| 9522 | my($dev,$ino) = @stat_list; |
---|
| 9523 | if ($dev != $self->{tempdir_dev} || $ino != $self->{tempdir_ino}) { |
---|
| 9524 | do_log(-1,"prepare_tempdir: $dname is no longer the same directory!!!"); |
---|
| 9525 | ($self->{tempdir_dev},$self->{tempdir_ino}) = @stat_list; |
---|
| 9526 | } |
---|
| 9527 | } elsif ($errn == ENOENT) { |
---|
| 9528 | do_log(4,"prepare_tempdir: creating directory $dname"); |
---|
| 9529 | mkdir($dname,0750) or die "Can't create directory $dname: $!"; |
---|
| 9530 | ($self->{tempdir_dev},$self->{tempdir_ino}) = lstat($dname); |
---|
| 9531 | $self->{tempdir_empty} = 1; |
---|
| 9532 | section_time('mkdir tempdir'); |
---|
| 9533 | } |
---|
| 9534 | # prepare temporary file for writing (and reading later) |
---|
| 9535 | my($fname) = $dname . '/email.txt'; |
---|
| 9536 | @stat_list = lstat($fname); $errn = @stat_list ? 0 : 0+$!; |
---|
| 9537 | if (!$errn && ! -f _) { # exists, but is not a regular file !? |
---|
| 9538 | die "prepare_tempdir: $fname is not a regular file!!!"; |
---|
| 9539 | } elsif ($self->{fh_pers} && !$errn && -f _) { |
---|
| 9540 | my($dev,$ino) = @stat_list; |
---|
| 9541 | if ($dev != $self->{file_dev} || $ino != $self->{file_ino}) { |
---|
| 9542 | do_log(-1,"prepare_tempdir: $fname is no longer the same file!!!"); |
---|
| 9543 | ($self->{file_dev}, $self->{file_ino}) = @stat_list; |
---|
| 9544 | } |
---|
| 9545 | $self->{fh_pers}->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
| 9546 | $self->{fh_pers}->truncate(0) or die "Can't truncate mail file: $!"; |
---|
| 9547 | } else { |
---|
| 9548 | do_log(4,"prepare_tempdir: creating file $fname"); |
---|
| 9549 | $self->{fh_pers} = IO::File->new($fname,'+>',0640) |
---|
| 9550 | or die "Can't create file $fname: $!"; |
---|
| 9551 | ($self->{file_dev}, $self->{file_ino}) = lstat($fname); |
---|
| 9552 | section_time('create email.txt'); |
---|
| 9553 | } |
---|
| 9554 | } |
---|
| 9555 | |
---|
| 9556 | sub authenticate($$$) { |
---|
| 9557 | my($state,$auth_mech,$auth_resp) = @_; |
---|
| 9558 | my($result,$newchallenge); |
---|
| 9559 | if ($auth_mech eq 'ANONYMOUS') { # rfc2245 |
---|
| 9560 | $result = [$auth_resp,undef]; |
---|
| 9561 | } elsif ($auth_mech eq 'PLAIN') { # rfc2595, "user\0authname\0pass" |
---|
| 9562 | if (!defined($auth_resp)) { $newchallenge = '' } |
---|
| 9563 | else { $result = [ (split(/\000/,$auth_resp,-1))[0,2] ] } |
---|
| 9564 | } elsif ($auth_mech eq 'LOGIN' && !defined $state) { |
---|
| 9565 | $newchallenge = 'Username:'; $state = []; |
---|
| 9566 | } elsif ($auth_mech eq 'LOGIN' && @$state==0) { |
---|
| 9567 | push(@$state, $auth_resp); $newchallenge = 'Password:'; |
---|
| 9568 | } elsif ($auth_mech eq 'LOGIN' && @$state==1) { |
---|
| 9569 | push(@$state, $auth_resp); $result = $state; |
---|
| 9570 | } # CRAM-MD5:rfc2195, DIGEST-MD5:rfc2831 |
---|
| 9571 | ($state,$result,$newchallenge); |
---|
| 9572 | } |
---|
| 9573 | |
---|
| 9574 | # Accept a SMTP or LMTP connect (which can do any number of SMTP transactions) |
---|
| 9575 | # and call content checking for each message received |
---|
| 9576 | # |
---|
| 9577 | sub process_smtp_request($$$$) { |
---|
| 9578 | my($self, $sock, $lmtp, $conn, $check_mail) = @_; |
---|
| 9579 | # $sock: connected socket from Net::Server |
---|
| 9580 | # $lmtp: use LMTP protocol instead of (E)SMTP |
---|
| 9581 | # $conn: information about client connection |
---|
| 9582 | # $check_mail: subroutine ref to be called with file handle |
---|
| 9583 | |
---|
| 9584 | my($msginfo,$authenticated,$auth_user,$auth_pass); |
---|
| 9585 | $self->{sock} = $sock; |
---|
| 9586 | $self->{pipelining} = 0; # may we buffer responses? |
---|
| 9587 | $self->{smtp_outbuf} = []; # SMTP responses buffer for PIPELINING |
---|
| 9588 | |
---|
| 9589 | my($myheloname); |
---|
| 9590 | # $myheloname = $myhostname; |
---|
| 9591 | # $myheloname = 'localhost'; |
---|
| 9592 | # $myheloname = '[127.0.0.1]'; |
---|
| 9593 | $myheloname = '[' . $conn->socket_ip . ']'; |
---|
| 9594 | |
---|
| 9595 | new_am_id(undef, $Amavis::child_invocation_count, undef); |
---|
| 9596 | my($initial_am_id) = 1; my($sender,@recips); my($got_rcpt); |
---|
| 9597 | my($max_recip_size_limit); # maximum of per-recipient message size limits |
---|
| 9598 | my($terminating,$aborting,$eof,$voluntary_exit); my($seq) = 0; |
---|
| 9599 | my(%xforward_args); my(%baseline_policy_bank); my($policy_changed); |
---|
| 9600 | %baseline_policy_bank = %current_policy_bank; $policy_changed = 0; |
---|
| 9601 | $conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'SMTP'); |
---|
| 9602 | |
---|
| 9603 | # system-wide message size limit, if any |
---|
| 9604 | my($message_size_limit) = c('smtpd_message_size_limit'); |
---|
| 9605 | if ($message_size_limit && $message_size_limit < 65536) |
---|
| 9606 | { $message_size_limit = 65536 } # rfc2821 requires at least 64k |
---|
| 9607 | my($smtpd_greeting_banner_tmp) = c('smtpd_greeting_banner'); |
---|
| 9608 | $smtpd_greeting_banner_tmp =~ |
---|
| 9609 | s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) } |
---|
| 9610 | { { 'helo-name' => $myheloname, |
---|
| 9611 | 'version' => $myversion, |
---|
| 9612 | 'version-id' => $myversion_id, |
---|
| 9613 | 'version-date' => $myversion_date, |
---|
| 9614 | 'product' => $myproduct_name, |
---|
| 9615 | 'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)} |
---|
| 9616 | }egx; |
---|
| 9617 | $self->smtp_resp(1, "220 $smtpd_greeting_banner_tmp"); |
---|
| 9618 | |
---|
| 9619 | $0 = sprintf("amavisd (ch%d-idle)", $Amavis::child_invocation_count); |
---|
| 9620 | Amavis::Timing::go_idle(4); |
---|
| 9621 | undef $!; |
---|
| 9622 | while(<$sock>) { |
---|
| 9623 | $0 = sprintf("amavisd (ch%d-%s)", |
---|
| 9624 | $Amavis::child_invocation_count, am_id()); |
---|
| 9625 | Amavis::Timing::go_busy(5); |
---|
| 9626 | prolong_timer('reading SMTP command'); |
---|
| 9627 | { # a block is used as a 'switch' statement - 'last' will exit from it |
---|
| 9628 | my($cmd) = $_; |
---|
| 9629 | do_log(4, $self->{proto} . "< $cmd"); |
---|
| 9630 | !/^ \s* ([A-Za-z]+) (?: \s+ (.*?) )? \s* \015\012 \z/xs && do { |
---|
| 9631 | $self->smtp_resp(1,"500 5.5.2 Error: bad syntax", 1, $cmd); last; |
---|
| 9632 | }; |
---|
| 9633 | $_ = uc($1); my($args) = $2; |
---|
| 9634 | /^RSET|DATA|QUIT\z/ && $args ne '' && do { |
---|
| 9635 | $self->smtp_resp(1,"501 5.5.4 Error: $_ does not accept arguments", |
---|
| 9636 | 1,$cmd); |
---|
| 9637 | last; |
---|
| 9638 | }; |
---|
| 9639 | /^RSET\z/ && do { $sender = undef; @recips = (); $got_rcpt = 0; |
---|
| 9640 | $max_recip_size_limit = undef; $msginfo = undef; |
---|
| 9641 | if ($policy_changed) { |
---|
| 9642 | %current_policy_bank = %baseline_policy_bank; |
---|
| 9643 | $policy_changed = 0; |
---|
| 9644 | } |
---|
| 9645 | $self->smtp_resp(0,"250 2.0.0 Ok $_"); last }; |
---|
| 9646 | /^NOOP\z/ && do { $self->smtp_resp(1,"250 2.0.0 Ok $_"); last }; |
---|
| 9647 | /^QUIT\z/ && do { |
---|
| 9648 | my($smtpd_quit_banner_tmp) = c('smtpd_quit_banner'); |
---|
| 9649 | $smtpd_quit_banner_tmp =~ |
---|
| 9650 | s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) } |
---|
| 9651 | { { 'helo-name' => $myheloname, |
---|
| 9652 | 'version' => $myversion, |
---|
| 9653 | 'version-id' => $myversion_id, |
---|
| 9654 | 'version-date' => $myversion_date, |
---|
| 9655 | 'product' => $myproduct_name, |
---|
| 9656 | 'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)} |
---|
| 9657 | }egx; |
---|
| 9658 | $self->smtp_resp(1,"221 2.0.0 $smtpd_quit_banner_tmp"); |
---|
| 9659 | $terminating=1; last; |
---|
| 9660 | }; |
---|
| 9661 | ### !$lmtp && /^HELO\z/ && do { # strict |
---|
| 9662 | /^HELO\z/ && do { |
---|
| 9663 | $sender = undef; @recips = (); $got_rcpt = 0; # implies RSET |
---|
| 9664 | $max_recip_size_limit = undef; $msginfo = undef; # forget previous |
---|
| 9665 | if ($policy_changed) |
---|
| 9666 | { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 } |
---|
| 9667 | $self->{pipelining} = 0; $self->smtp_resp(0,"250 $myheloname"); |
---|
| 9668 | $lmtp = 0; $conn->smtp_proto($self->{proto} = 'SMTP'); |
---|
| 9669 | $conn->smtp_helo($args); section_time('SMTP HELO'); last; |
---|
| 9670 | }; |
---|
| 9671 | ### (!$lmtp && /^EHLO\z/ || $lmtp && /^LHLO\z/) && do { # strict |
---|
| 9672 | (/^EHLO\z/ || /^LHLO\z/) && do { |
---|
| 9673 | $sender = undef; @recips = (); $got_rcpt = 0; # implies RSET |
---|
| 9674 | $max_recip_size_limit = undef; $msginfo = undef; # forget previous |
---|
| 9675 | if ($policy_changed) |
---|
| 9676 | { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 } |
---|
| 9677 | $lmtp = /^LHLO\z/ ? 1 : 0; |
---|
| 9678 | $conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'ESMTP'); |
---|
| 9679 | $self->{pipelining} = 1; |
---|
| 9680 | $self->smtp_resp(0,"250 $myheloname\n" . join("\n", |
---|
| 9681 | 'PIPELINING', |
---|
| 9682 | !defined($message_size_limit) ? 'SIZE' |
---|
| 9683 | : sprintf('SIZE %d',$message_size_limit), |
---|
| 9684 | '8BITMIME', |
---|
| 9685 | 'ENHANCEDSTATUSCODES', |
---|
| 9686 | !@{ca('auth_mech_avail')} ? () |
---|
| 9687 | : join(' ','AUTH',@{ca('auth_mech_avail')}), |
---|
| 9688 | 'XFORWARD NAME ADDR PROTO HELO' )); |
---|
| 9689 | $conn->smtp_helo($args); section_time("SMTP $_"); |
---|
| 9690 | last; |
---|
| 9691 | }; |
---|
| 9692 | /^XFORWARD\z/ && do { # Postfix extension |
---|
| 9693 | if (defined($sender)) { |
---|
| 9694 | $self->smtp_resp(0,"503 5.5.1 Error: XFORWARD not allowed within transaction", 1, $cmd); |
---|
| 9695 | last; |
---|
| 9696 | } |
---|
| 9697 | my($bad); |
---|
| 9698 | for (split(' ',$args)) { |
---|
| 9699 | if (!/^( [A-Za-z0-9] [A-Za-z0-9-]* ) = ( [\041-\176]{0,255} )\z/xs) { |
---|
| 9700 | $self->smtp_resp(0,"501 5.5.4 Syntax error in XFORWARD parameters", |
---|
| 9701 | 1, $cmd); |
---|
| 9702 | $bad = 1; last; |
---|
| 9703 | } else { |
---|
| 9704 | my($name,$val) = (uc($1), $2); |
---|
| 9705 | if ($name =~ /^(?:NAME|ADDR|PROTO|HELO)\z/) { |
---|
| 9706 | $val = undef if uc($val) eq '[UNAVAILABLE]'; |
---|
| 9707 | $xforward_args{$name} = $val; |
---|
| 9708 | } else { |
---|
| 9709 | $self->smtp_resp(0,"501 5.5.4 XFORWARD command parameter error: $name=$val",1,$cmd); |
---|
| 9710 | $bad = 1; last; |
---|
| 9711 | } |
---|
| 9712 | } |
---|
| 9713 | } |
---|
| 9714 | $self->smtp_resp(1,"250 2.5.0 Ok") if !$bad; |
---|
| 9715 | last; |
---|
| 9716 | }; |
---|
| 9717 | /^HELP\z/ && do { |
---|
| 9718 | $self->smtp_resp(1,"214 2.0.0 See amavisd-new home page at:\n". |
---|
| 9719 | "http://www.ijs.si/software/amavisd/"); |
---|
| 9720 | last; |
---|
| 9721 | }; |
---|
| 9722 | /^AUTH\z/ && @{ca('auth_mech_avail')} && do { # rfc2554 |
---|
| 9723 | if ($args !~ /^([^ ]+)(?: ([^ ]*))?\z/is) { |
---|
| 9724 | $self->smtp_resp(0,"501 5.5.2 Syntax: AUTH mech [initresp]",1,$cmd); |
---|
| 9725 | last; |
---|
| 9726 | } |
---|
| 9727 | my($auth_mech,$auth_resp) = (uc($1), $2); |
---|
| 9728 | if ($authenticated) { |
---|
| 9729 | $self->smtp_resp(0,"503 5.5.1 Error: session already authenticated", 1, $cmd); |
---|
| 9730 | } elsif (defined($sender)) { |
---|
| 9731 | $self->smtp_resp(0,"503 5.5.1 Error: AUTH not allowed within transaction", 1, $cmd); |
---|
| 9732 | } elsif (!grep {uc($_) eq $auth_mech} @{ca('auth_mech_avail')}) { |
---|
| 9733 | $self->smtp_resp(0,"504 5.7.6 Error: requested authentication mechanism not supported", 1, $cmd); |
---|
| 9734 | } else { |
---|
| 9735 | my($state,$result,$challenge); |
---|
| 9736 | if ($auth_resp eq '=') { $auth_resp = '' } # zero length |
---|
| 9737 | elsif ($auth_resp eq '') { $auth_resp = undef } |
---|
| 9738 | for (;;) { |
---|
| 9739 | if ($auth_resp !~ m{^[A-Za-z0-9+/=]*\z}) { |
---|
| 9740 | $self->smtp_resp(0,"501 5.5.4 Authentication failed: malformed authentication response", 1, $cmd); |
---|
| 9741 | last; |
---|
| 9742 | } else { |
---|
| 9743 | $auth_resp = decode_base64($auth_resp) if $auth_resp ne ''; |
---|
| 9744 | ($state,$result,$challenge) = |
---|
| 9745 | authenticate($state, $auth_mech, $auth_resp); |
---|
| 9746 | if (ref($result) eq 'ARRAY') { |
---|
| 9747 | $self->smtp_resp(0,"235 2.7.1 Authentication successful"); |
---|
| 9748 | $authenticated = 1; ($auth_user,$auth_pass) = @$result; |
---|
| 9749 | do_log(2,"AUTH $auth_mech, user=$auth_user"); |
---|
| 9750 | # do_log(2,"AUTH $auth_mech, user=$auth_user, pass=$auth_resp"); |
---|
| 9751 | last; |
---|
| 9752 | } elsif (defined $result && !$result) { |
---|
| 9753 | $self->smtp_resp(0,"535 5.7.1 Authentication failed", 1, $cmd); |
---|
| 9754 | last; |
---|
| 9755 | } |
---|
| 9756 | } |
---|
| 9757 | # server challenge or ready prompt |
---|
| 9758 | $self->smtp_resp(1,"334 ".encode_base64($challenge,'')); |
---|
| 9759 | $auth_resp = <$sock>; |
---|
| 9760 | do_log(5, $self->{proto} . "< $auth_resp"); |
---|
| 9761 | $auth_resp =~ s/\015?\012\z//; |
---|
| 9762 | if ($auth_resp eq '*') { |
---|
| 9763 | $self->smtp_resp(0,"501 5.7.1 Authentication aborted"); |
---|
| 9764 | last; |
---|
| 9765 | } |
---|
| 9766 | } |
---|
| 9767 | } |
---|
| 9768 | last; |
---|
| 9769 | }; |
---|
| 9770 | /^VRFY\z/ && do { |
---|
| 9771 | $self->smtp_resp(1,"502 5.5.1 Command $_ not implemented", 1, $cmd); |
---|
| 9772 | # if ($args eq '') { |
---|
| 9773 | # $self->smtp_resp(1,"501 5.5.2 Syntax: VRFY address", 1, $cmd); |
---|
| 9774 | # } else { |
---|
| 9775 | # $self->smtp_resp(1,"252 2.0.0 Cannot VRFY user, but will accept ". |
---|
| 9776 | # "message and attempt delivery", 0, $cmd); |
---|
| 9777 | # } |
---|
| 9778 | last; |
---|
| 9779 | }; |
---|
| 9780 | /^MAIL\z/ && do { # begin new transaction |
---|
| 9781 | if (defined($sender)) { |
---|
| 9782 | $self->smtp_resp(0,"503 5.5.1 Error: nested MAIL command", 1, $cmd); |
---|
| 9783 | last; |
---|
| 9784 | } |
---|
| 9785 | if (!$authenticated && |
---|
| 9786 | c('auth_required_inp') && @{ca('auth_mech_avail')} ) { |
---|
| 9787 | $self->smtp_resp(0,"530 5.7.1 Authentication required", 1, $cmd); |
---|
| 9788 | last; |
---|
| 9789 | } |
---|
| 9790 | # begin SMTP transaction |
---|
| 9791 | my($now) = time; |
---|
| 9792 | prolong_timer('MAIL FROM received - timer reset', $child_timeout); |
---|
| 9793 | if (!$seq) { # the first connect |
---|
| 9794 | section_time('SMTP pre-MAIL'); |
---|
| 9795 | } else { # establish new time reference for each transaction |
---|
| 9796 | Amavis::Timing::init(); snmp_counters_init(); |
---|
| 9797 | } |
---|
| 9798 | $seq++; |
---|
| 9799 | new_am_id(undef,$Amavis::child_invocation_count,$seq) |
---|
| 9800 | if !$initial_am_id; |
---|
| 9801 | $initial_am_id = 0; $self->prepare_tempdir; |
---|
| 9802 | my($cl_ip) = $xforward_args{'ADDR'}; |
---|
| 9803 | if ($cl_ip ne '' && defined $policy_bank{'MYNETS'} |
---|
| 9804 | && lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) { |
---|
| 9805 | Amavis::load_policy_bank('MYNETS'); $policy_changed = 1; |
---|
| 9806 | } |
---|
| 9807 | $msginfo = Amavis::In::Message->new; |
---|
| 9808 | $msginfo->rx_time($now); |
---|
| 9809 | $msginfo->delivery_method(c('forward_method')); |
---|
| 9810 | my($submitter); |
---|
| 9811 | if ($authenticated) { |
---|
| 9812 | $msginfo->auth_user($auth_user); $msginfo->auth_pass($auth_pass); |
---|
| 9813 | $conn->smtp_proto($self->{proto}.'A') # rfc3848 |
---|
| 9814 | if $self->{proto} =~ /^(LMTP|ESMTP)\z/i; |
---|
| 9815 | } elsif (c('auth_reauthenticate_forwarded') && |
---|
| 9816 | c('amavis_auth_user') ne '') { |
---|
| 9817 | $msginfo->auth_user(c('amavis_auth_user')); |
---|
| 9818 | $msginfo->auth_pass(c('amavis_auth_pass')); |
---|
| 9819 | $submitter = qquote_rfc2821_local(c('mailfrom_notify_recip')); |
---|
| 9820 | } |
---|
| 9821 | $msginfo->client_addr($xforward_args{'ADDR'}); |
---|
| 9822 | $msginfo->client_name($xforward_args{'NAME'}); |
---|
| 9823 | $msginfo->client_proto($xforward_args{'PROTO'}); |
---|
| 9824 | $msginfo->client_helo($xforward_args{'HELO'}); |
---|
| 9825 | %xforward_args = (); # reset values for the next transation |
---|
| 9826 | # permit some sloppy syntax without angle brackets |
---|
| 9827 | if ($args !~ /^FROM: \s* |
---|
| 9828 | ( < (?: " (?: \\. | [^\\"] )* " | [^"@] )* |
---|
| 9829 | (?: @ (?: \[ (?: \\. | [^\]\\] )* \] | |
---|
| 9830 | [^\[\]\\>] )* )? |
---|
| 9831 | > | |
---|
| 9832 | [^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )* |
---|
| 9833 | ) (?: \s+ ([\040-\176]+) )? \z/isx ) { |
---|
| 9834 | $self->smtp_resp(0,"501 5.5.2 Syntax: MAIL FROM: <address>",1,$cmd); |
---|
| 9835 | last; |
---|
| 9836 | } |
---|
| 9837 | my($bad); my($addr,$opt) = ($1,$2); |
---|
| 9838 | for (split(' ',$opt)) { |
---|
| 9839 | if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* ) = |
---|
| 9840 | ( [\041-\074\076-\176]+ ) \z/xs) { # printable, not '=' or SP |
---|
| 9841 | $self->smtp_resp(0,"501 5.5.4 Syntax error in MAIL FROM parameters", |
---|
| 9842 | 1,$cmd); |
---|
| 9843 | $bad = 1; last; |
---|
| 9844 | } else { |
---|
| 9845 | my($name,$val) = (uc($1),$2); |
---|
| 9846 | if ($name eq 'SIZE' && $val=~/^\d{1,20}\z/) { # rfc1870 |
---|
| 9847 | $msginfo->msg_size($val+0); |
---|
| 9848 | if ($message_size_limit && $val > $message_size_limit) { |
---|
| 9849 | my($msg) = "552 5.3.4 Declared message size ($val B) exceeds ". |
---|
| 9850 | "fixed maximium message size of $message_size_limit B"; |
---|
| 9851 | do_log(0, $self->{proto}." REJECT 'MAIL FROM': $msg"); |
---|
| 9852 | $self->smtp_resp(0,$msg, 0,$cmd); |
---|
| 9853 | $bad = 1; last; |
---|
| 9854 | } |
---|
| 9855 | } elsif ($name eq 'BODY' && $val=~/^7BIT|8BITMIME\z/i){ |
---|
| 9856 | $msginfo->body_type(uc($val)); |
---|
| 9857 | } elsif ($name eq 'AUTH' && @{ca('auth_mech_avail')} && |
---|
| 9858 | !defined($submitter) ) { # rfc2554 |
---|
| 9859 | $submitter = $val; # encoded as xtext: rfc3461 |
---|
| 9860 | $submitter =~ s/\+([0-9a-fA-F]{2})/pack("C",hex($1))/eg; |
---|
| 9861 | do_log(5, "MAIL command, $authenticated, submitter: $submitter"); |
---|
| 9862 | } else { |
---|
| 9863 | my($msg); |
---|
| 9864 | if ($name eq 'AUTH' && !@{ca('auth_mech_avail')}) { |
---|
| 9865 | $msg = "503 5.7.4 Error: authentication disabled"; |
---|
| 9866 | } else { |
---|
| 9867 | $msg = "504 5.5.4 MAIL command parameter error: $name=$val"; |
---|
| 9868 | } |
---|
| 9869 | $self->smtp_resp(0,$msg,1,$cmd); |
---|
| 9870 | $bad = 1; last; |
---|
| 9871 | } |
---|
| 9872 | } |
---|
| 9873 | } |
---|
| 9874 | if (!$bad) { |
---|
| 9875 | $addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr; |
---|
| 9876 | $self->smtp_resp(0,"250 2.1.0 Sender $addr OK"); |
---|
| 9877 | $sender = unquote_rfc2821_local($addr); |
---|
| 9878 | debug_oneshot(lookup(0,$sender,@{ca('debug_sender_maps')}) ? 1 : 0, |
---|
| 9879 | $self->{proto} . "< $cmd"); |
---|
| 9880 | # $submitter = "<$addr>" if !defined($submitter); # rfc2554: MAY |
---|
| 9881 | $submitter = '<>' if !defined($msginfo->auth_user); |
---|
| 9882 | $msginfo->auth_submitter($submitter); |
---|
| 9883 | }; |
---|
| 9884 | last; |
---|
| 9885 | }; |
---|
| 9886 | /^RCPT\z/ && do { |
---|
| 9887 | if (!defined($sender)) { |
---|
| 9888 | $self->smtp_resp(0,"503 5.5.1 Need MAIL command before RCPT",1,$cmd); |
---|
| 9889 | @recips = (); $got_rcpt = 0; |
---|
| 9890 | last; |
---|
| 9891 | } |
---|
| 9892 | $got_rcpt++; |
---|
| 9893 | # permit some sloppy syntax without angle brackets |
---|
| 9894 | if ($args !~ /^TO: \s* |
---|
| 9895 | ( < (?: " (?: \\. | [^\\"] )* " | [^"@] )* |
---|
| 9896 | (?: @ (?: \[ (?: \\. | [^\]\\] )* \] | |
---|
| 9897 | [^\[\]\\>] )* )? |
---|
| 9898 | > | |
---|
| 9899 | [^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )* |
---|
| 9900 | ) (?: \s+ ([\040-\176]+) )? \z/isx ) { |
---|
| 9901 | $self->smtp_resp(0,"501 5.5.2 Syntax: RCPT TO: <address>",1,$cmd); |
---|
| 9902 | last; |
---|
| 9903 | } |
---|
| 9904 | if ($2 ne '') { |
---|
| 9905 | $self->smtp_resp(0,"504 5.5.4 RCPT command parameter not implemented: $2", |
---|
| 9906 | 1, $cmd); |
---|
| 9907 | ### $self->smtp_resp(0,"555 5.5.4 RCPT command parameter unrecognized: $2", 1, $cmd); |
---|
| 9908 | } elsif ($got_rcpt > $smtpd_recipient_limit) { |
---|
| 9909 | $self->smtp_resp(0,"452 4.5.3 Too many recipients"); |
---|
| 9910 | } else { |
---|
| 9911 | my($addr,$opt) = ($1, $2); |
---|
| 9912 | $addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr; |
---|
| 9913 | my($addr_unq) = unquote_rfc2821_local($addr); |
---|
| 9914 | my($recip_size_limit); my($mslm) = ca('message_size_limit_maps'); |
---|
| 9915 | $recip_size_limit = lookup(0,$addr_unq, @$mslm) if @$mslm; |
---|
| 9916 | if ($recip_size_limit && $recip_size_limit < 65536) |
---|
| 9917 | { $recip_size_limit = 65536 } # rfc2821 requires at least 64k |
---|
| 9918 | if ($recip_size_limit > $max_recip_size_limit) |
---|
| 9919 | { $max_recip_size_limit = $recip_size_limit } |
---|
| 9920 | my($mail_size) = $msginfo->msg_size; |
---|
| 9921 | if (defined $mail_size && $recip_size_limit && $mail_size > $recip_size_limit) { |
---|
| 9922 | my($msg) = "552 5.3.4 Declared message size ($mail_size B) ". |
---|
| 9923 | "exceeds fixed maximium message size of $recip_size_limit B, ". |
---|
| 9924 | "recipient $addr"; |
---|
| 9925 | do_log(0, $self->{proto}." REJECT 'RCPT TO': $msg"); |
---|
| 9926 | $self->smtp_resp(0,$msg, 0,$cmd); |
---|
| 9927 | } else { |
---|
| 9928 | push(@recips,$addr_unq); |
---|
| 9929 | $self->smtp_resp(0,"250 2.1.5 Recipient $addr OK"); |
---|
| 9930 | } |
---|
| 9931 | }; |
---|
| 9932 | last; |
---|
| 9933 | }; |
---|
| 9934 | /^DATA\z/ && !@recips && do { |
---|
| 9935 | if (!defined($sender)) { |
---|
| 9936 | $self->smtp_resp(1,"503 5.5.1 Need MAIL command before DATA",1,$cmd); |
---|
| 9937 | } elsif (!$got_rcpt) { |
---|
| 9938 | $self->smtp_resp(1,"503 5.5.1 Need RCPT command before DATA",1,$cmd); |
---|
| 9939 | } elsif ($lmtp) { # rfc2033 requires 503 code! |
---|
| 9940 | $self->smtp_resp(1,"503 5.1.1 Error (DATA): no valid recipients",0,$cmd); |
---|
| 9941 | } else { |
---|
| 9942 | $self->smtp_resp(1,"554 5.1.1 Error (DATA): no valid recipients",0,$cmd); |
---|
| 9943 | } |
---|
| 9944 | last; |
---|
| 9945 | }; |
---|
| 9946 | /^DATA\z/ && do { |
---|
| 9947 | # set timer to the initial value, MTA timer starts here |
---|
| 9948 | prolong_timer('DATA received - timer reset', $child_timeout); |
---|
| 9949 | if ($message_size_limit) { # enforce system-wide size limit |
---|
| 9950 | if (!$max_recip_size_limit || |
---|
| 9951 | $max_recip_size_limit > $message_size_limit) { |
---|
| 9952 | $max_recip_size_limit = $message_size_limit; |
---|
| 9953 | } |
---|
| 9954 | } |
---|
| 9955 | my($within_data_transfer,$complete); |
---|
| 9956 | my($size) = 0; my($over_size) = 0; |
---|
| 9957 | eval { |
---|
| 9958 | $msginfo->sender($sender); $msginfo->recips(\@recips); |
---|
| 9959 | ll(1) && do_log(1, sprintf("%s:%s:%s %s: <%s> -> %s Received: %s", |
---|
| 9960 | $conn->smtp_proto, |
---|
| 9961 | $conn->socket_ip eq $inet_socket_bind ? '' |
---|
| 9962 | : '['.$conn->socket_ip.']', |
---|
| 9963 | $conn->socket_port, $self->{tempdir_pers}, |
---|
| 9964 | $sender, join(',', qquote_rfc2821_local(@recips)), |
---|
| 9965 | join(' ', ($msginfo->msg_size eq '' ? () |
---|
| 9966 | : 'SIZE='.$msginfo->msg_size), |
---|
| 9967 | ($msginfo->body_type eq '' ? () |
---|
| 9968 | : 'BODY='.$msginfo->body_type), |
---|
| 9969 | received_line($conn,$msginfo,am_id(),0) ) |
---|
| 9970 | ) ); |
---|
| 9971 | $self->smtp_resp(1,"354 End data with <CR><LF>.<CR><LF>"); |
---|
| 9972 | $within_data_transfer = 1; |
---|
| 9973 | section_time('SMTP pre-DATA-flush') if $self->{pipelining}; |
---|
| 9974 | $self->{tempdir_empty} = 0; |
---|
| 9975 | do { local($/) = "\015\012"; # set input line terminator to CRLF |
---|
| 9976 | while(<$sock>) { # use native I/O for speed |
---|
| 9977 | # do_log(5, $self->{proto} . "< $_"); |
---|
| 9978 | if (/^\./) { |
---|
| 9979 | if ($_ eq ".\015\012") |
---|
| 9980 | { $complete = 1; $within_data_transfer = 0; last } |
---|
| 9981 | s/^\.(.+\015\012)\z/$1/s; # dot de-stuffing, rfc2821 |
---|
| 9982 | } |
---|
| 9983 | $size += length($_); # message size is defined in rfc1870 |
---|
| 9984 | if (!$over_size) { |
---|
| 9985 | chomp; # remove \015\012 (the $/), faster than s/// |
---|
| 9986 | print {$self->{fh_pers}} $_,$eol |
---|
| 9987 | or die "Can't write to mail file: $!"; |
---|
| 9988 | if ($max_recip_size_limit && $size > $max_recip_size_limit) { |
---|
| 9989 | do_log(1,"Message size exceeded $max_recip_size_limit B, ". |
---|
| 9990 | "skiping further input"); |
---|
| 9991 | print {$self->{fh_pers}} $eol,"***TRUNCATED***",$eol |
---|
| 9992 | or die "Can't write to mail file: $!"; |
---|
| 9993 | $over_size = 1; |
---|
| 9994 | } |
---|
| 9995 | } |
---|
| 9996 | } |
---|
| 9997 | $eof = 1 if !$complete; |
---|
| 9998 | }; # restores line terminator |
---|
| 9999 | # normal data termination, or eof on socket, or fatal error |
---|
| 10000 | do_log(4, $self->{proto} . "< .\015\012") if $complete; |
---|
| 10001 | $self->{fh_pers}->flush or die "Can't flush mail file: $!"; |
---|
| 10002 | # On some systems you have to do a seek whenever you |
---|
| 10003 | # switch between reading and writing. Amongst other things, |
---|
| 10004 | # this may have the effect of calling stdio's clearerr(3). |
---|
| 10005 | $self->{fh_pers}->seek(0,1) or die "Can't seek on file: $!"; |
---|
| 10006 | section_time('SMTP DATA'); |
---|
| 10007 | }; |
---|
| 10008 | if ($@ ne '' || !$complete || $over_size) { # err or connection broken |
---|
| 10009 | chomp($@); |
---|
| 10010 | # on error, either send: '421 Shutting down', |
---|
| 10011 | # or: '451 Aborted, error in processing' and NOT shut down! |
---|
| 10012 | if ($over_size && $@ eq '' && !$within_data_transfer) { |
---|
| 10013 | my($msg) = "552 5.3.4 Message size ($size B) exceeds ". |
---|
| 10014 | "fixed maximium message size of $max_recip_size_limit B"; |
---|
| 10015 | do_log(0, $self->{proto}." REJECT: $msg"); |
---|
| 10016 | $self->smtp_resp(0,$msg, 0,$cmd); |
---|
| 10017 | } elsif (!$within_data_transfer) { |
---|
| 10018 | my($msg) = "Error in processing: " . |
---|
| 10019 | !$complete && $@ eq '' ? 'incomplete' : $@; |
---|
| 10020 | do_log(-2, $self->{proto}." TROUBLE: 451 4.5.0 $msg"); |
---|
| 10021 | $self->smtp_resp(1, "451 4.5.0 $msg"); |
---|
| 10022 | ### $aborting = $msg; |
---|
| 10023 | } else { |
---|
| 10024 | $aborting = "client broke the connection ". |
---|
| 10025 | "during data transfer" if $eof; |
---|
| 10026 | $aborting .= ', ' if $aborting ne '' && $@ ne ''; |
---|
| 10027 | $aborting .= $@; |
---|
| 10028 | $aborting = '???' if $aborting eq ''; |
---|
| 10029 | do_log($@ ne '' ? -1 : 3, |
---|
| 10030 | $self->{proto}." TROUBLE, ABORTING: $aborting"); |
---|
| 10031 | } |
---|
| 10032 | } else { # all OK |
---|
| 10033 | # |
---|
| 10034 | # Is it acceptable to do all this processing here, |
---|
| 10035 | # before returning response??? According to rfc1047 |
---|
| 10036 | # it is not a good idea! But at the moment we do not have |
---|
| 10037 | # much choice, amavis has no queueing mechanism and can not |
---|
| 10038 | # accept responsibility for delivery. |
---|
| 10039 | # |
---|
| 10040 | # check contents before responding |
---|
| 10041 | # check_mail() expects open file on $self->{fh_pers}, |
---|
| 10042 | # need not be rewound |
---|
| 10043 | $msginfo->mail_tempdir($self->{tempdir_pers}); |
---|
| 10044 | $msginfo->mail_text_fn($self->{tempdir_pers} . '/email.txt'); |
---|
| 10045 | $msginfo->mail_text($self->{fh_pers}); |
---|
| 10046 | my($declared_size) = $msginfo->msg_size; |
---|
| 10047 | if (!defined($declared_size)) { |
---|
| 10048 | $msginfo->msg_size($size); |
---|
| 10049 | } elsif ($size > $declared_size) { # shouldn't happen with decent MTA |
---|
| 10050 | do_log(2,"Actual message size $size B greater than the ". |
---|
| 10051 | "declared $declared_size B"); |
---|
| 10052 | $msginfo->msg_size($size); |
---|
| 10053 | } elsif ($size < $declared_size) { # not unusual |
---|
| 10054 | do_log(4,"Actual message size $size B, declared $declared_size B"); |
---|
| 10055 | $msginfo->msg_size($size); |
---|
| 10056 | } |
---|
| 10057 | my($smtp_resp, $exit_code, $preserve_evidence) = |
---|
| 10058 | &$check_mail($conn,$msginfo, $lmtp,$self->{tempdir_pers}); |
---|
| 10059 | if ($preserve_evidence) { $self->preserve_evidence(1) } |
---|
| 10060 | if ($smtp_resp !~ /^4/ && |
---|
| 10061 | grep { !$_->recip_done } @{$msginfo->per_recip_data}) { |
---|
| 10062 | die "TROUBLE: (MISCONFIG) not all recipients done, " . |
---|
| 10063 | "forward_method is: " . $msginfo->delivery_method; |
---|
| 10064 | } |
---|
| 10065 | if (!$lmtp) { |
---|
| 10066 | do_log(4, "sending SMTP response: \"$smtp_resp\""); |
---|
| 10067 | $self->smtp_resp(0, $smtp_resp); |
---|
| 10068 | } else { |
---|
| 10069 | my($bounced) = $msginfo->dsn_sent; |
---|
| 10070 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 10071 | my($resp) = $r->recip_smtp_response; |
---|
| 10072 | if ($bounced && $smtp_resp=~/^2/ && $resp!~/^2/) { |
---|
| 10073 | # as the message was already bounced by us, |
---|
| 10074 | # MTA must not bounce it again; failure status |
---|
| 10075 | # needs to be converted into success! |
---|
| 10076 | $resp = sprintf("250 2.5.0 Ok, DSN %s (%s)", |
---|
| 10077 | $bounced==1 ? 'sent' : 'muted', $resp); |
---|
| 10078 | } |
---|
| 10079 | do_log(4, sprintf("sending LMTP response for <%s>: \"%s\"", |
---|
| 10080 | $r->recip_addr, $resp)); |
---|
| 10081 | $self->smtp_resp(0, $resp); |
---|
| 10082 | } |
---|
| 10083 | } |
---|
| 10084 | }; |
---|
| 10085 | alarm(0); do_log(5,"timer stopped after DATA end"); |
---|
| 10086 | if ($self->preserve_evidence && !$self->{tempdir_empty}) { |
---|
| 10087 | # keep evidence in case of trouble |
---|
| 10088 | do_log(-1,"PRESERVING EVIDENCE in ".$self->{tempdir_pers}); |
---|
| 10089 | $self->{fh_pers}->close or die "Can't close mail file: $!"; |
---|
| 10090 | $self->{fh_pers} = undef; $self->{tempdir_pers} = undef; |
---|
| 10091 | $self->{tempdir_empty} = 1; |
---|
| 10092 | } |
---|
| 10093 | # cleanup, but leave directory (and file handle if possible) for reuse |
---|
| 10094 | if ($self->{fh_pers} && !$can_truncate) { |
---|
| 10095 | # truncate is not standard across all Unix variants, |
---|
| 10096 | # it is not Posix, but is XPG4-UNIX. |
---|
| 10097 | # So if we can't truncate a file and leave it open, |
---|
| 10098 | # we have to create it anew later, at some cost. |
---|
| 10099 | # |
---|
| 10100 | $self->{fh_pers}->close or die "Can't close mail file: $!"; |
---|
| 10101 | $self->{fh_pers} = undef; |
---|
| 10102 | unlink($self->{tempdir_pers}.'/email.txt') |
---|
| 10103 | or die "Can't delete file ".$self->{tempdir_pers}."/email.txt: $!"; |
---|
| 10104 | section_time('delete email.txt'); |
---|
| 10105 | } |
---|
| 10106 | if (defined $self->{tempdir_pers}) { # prepare for the next one |
---|
| 10107 | strip_tempdir($self->{tempdir_pers}); $self->{tempdir_empty} = 1; |
---|
| 10108 | } |
---|
| 10109 | $sender = undef; @recips = (); $got_rcpt = 0; # implicit RSET |
---|
| 10110 | $max_recip_size_limit = undef; $msginfo = undef; # forget previous |
---|
| 10111 | if ($policy_changed) |
---|
| 10112 | { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 } |
---|
| 10113 | $self->preserve_evidence(0); # reset |
---|
| 10114 | # report elapsed times by section for each transaction |
---|
| 10115 | # (the time for the QUIT remains unaccounted for) |
---|
| 10116 | do_log(2, Amavis::Timing::report()); |
---|
| 10117 | Amavis::Timing::init(); snmp_counters_init(); |
---|
| 10118 | last; |
---|
| 10119 | }; # DATA |
---|
| 10120 | # catchall (EXPN, TURN, unknown): |
---|
| 10121 | $self->smtp_resp(1,"502 5.5.1 Error: command ($_) not implemented",1,$cmd); |
---|
| 10122 | # $self->smtp_resp(1,"500 5.5.2 Error: command ($_) not recognized", 1,$cmd); |
---|
| 10123 | }; # end of 'switch' block |
---|
| 10124 | if ($terminating || defined $aborting) { # exit SMTP-session loop |
---|
| 10125 | $voluntary_exit = 1; last; |
---|
| 10126 | } |
---|
| 10127 | # rfc2920 requires a flush whenever the local TCP input buffer is |
---|
| 10128 | # emptied. Since we can't check it (unless we use sysread & select), |
---|
| 10129 | # we should do a flush here to be in compliance. We could only break |
---|
| 10130 | # the requirement if we _knew_ we talk with a local MTA client which |
---|
| 10131 | # uses client-side pipelining. |
---|
| 10132 | $self->smtp_resp_flush; |
---|
| 10133 | $0 = sprintf("amavisd (ch%d-%s-idle)", |
---|
| 10134 | $Amavis::child_invocation_count, am_id()); |
---|
| 10135 | Amavis::Timing::go_idle(6); |
---|
| 10136 | undef $!; |
---|
| 10137 | } # end of while |
---|
| 10138 | my($errn,$errs); |
---|
| 10139 | if (!$voluntary_exit) { |
---|
| 10140 | $eof = 1; |
---|
| 10141 | if (!defined($_)) { $errn = 0+$!; $errs = "$!" } |
---|
| 10142 | } |
---|
| 10143 | $0 = sprintf("amavisd (ch%d)", $Amavis::child_invocation_count); |
---|
| 10144 | Amavis::Timing::go_busy(7); |
---|
| 10145 | # come here when: QUIT is received, eof or err on socket, or we need to abort |
---|
| 10146 | $self->smtp_resp_flush; # just in case, the session might have been disconnected |
---|
| 10147 | my($msg) = |
---|
| 10148 | defined $aborting && !$eof ? "ABORTING the session: $aborting" : |
---|
| 10149 | defined $aborting ? $aborting : |
---|
| 10150 | !$terminating ? "client broke the connection without a QUIT ($errs)" : ''; |
---|
| 10151 | do_log($aborting?-1:3, $self->{proto}.': NOTICE: '.$msg) if $msg ne ''; |
---|
| 10152 | if (defined $aborting && !$eof) |
---|
| 10153 | { $self->smtp_resp(1,"421 4.3.2 Service shutting down, ".$aborting) } |
---|
| 10154 | $self->{session_closed_normally} = 1; |
---|
| 10155 | # closes connection after child_finish_hook |
---|
| 10156 | } |
---|
| 10157 | |
---|
| 10158 | # sends a SMTP response consisting of 3-digit code and an optional message; |
---|
| 10159 | # slow down evil clients by delaying response on permanent errors |
---|
| 10160 | sub smtp_resp($$$;$$) { |
---|
| 10161 | my($self, $flush,$resp, $penalize,$line) = @_; |
---|
| 10162 | if ($penalize) { |
---|
| 10163 | do_log(-1, $self->{proto} . ": $resp; PENALIZE: $line"); |
---|
| 10164 | sleep 5; |
---|
| 10165 | section_time('SMTP penalty wait'); |
---|
| 10166 | } |
---|
| 10167 | $resp = sanitize_str($resp,1); |
---|
| 10168 | local($1,$2,$3,$4); |
---|
| 10169 | if ($resp !~ /^ ([1-5]\d\d) (\ |-|\z) |
---|
| 10170 | ([245] \. \d{1,3} \. \d{1,3} (?: \ |\z) )? |
---|
| 10171 | (.*) \z/xs) |
---|
| 10172 | { die "Internal error(2): bad SMTP response code: '$resp'" } |
---|
| 10173 | my($resp_code,$continuation,$enhanced,$tail) = ($1,$2,$3, $4); |
---|
| 10174 | my($lead_len) = length($resp_code) + 1 + length($enhanced); |
---|
| 10175 | while (length($tail) > 512-2-$lead_len || $tail =~ /\n/) { |
---|
| 10176 | # rfc2821: The maximum total length of a reply line including the |
---|
| 10177 | # reply code and the <CRLF> is 512 characters. More information |
---|
| 10178 | # may be conveyed through multiple-line replies. |
---|
| 10179 | my($head) = substr($tail,0,512-2-$lead_len); |
---|
| 10180 | if ($head =~ /^([^\n]*\n)/) { $head = $1 } |
---|
| 10181 | $tail = substr($tail,length($head)); chomp($head); |
---|
| 10182 | push(@{$self->{smtp_outbuf}}, $resp_code.'-'.$enhanced.$head); |
---|
| 10183 | } |
---|
| 10184 | push(@{$self->{smtp_outbuf}},$resp_code.$continuation.$enhanced.$tail); |
---|
| 10185 | $self->smtp_resp_flush if $flush || !$self->{pipelining} || |
---|
| 10186 | @{$self->{smtp_outbuf}} > 200; |
---|
| 10187 | } |
---|
| 10188 | |
---|
| 10189 | sub smtp_resp_flush($) { |
---|
| 10190 | my($self) = shift; |
---|
| 10191 | if (@{$self->{smtp_outbuf}}) { |
---|
| 10192 | for my $resp (@{$self->{smtp_outbuf}}) { |
---|
| 10193 | do_log(4, $self->{proto} . "> $resp"); |
---|
| 10194 | }; |
---|
| 10195 | my($stat) = |
---|
| 10196 | $self->{sock}->print(map { $_."\015\012" } @{$self->{smtp_outbuf}} ); |
---|
| 10197 | @{$self->{smtp_outbuf}} = (); # prevent printing again even if error |
---|
| 10198 | $stat or die "Error writing a SMTP response to the socket: $!"; |
---|
| 10199 | } |
---|
| 10200 | } |
---|
| 10201 | |
---|
| 10202 | 1; |
---|
| 10203 | |
---|
| 10204 | __DATA__ |
---|
| 10205 | # |
---|
| 10206 | package Amavis::In::QMQPqq; |
---|
| 10207 | use strict; |
---|
| 10208 | # use re 'taint'; # (is this module ready for this yet?) |
---|
| 10209 | |
---|
| 10210 | BEGIN { |
---|
| 10211 | use Exporter (); |
---|
| 10212 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 10213 | $VERSION = '1.17'; |
---|
| 10214 | @ISA = qw(Exporter); |
---|
| 10215 | } |
---|
| 10216 | use POSIX qw(strftime); |
---|
| 10217 | use Errno qw(ENOENT); |
---|
| 10218 | |
---|
| 10219 | BEGIN { |
---|
| 10220 | import Amavis::Conf qw(:platform :confvars :dynamic_confvars c cr ca); |
---|
| 10221 | import Amavis::Util qw(do_log am_id prolong_timer debug_oneshot |
---|
| 10222 | untaint sanitize_str strip_tempdir rmdir_recursively); |
---|
| 10223 | import Amavis::Lookup qw(lookup); |
---|
| 10224 | import Amavis::Timing qw(section_time); |
---|
| 10225 | import Amavis::rfc2821_2822_Tools; |
---|
| 10226 | import Amavis::In::Message; |
---|
| 10227 | import Amavis::In::Connection; |
---|
| 10228 | } |
---|
| 10229 | |
---|
| 10230 | sub new($) { |
---|
| 10231 | my($class) = @_; |
---|
| 10232 | my($self) = bless {}, $class; |
---|
| 10233 | $self->{fh_pers} = undef; # persistent file handle for email.txt |
---|
| 10234 | $self->{tempdir_pers} = undef; # temporary directory for check_mail |
---|
| 10235 | $self->{preserve} = undef; # don't delete tempdir on exit |
---|
| 10236 | $self->{tempdir_empty} = 1; # anything of interest in tempdir? |
---|
| 10237 | $self->{bytesleft} = undef; # bytes left for whole package |
---|
| 10238 | $self->{len} = undef; # set by getlen() method |
---|
| 10239 | $self->{sock} = undef; # connected socket |
---|
| 10240 | $self->{proto} = undef; # protocol |
---|
| 10241 | $self->{session_closed_normally} = undef; # closed properly? (waited for K/Z/D) |
---|
| 10242 | $self; |
---|
| 10243 | } |
---|
| 10244 | |
---|
| 10245 | sub preserve_evidence # try to preserve temporary files etc in case of trouble |
---|
| 10246 | { my($self)=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift) } |
---|
| 10247 | |
---|
| 10248 | sub DESTROY { |
---|
| 10249 | my($self) = shift; |
---|
| 10250 | # do_log(0, "Amavis::In::QMQPqq::DESTROY called"); |
---|
| 10251 | $self->{fh_pers}->close |
---|
| 10252 | or die "Can't close temp file: $!" if $self->{fh_pers}; |
---|
| 10253 | my($errn) = $self->{tempdir_pers} eq '' ? ENOENT |
---|
| 10254 | : (stat($self->{tempdir_pers}) ? 0 : 0+$!); |
---|
| 10255 | if (defined $self->{tempdir_pers} && $errn != ENOENT) { |
---|
| 10256 | # this will not be included in the TIMING report, |
---|
| 10257 | # but it only occurs infrequently and doesn't take that long |
---|
| 10258 | if ($self->preserve_evidence && !$self->{tempdir_empty}) { |
---|
| 10259 | do_log(0, "tempdir is to be PRESERVED: ".$self->{tempdir_pers}); |
---|
| 10260 | } else { |
---|
| 10261 | do_log(2, "tempdir being removed: ".$self->{tempdir_pers}); |
---|
| 10262 | rmdir_recursively($self->{tempdir_pers}); |
---|
| 10263 | } |
---|
| 10264 | } |
---|
| 10265 | if (! $self->{session_closed_normally}) { |
---|
| 10266 | $self->qmqpqq_resp("Z","Service shutting down, closing channel"); |
---|
| 10267 | } |
---|
| 10268 | } |
---|
| 10269 | |
---|
| 10270 | sub prepare_tempdir($) { |
---|
| 10271 | my($self) = @_; |
---|
| 10272 | if (! defined $self->{tempdir_pers} ) { |
---|
| 10273 | # invent a name for a temporary directory for this child, and create it |
---|
| 10274 | my($now_iso8601) = strftime("%Y%m%dT%H%M%S", localtime); |
---|
| 10275 | $self->{tempdir_pers} = sprintf("%s/amavis-%s-%05d", |
---|
| 10276 | $TEMPBASE, $now_iso8601, $$); |
---|
| 10277 | } |
---|
| 10278 | my($errn) = stat($self->{tempdir_pers}) ? 0 : 0+$!; |
---|
| 10279 | if ($errn == ENOENT || ! -d _) { |
---|
| 10280 | mkdir($self->{tempdir_pers}, 0750) |
---|
| 10281 | or die "Can't create directory $self->{tempdir_pers}: $!"; |
---|
| 10282 | $self->{tempdir_empty} = 1; |
---|
| 10283 | section_time('mkdir tempdir'); |
---|
| 10284 | } |
---|
| 10285 | # prepare temporary file for writing (and reading later) |
---|
| 10286 | my($fname) = $self->{tempdir_pers} . "/email.txt"; |
---|
| 10287 | my($errn) = stat($fname) ? 0 : 0+$!; |
---|
| 10288 | if ($self->{fh_pers} && !$errn && -f _) { |
---|
| 10289 | $self->{fh_pers}->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
| 10290 | $self->{fh_pers}->truncate(0) or die "Can't truncate mail file: $!"; |
---|
| 10291 | } else { |
---|
| 10292 | $self->{fh_pers} = IO::File->new($fname, 'w+', 0640) |
---|
| 10293 | or die "Can't create file $fname: $!"; |
---|
| 10294 | section_time('create email.txt'); |
---|
| 10295 | } |
---|
| 10296 | } |
---|
| 10297 | |
---|
| 10298 | |
---|
| 10299 | # get byte, die if no bytes left |
---|
| 10300 | sub getbyte($) { |
---|
| 10301 | my($self) = shift; |
---|
| 10302 | if(!$self->{bytesleft}--) { |
---|
| 10303 | die("No bytes left"); |
---|
| 10304 | } |
---|
| 10305 | if(defined($_ = $self->{sock}->getc)) { |
---|
| 10306 | return($_); |
---|
| 10307 | } |
---|
| 10308 | die("EOF on socket"); |
---|
| 10309 | } |
---|
| 10310 | |
---|
| 10311 | sub getlen($) { |
---|
| 10312 | my($self) = shift; |
---|
| 10313 | my($ch,$len); |
---|
| 10314 | |
---|
| 10315 | for(;;) { |
---|
| 10316 | $ch = $self->getbyte; |
---|
| 10317 | if($ch eq ':') { |
---|
| 10318 | return($self->{len} = $len); |
---|
| 10319 | } |
---|
| 10320 | if($ch !~ /^\d$/) { |
---|
| 10321 | die("Char '$ch' is not a number while determining length"); |
---|
| 10322 | } |
---|
| 10323 | $len .= $ch; |
---|
| 10324 | } |
---|
| 10325 | } |
---|
| 10326 | |
---|
| 10327 | sub getcomma($) { |
---|
| 10328 | my($self) = shift; |
---|
| 10329 | if($self->getbyte ne ',') { |
---|
| 10330 | die("Comma expected, found '$_'"); |
---|
| 10331 | } |
---|
| 10332 | } |
---|
| 10333 | |
---|
| 10334 | sub getnetstring($$) { |
---|
| 10335 | my($self) = shift; |
---|
| 10336 | ($self->{sock}->read($_[0],$self->getlen) == $self->{len}) || |
---|
| 10337 | die("EOF on socket"); |
---|
| 10338 | $self->{bytesleft} -= $self->{len}; |
---|
| 10339 | $self->getcomma; |
---|
| 10340 | } |
---|
| 10341 | |
---|
| 10342 | |
---|
| 10343 | # Accept a QMQPqq connect |
---|
| 10344 | # and call content checking for the message received |
---|
| 10345 | # |
---|
| 10346 | sub process_qmqpqq_request($$$$) { |
---|
| 10347 | my($self,$sock,$conn,$check_mail) = @_; |
---|
| 10348 | # $sock: connected socket from Net::Server |
---|
| 10349 | # $conn: information about client connection |
---|
| 10350 | # $check_mail: subroutine ref to be called with file handle |
---|
| 10351 | |
---|
| 10352 | $self->{proto} = "QMQPqq"; |
---|
| 10353 | $self->{sock} = $sock; # store $sock info for getbyte() method |
---|
| 10354 | $self->{bytesleft} = 20; # initial bytesleft value, there should |
---|
| 10355 | # NEVER EVER be longer email than 10^20 (approximately) |
---|
| 10356 | # bytes but increase if needed ;) |
---|
| 10357 | $self->{len} = undef; |
---|
| 10358 | |
---|
| 10359 | my($msginfo); |
---|
| 10360 | |
---|
| 10361 | my($sender,@recips); |
---|
| 10362 | |
---|
| 10363 | my($len); |
---|
| 10364 | |
---|
| 10365 | $conn->smtp_proto("QMQPqq"); # the name of the method is too specific |
---|
| 10366 | eval { |
---|
| 10367 | # get length of whole package |
---|
| 10368 | $self->{bytesleft} = $self->getlen; |
---|
| 10369 | |
---|
| 10370 | # get length of 'email' |
---|
| 10371 | $len = $self->getlen; |
---|
| 10372 | section_time('initial length determination'); |
---|
| 10373 | |
---|
| 10374 | am_id(sprintf("%05d-%02d",$$,$Amavis::child_invocation_count)); |
---|
| 10375 | |
---|
| 10376 | # prepare tempdir |
---|
| 10377 | $self->prepare_tempdir; |
---|
| 10378 | $msginfo = Amavis::In::Message->new; |
---|
| 10379 | $msginfo->rx_time(time); |
---|
| 10380 | $msginfo->delivery_method(c('forward_method')); |
---|
| 10381 | |
---|
| 10382 | # get 'email' |
---|
| 10383 | $self->{tempdir_empty} = 0; |
---|
| 10384 | my $size = 16384; |
---|
| 10385 | while(($len > 0) && ($sock->read($_,($len >= $size ? $size : $size = $len)) == $size)) { |
---|
| 10386 | (print {$self->{fh_pers}} $_) || |
---|
| 10387 | die("Can't write to mail file: $!"); |
---|
| 10388 | $len -= $size; |
---|
| 10389 | } |
---|
| 10390 | if($len > 0) { |
---|
| 10391 | die("EOF on socket"); |
---|
| 10392 | } |
---|
| 10393 | $self->{fh_pers}->flush || die("Can't flush mail file: $!"); |
---|
| 10394 | $self->{fh_pers}->seek(0,1) || die("Can't seek on file: $!"); |
---|
| 10395 | $self->{bytesleft} -= $self->{len}; |
---|
| 10396 | section_time('email receiving'); |
---|
| 10397 | # comma has to follow |
---|
| 10398 | $self->getcomma; |
---|
| 10399 | |
---|
| 10400 | # get sender |
---|
| 10401 | $self->getnetstring($sender); |
---|
| 10402 | section_time('sender receiving'); |
---|
| 10403 | |
---|
| 10404 | # get recips |
---|
| 10405 | my $i = 0; |
---|
| 10406 | while($self->{bytesleft}) { |
---|
| 10407 | $self->getnetstring($recips[$i++]); |
---|
| 10408 | } |
---|
| 10409 | section_time('recips receiving'); |
---|
| 10410 | |
---|
| 10411 | # final comma has to follow |
---|
| 10412 | $self->{bytesleft} = 1; |
---|
| 10413 | $self->getcomma; |
---|
| 10414 | |
---|
| 10415 | $msginfo->sender($sender); |
---|
| 10416 | $msginfo->recips(\@recips); |
---|
| 10417 | |
---|
| 10418 | do_log(1, sprintf("%s:%s:%s %s: <%s> -> %s Received: %s", |
---|
| 10419 | $self->{proto},$conn->socket_ip eq $inet_socket_bind ? |
---|
| 10420 | '' : '['.$conn->socket_ip.']', |
---|
| 10421 | $conn->socket_port, $self->{tempdir_pers}, |
---|
| 10422 | $sender, join(',', map{"<$_>"}@recips), |
---|
| 10423 | join(' ', |
---|
| 10424 | ($msginfo->msg_size eq '' ? () |
---|
| 10425 | : 'SIZE='.$msginfo->msg_size), |
---|
| 10426 | ($msginfo->body_type eq '' ? () |
---|
| 10427 | : 'BODY='.$msginfo->body_type), |
---|
| 10428 | received_line($conn,$msginfo,am_id(),0) ) |
---|
| 10429 | )); |
---|
| 10430 | |
---|
| 10431 | $msginfo->mail_tempdir($self->{tempdir_pers}); |
---|
| 10432 | $msginfo->mail_text_fn($self->{tempdir_pers} . '/email.txt'); |
---|
| 10433 | $msginfo->mail_text($self->{fh_pers}); |
---|
| 10434 | |
---|
| 10435 | my($smtp_resp,$exit_code,$preserve_evidence) = |
---|
| 10436 | &$check_mail($conn,$msginfo,0,$self->{tempdir_pers}); |
---|
| 10437 | |
---|
| 10438 | if ($preserve_evidence) { |
---|
| 10439 | $self->preserve_evidence(1); |
---|
| 10440 | } |
---|
| 10441 | if ($smtp_resp !~ /^4/ && |
---|
| 10442 | grep { !$_->recip_done } @{$msginfo->per_recip_data}) { |
---|
| 10443 | die("TROUBLE/MISCONFIG: not all recipients done, ". |
---|
| 10444 | "\$forward_method is \"$forward_method\""); |
---|
| 10445 | } |
---|
| 10446 | |
---|
| 10447 | # all ok |
---|
| 10448 | if($smtp_resp =~ /^2/) { |
---|
| 10449 | $self->qmqpqq_resp("K",$smtp_resp); |
---|
| 10450 | } |
---|
| 10451 | # permanent reject |
---|
| 10452 | elsif($smtp_resp =~ /^5/) { |
---|
| 10453 | $self->qmqpqq_resp("D",$smtp_resp); |
---|
| 10454 | } |
---|
| 10455 | # temporary reject (or other error if !~ /^4/) |
---|
| 10456 | else { |
---|
| 10457 | $self->qmqpqq_resp("Z",$smtp_resp); |
---|
| 10458 | } |
---|
| 10459 | }; |
---|
| 10460 | |
---|
| 10461 | alarm(0); do_log(5,"timer stopped after QMQPqq eval"); |
---|
| 10462 | |
---|
| 10463 | if($@ ne '') { |
---|
| 10464 | chomp($@); |
---|
| 10465 | |
---|
| 10466 | do_log(0,"QMQPqq: NOTICE: $@"); |
---|
| 10467 | $self->qmqpqq_resp("Z","Service shutting down, $@"); |
---|
| 10468 | } |
---|
| 10469 | |
---|
| 10470 | if ($self->preserve_evidence && !$self->{tempdir_empty}) { |
---|
| 10471 | # keep evidence in case of trouble |
---|
| 10472 | do_log(0,"PRESERVING EVIDENCE in ".$self->{tempdir_pers}); |
---|
| 10473 | $self->{fh_pers}->close or die "Can't close mail file: $!"; |
---|
| 10474 | $self->{fh_pers} = undef; $self->{tempdir_pers} = undef; |
---|
| 10475 | $self->{tempdir_empty} = 1; |
---|
| 10476 | } |
---|
| 10477 | |
---|
| 10478 | # cleanup, but leave directory (and file handle |
---|
| 10479 | # if possible) for reuse |
---|
| 10480 | if ($self->{fh_pers} && !$can_truncate) { |
---|
| 10481 | # truncate is not standard across all Unix variants, |
---|
| 10482 | # it is not Posix, but is XPG4-UNIX. |
---|
| 10483 | # So if we can't truncate a file and leave it open, |
---|
| 10484 | # we have to create it anew later, at some cost. |
---|
| 10485 | # |
---|
| 10486 | $self->{fh_pers}->close or die "Can't close mail file: $!"; |
---|
| 10487 | $self->{fh_pers} = undef; |
---|
| 10488 | unlink($self->{tempdir_pers}."/email.txt") |
---|
| 10489 | or die "Can't delete file ". |
---|
| 10490 | $self->{tempdir_pers}."/email.txt: $!"; |
---|
| 10491 | section_time('delete email.txt'); |
---|
| 10492 | } |
---|
| 10493 | |
---|
| 10494 | if (defined $self->{tempdir_pers}) { # prepare for the next one |
---|
| 10495 | strip_tempdir($self->{tempdir_pers}); |
---|
| 10496 | $self->{tempdir_empty} = 1; |
---|
| 10497 | } |
---|
| 10498 | |
---|
| 10499 | $self->preserve_evidence(0); # reset |
---|
| 10500 | # report elapsed times by section for each transaction |
---|
| 10501 | do_log(2, Amavis::Timing::report()); |
---|
| 10502 | |
---|
| 10503 | $self->{session_closed_normally} = 1; |
---|
| 10504 | # closes connection after child_finish_hook |
---|
| 10505 | } |
---|
| 10506 | |
---|
| 10507 | # sends a QMQPqq response consisting of K/D/Z code and an optional message; |
---|
| 10508 | # slow down evil clients by delaying response on permanent errors |
---|
| 10509 | sub qmqpqq_resp($$$;$$) { |
---|
| 10510 | my($self,$code,$resp,$penalize,$line) = @_; |
---|
| 10511 | if($code !~ /^(K|Z|D)$/) { |
---|
| 10512 | die("Internal error(2): bad QMQPqq response code: '$code'"); |
---|
| 10513 | } |
---|
| 10514 | if($penalize) { |
---|
| 10515 | do_log(0,"QMQPqq: $resp; PENALIZE: $line"); |
---|
| 10516 | sleep 5; |
---|
| 10517 | section_time('QMQPqq penalty wait'); |
---|
| 10518 | } |
---|
| 10519 | $resp = sanitize_str($resp,1); |
---|
| 10520 | do_log(4,"QMQPqq> $resp"); |
---|
| 10521 | print($self->netstring($code . $resp)); |
---|
| 10522 | } |
---|
| 10523 | |
---|
| 10524 | sub netstring($$) { |
---|
| 10525 | my($self,$string) = @_; |
---|
| 10526 | return(sprintf("%d:%s,",length($string),$string)); |
---|
| 10527 | } |
---|
| 10528 | |
---|
| 10529 | 1; |
---|
| 10530 | |
---|
| 10531 | __DATA__ |
---|
| 10532 | # |
---|
| 10533 | package Amavis::AV; |
---|
| 10534 | use strict; |
---|
| 10535 | use re 'taint'; |
---|
| 10536 | |
---|
| 10537 | BEGIN { |
---|
| 10538 | use Exporter (); |
---|
| 10539 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 10540 | $VERSION = '2.034'; |
---|
| 10541 | @ISA = qw(Exporter); |
---|
| 10542 | @EXPORT_OK = qw(&sophos_savi_init); |
---|
| 10543 | } |
---|
| 10544 | |
---|
| 10545 | use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED |
---|
| 10546 | WEXITSTATUS WTERMSIG WSTOPSIG); |
---|
| 10547 | use Errno qw(EPIPE ENOTCONN ENOENT EACCES); |
---|
| 10548 | use Socket; |
---|
| 10549 | use IO::Socket; |
---|
| 10550 | use IO::Socket::UNIX; |
---|
| 10551 | |
---|
| 10552 | use subs @EXPORT_OK; |
---|
| 10553 | use vars @EXPORT; |
---|
| 10554 | |
---|
| 10555 | BEGIN { |
---|
| 10556 | import Amavis::Conf qw(:platform :confvars c cr ca); |
---|
| 10557 | import Amavis::Util qw(ll untaint min max do_log am_id rmdir_recursively |
---|
| 10558 | exit_status_str run_command); |
---|
| 10559 | import Amavis::Timing qw(section_time); |
---|
| 10560 | } |
---|
| 10561 | |
---|
| 10562 | use vars qw(%st_socket_created %st_sock); # keep persistent state (per-socket) |
---|
| 10563 | |
---|
| 10564 | # subroutine available for calling from @av_scanners list entries; |
---|
| 10565 | # it has the same args and returns as run_av() below |
---|
| 10566 | sub ask_daemon { ask_av(\&ask_daemon_internal, @_) } |
---|
| 10567 | |
---|
| 10568 | sub clamav_module_init($) { |
---|
| 10569 | my($av_name) = @_; |
---|
| 10570 | # each child should reinitialize clamav module to reload databases. |
---|
| 10571 | my($clamav_version) = Mail::ClamAV->VERSION; |
---|
| 10572 | my($dbdir) = Mail::ClamAV::retdbdir(); |
---|
| 10573 | my($clamav_obj) = Mail::ClamAV->new($dbdir); |
---|
| 10574 | ref $clamav_obj |
---|
| 10575 | or die "$av_name: Can't load db from $dbdir: $Mail::ClamAV::Error"; |
---|
| 10576 | $clamav_obj->buildtrie; |
---|
| 10577 | $clamav_obj->maxreclevel($MAXLEVELS) if $MAXLEVELS; |
---|
| 10578 | $clamav_obj->maxfiles($MAXFILES); |
---|
| 10579 | $clamav_obj->maxfilesize($MAX_EXPANSION_QUOTA || 30*1024*1024); |
---|
| 10580 | if ($clamav_version >= 0.12) { |
---|
| 10581 | $clamav_obj->maxratio($MAX_EXPANSION_FACTOR); |
---|
| 10582 | # $clamav_obj->archivememlim(0); # limit memory usage for bzip2 (0/1) |
---|
| 10583 | } |
---|
| 10584 | do_log(2,"$av_name init"); |
---|
| 10585 | section_time('clamav_module_init'); |
---|
| 10586 | ($clamav_obj,$clamav_version); |
---|
| 10587 | } |
---|
| 10588 | |
---|
| 10589 | # to be called from sub ask_clamav |
---|
| 10590 | use vars qw($clamav_obj $clamav_version); |
---|
| 10591 | sub clamav_module_internal($@) { |
---|
| 10592 | my($query, $bare_fnames,$names_to_parts,$tempdir, $av_name) = @_; |
---|
| 10593 | if (!defined $clamav_obj) { |
---|
| 10594 | ($clamav_obj,$clamav_version) = clamav_module_init($av_name); # first time |
---|
| 10595 | } elsif ($clamav_obj->statchkdir) { # db reload needed? |
---|
| 10596 | do_log(2, "$av_name: reloading virus database"); |
---|
| 10597 | ($clamav_obj,$clamav_version) = clamav_module_init($av_name); |
---|
| 10598 | } |
---|
| 10599 | my($fname) = "$tempdir/parts/$query"; # file to be checked |
---|
| 10600 | my($part) = $names_to_parts->{$query}; # get corresponding parts object |
---|
| 10601 | my($options) = 0; # bitfield of options to Mail::ClamAV::scan |
---|
| 10602 | my($opt_archive,$opt_mail); |
---|
| 10603 | if ($clamav_version < 0.12) { |
---|
| 10604 | $opt_archive = &Mail::ClamAV::CL_ARCHIVE; |
---|
| 10605 | $opt_mail = &Mail::ClamAV::CL_MAIL; |
---|
| 10606 | } else { # >= 0.12, reflects renamed flags in libclamav 0.80 |
---|
| 10607 | $opt_archive = &Mail::ClamAV::CL_SCAN_ARCHIVE; |
---|
| 10608 | $opt_mail = &Mail::ClamAV::CL_SCAN_MAIL; |
---|
| 10609 | } |
---|
| 10610 | $options |= &Mail::ClamAV::CL_SCAN_STDOPT if $clamav_version >= 0.13; |
---|
| 10611 | $options |= $opt_archive; # turn on ARCHIVE |
---|
| 10612 | $options &= ~$opt_mail; # turn off MAIL |
---|
| 10613 | if (ref($part) && (lc($part->type_short) eq 'mail' || |
---|
| 10614 | lc($part->type_declared) eq 'message/rfc822')) { |
---|
| 10615 | do_log(2, "$av_name: $query - enabling option CL_MAIL"); |
---|
| 10616 | $options |= $opt_mail; # turn on MAIL |
---|
| 10617 | } |
---|
| 10618 | my($ret) = $clamav_obj->scan(untaint($fname), $options); |
---|
| 10619 | my($output,$status); |
---|
| 10620 | if ($ret->virus) { $status = 1; $output = "INFECTED: $ret" } |
---|
| 10621 | elsif ($ret->clean) { $status = 0; $output = "CLEAN" } |
---|
| 10622 | else { $status = 2; $output = $ret->error.", errno=".$ret->errno } |
---|
| 10623 | ($status,$output); # return synthesised status and a result string |
---|
| 10624 | } |
---|
| 10625 | |
---|
| 10626 | # subroutine available for calling from @av_scanners list entries; |
---|
| 10627 | # it has the same args and returns as run_av() below |
---|
| 10628 | sub ask_clamav { ask_av(\&clamav_module_internal, @_) } |
---|
| 10629 | |
---|
| 10630 | |
---|
| 10631 | use vars qw($savi_obj); |
---|
| 10632 | sub sophos_savi_init { |
---|
| 10633 | my($av_name, $command) = @_; |
---|
| 10634 | my(@savi_bool_options) = qw( |
---|
| 10635 | FullSweep DynamicDecompression FullMacroSweep OLE2Handling |
---|
| 10636 | IgnoreTemplateBit VBA3Handling VBA5Handling OF95DecryptHandling |
---|
| 10637 | HelpHandling DecompressVBA5 Emulation PEHandling ExcelFormulaHandling |
---|
| 10638 | PowerPointMacroHandling PowerPointEmbeddedHandling ProjectHandling |
---|
| 10639 | ZipDecompression ArjDecompression RarDecompression UueDecompression |
---|
| 10640 | GZipDecompression TarDecompression CmzDecompression HqxDecompression |
---|
| 10641 | MbinDecompression !LoopBackEnabled |
---|
| 10642 | Lha SfxArchives MSCabinet TnefAttachmentHandling MSCompress |
---|
| 10643 | !DeleteAllMacros Vbe !ExecFileDisinfection VisioFileHandling |
---|
| 10644 | ActiveMimeHandling !DelVBA5Project |
---|
| 10645 | ScrapObjectHandling SrpStreamHandling Office2001Handling |
---|
| 10646 | Upx PalmPilotHandling HqxDecompression |
---|
| 10647 | Pdf Rtf Html Elf WordB OutlookExpress |
---|
| 10648 | ); |
---|
| 10649 | # starting with SAVI V3: Mac and SafeMacDfHandling options were removed; |
---|
| 10650 | # new option GrpArchiveUnpack makes individual settings unnecessary; |
---|
| 10651 | # option 'Mime' may cause a CPU loop when checking broken mail with some |
---|
| 10652 | # versions of Sophos library (even with more recent ones!) |
---|
| 10653 | my($savi_obj) = SAVI->new; |
---|
| 10654 | ref $savi_obj or die "$av_name: Can't create SAVI object, err=$savi_obj"; |
---|
| 10655 | my($version) = $savi_obj->version; |
---|
| 10656 | ref $version or die "$av_name: Can't get SAVI version, err=$version"; |
---|
| 10657 | do_log(2,sprintf("$av_name init: Version %s (engine %d.%d) recognizing %d viruses\n", |
---|
| 10658 | $version->string, $version->major, $version->minor, $version->count)); |
---|
| 10659 | # for ($version->ide_list) |
---|
| 10660 | # { do_log(2, sprintf("$av_name: IDE %s released %s", $_->name, $_->date)) } |
---|
| 10661 | my($error) = $savi_obj->set('MaxRecursionDepth', $MAXLEVELS, 1); |
---|
| 10662 | !defined $error or die "$av_name: error setting MaxRecursionDepth: err=$error"; |
---|
| 10663 | $error = $savi_obj->set('NamespaceSupport', 3); # new with Sophos 3.67 |
---|
| 10664 | !defined $error |
---|
| 10665 | or do_log(-1,"$av_name: error setting NamespaceSupport: err=$error"); |
---|
| 10666 | for (@savi_bool_options) { |
---|
| 10667 | my($value) = /^!/ ? 0 : 1; s/^!+//; |
---|
| 10668 | $error = $savi_obj->set($_, $value); |
---|
| 10669 | !defined $error or die "$av_name: Error setting $_: err=$error"; |
---|
| 10670 | } |
---|
| 10671 | section_time('sophos_savi_init'); |
---|
| 10672 | $savi_obj; |
---|
| 10673 | } |
---|
| 10674 | |
---|
| 10675 | # to be called from sub sophos_savi |
---|
| 10676 | sub sophos_savi_internal { |
---|
| 10677 | my($query, |
---|
| 10678 | $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args) = @_; |
---|
| 10679 | # if (defined $args) { $savi_obj = $args } |
---|
| 10680 | # else { |
---|
| 10681 | $savi_obj = sophos_savi_init($av_name,$command) if !defined $savi_obj; |
---|
| 10682 | # } |
---|
| 10683 | my($fname) = "$tempdir/parts/$query"; # file to be checked |
---|
| 10684 | my($part) = $names_to_parts->{$query}; # get corresponding parts object |
---|
| 10685 | my($mime_option_value) = 0; |
---|
| 10686 | if (ref($part) && (lc($part->type_short) eq 'mail' || |
---|
| 10687 | lc($part->type_declared) eq 'message/rfc822')) { |
---|
| 10688 | do_log(2, "$av_name: $query - enabling option MIME"); |
---|
| 10689 | $mime_option_value = 1; |
---|
| 10690 | } |
---|
| 10691 | my($error) = $savi_obj->set('MIME', $mime_option_value); |
---|
| 10692 | !defined $error or die sprintf("%s: Error %s option MIME: err=%s", |
---|
| 10693 | $av_name, $mime_option_value ? 'setting' : 'clearing', $error); |
---|
| 10694 | my($output,$status); my($result) = $savi_obj->scan($fname); |
---|
| 10695 | if (!ref($result)) { # error |
---|
| 10696 | my($msg) = "$av_name: error scanning file $fname, " . |
---|
| 10697 | $savi_obj->error_string($result) . " ($result) $!"; |
---|
| 10698 | if (! grep {$result == $_} (514,527,530,538,549) ) { |
---|
| 10699 | $status = 2; $output = "ERROR: $msg\n"; |
---|
| 10700 | } else { # don't panic on non-fatal (encrypted, corrupted, partial) |
---|
| 10701 | $status = 0; $output = "CLEAN: $msg\n"; |
---|
| 10702 | } |
---|
| 10703 | do_log(-1,$output); |
---|
| 10704 | } elsif ($result->infected) { |
---|
| 10705 | $status = 1; $output = "INFECTED $query\n"; |
---|
| 10706 | for my $virus_name ($result->viruses) { $output .= "$virus_name FOUND\n" } |
---|
| 10707 | } else { |
---|
| 10708 | $status = 0; $output = "CLEAN $query\n"; |
---|
| 10709 | } |
---|
| 10710 | ($status,$output); # return synthesised status and a result string |
---|
| 10711 | } |
---|
| 10712 | |
---|
| 10713 | # subroutine available for calling from @av_scanners list entries; |
---|
| 10714 | # it has the same args and returns as run_av() below |
---|
| 10715 | sub ask_sophos_savi { |
---|
| 10716 | my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args, |
---|
| 10717 | $sts_clean,$sts_infected,$how_to_get_names) = @_; |
---|
| 10718 | if (@_ < 3+6) { # supply default arguments for backwards compatibility |
---|
| 10719 | $args = ["*"]; $sts_clean = [0]; $sts_infected = [1]; |
---|
| 10720 | $how_to_get_names = qr/^(.*) FOUND$/; |
---|
| 10721 | } |
---|
| 10722 | ask_av(\&sophos_savi_internal, |
---|
| 10723 | $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args, |
---|
| 10724 | $sts_clean, $sts_infected, $how_to_get_names); |
---|
| 10725 | } |
---|
| 10726 | |
---|
| 10727 | |
---|
| 10728 | # same args and returns as run_av() below, |
---|
| 10729 | # but prepended by a $query, which is the string to be sent to the daemon. |
---|
| 10730 | # Handles both UNIX and INET domain sockets. |
---|
| 10731 | # More than one socket may be specified for redundancy, they will be tried |
---|
| 10732 | # one after the other until one succeeds. |
---|
| 10733 | # |
---|
| 10734 | sub ask_daemon_internal { |
---|
| 10735 | my($query, # expanded query template, often a command and a file or dir name |
---|
| 10736 | $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args, |
---|
| 10737 | $sts_clean,$sts_infected,$how_to_get_names, # regexps |
---|
| 10738 | ) = @_; |
---|
| 10739 | my($query_template_orig,$sockets) = @$args; |
---|
| 10740 | my($output); my($socketname,$is_inet); |
---|
| 10741 | if (!ref($sockets)) { $sockets = [ $sockets ] } |
---|
| 10742 | my($max_retries) = 2 * @$sockets; my($retries) = 0; |
---|
| 10743 | $SIG{PIPE} = 'IGNORE'; # 'send' to broken pipe would throw a signal |
---|
| 10744 | for (;;) { # gracefully handle cases when av child times out or restarts |
---|
| 10745 | @$sockets >= 1 or die "no sockets specified!?"; # sanity |
---|
| 10746 | $socketname = $sockets->[0]; # try the first one in the current list |
---|
| 10747 | $is_inet = $socketname =~ m{^/} ? 0 : 1; # simpleminded: unix vs. inet sock |
---|
| 10748 | eval { |
---|
| 10749 | if (!$st_socket_created{$socketname}) { |
---|
| 10750 | ll(3) && do_log(3, "$av_name: Connecting to socket " . |
---|
| 10751 | join(' ',$daemon_chroot_dir,$socketname). |
---|
| 10752 | (!$retries ? '' : ", retry #$retries") ); |
---|
| 10753 | if ($is_inet) { # inet socket |
---|
| 10754 | $st_sock{$socketname} = IO::Socket::INET->new($socketname) |
---|
| 10755 | or die "Can't connect to INET socket $socketname: $!\n"; |
---|
| 10756 | $st_socket_created{$socketname} = 1; |
---|
| 10757 | } else { # unix socket |
---|
| 10758 | $st_sock{$socketname} = IO::Socket::UNIX->new(Type => SOCK_STREAM) |
---|
| 10759 | or die "Can't create UNIX socket: $!\n"; |
---|
| 10760 | $st_socket_created{$socketname} = 1; |
---|
| 10761 | $st_sock{$socketname}->connect( pack_sockaddr_un($socketname) ) |
---|
| 10762 | or die "Can't connect to UNIX socket $socketname: $!\n"; |
---|
| 10763 | } |
---|
| 10764 | } |
---|
| 10765 | ll(3) && do_log(3,sprintf("$av_name: Sending %s to %s socket %s", |
---|
| 10766 | $query, $is_inet?"INET":"UNIX", $socketname)); |
---|
| 10767 | # UGLY: bypass send method in IO::Socket to be able to retrieve |
---|
| 10768 | # status/errno directly from 'send', not from 'getpeername': |
---|
| 10769 | defined send($st_sock{$socketname}, $query, 0) |
---|
| 10770 | or die "Can't send to socket $socketname: $!\n"; |
---|
| 10771 | if ($av_name =~ /^(Sophie|Trophie)/i) { |
---|
| 10772 | # Sophie and Trophie can accept multiple requests per session |
---|
| 10773 | # and return a single line response each time |
---|
| 10774 | defined $st_sock{$socketname}->recv($output, 1024) |
---|
| 10775 | or die "Can't receive from socket $socketname: $!\n"; |
---|
| 10776 | } else { |
---|
| 10777 | $output = join('', $st_sock{$socketname}->getlines); |
---|
| 10778 | $st_sock{$socketname}->close |
---|
| 10779 | or die "Can't close socket $socketname: $!\n"; |
---|
| 10780 | $st_sock{$socketname}=undef; $st_socket_created{$socketname}=0; |
---|
| 10781 | } |
---|
| 10782 | $! = undef; |
---|
| 10783 | $output ne '' or die "Empty result from $socketname\n"; |
---|
| 10784 | }; |
---|
| 10785 | last if $@ eq ''; |
---|
| 10786 | # error handling (most interesting error codes are EPIPE and ENOTCONN) |
---|
| 10787 | chomp($@); my($err) = "$!"; my($errn) = 0+$!; |
---|
| 10788 | ++$retries <= $max_retries |
---|
| 10789 | or die "Too many retries to talk to $socketname ($@)"; |
---|
| 10790 | # is ECONNREFUSED for INET sockets common enough too? |
---|
| 10791 | if ($retries <= 1 && $errn == EPIPE) { # common, don't cause concern |
---|
| 10792 | do_log(2,"$av_name broken pipe (don't worry), retrying ($retries)"); |
---|
| 10793 | } else { |
---|
| 10794 | do_log( ($retries>1?-1:1), "$av_name: $@, retrying ($retries)"); |
---|
| 10795 | if ($retries % @$sockets == 0) { # every time the list is exhausted |
---|
| 10796 | my($dly) = min(20, 1 + 5 * ($retries/@$sockets - 1)); |
---|
| 10797 | do_log(3,"$av_name: sleeping for $dly s"); |
---|
| 10798 | sleep($dly); # slow down a possible runaway |
---|
| 10799 | } |
---|
| 10800 | } |
---|
| 10801 | if ($st_socket_created{$socketname}) { |
---|
| 10802 | # prepare for a retry, ignore 'close' status |
---|
| 10803 | $st_sock{$socketname}->close; |
---|
| 10804 | $st_sock{$socketname} = undef; $st_socket_created{$socketname} = 0; |
---|
| 10805 | } |
---|
| 10806 | # leave good socket as the first entry in the list |
---|
| 10807 | # so that it will be tried first when needed again |
---|
| 10808 | push(@$sockets, shift @$sockets) if @$sockets>1; # circular shift left |
---|
| 10809 | } |
---|
| 10810 | (0,$output); # return synthesised status and result string |
---|
| 10811 | } |
---|
| 10812 | |
---|
| 10813 | # ask_av is a common subroutine available to be used by ask_daemon, ask_clamav, |
---|
| 10814 | # ask_sophos_savi and similar front-end routines used in @av_scanners entries. |
---|
| 10815 | # It traverses supplied files or directory ($bare_fnames) and calls a supplied |
---|
| 10816 | # subroutine for each file to be scanned, summarizing the final av scan result. |
---|
| 10817 | # It has the same args and returns as run_av() below, prepended by a checking |
---|
| 10818 | # subroutine argument. |
---|
| 10819 | sub ask_av { |
---|
| 10820 | my($code) = shift; # strip away the first argument, a subroutine ref |
---|
| 10821 | my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args, |
---|
| 10822 | $sts_clean,$sts_infected,$how_to_get_names) = @_; |
---|
| 10823 | my($query_template) = ref $args eq 'ARRAY' ? $args->[0] : $args; |
---|
| 10824 | do_log(5, "ask_av ($av_name): query template1: $query_template"); |
---|
| 10825 | my($checking_each_file) = $query_template =~ /\*/; |
---|
| 10826 | my($scan_status,@virusname); my($output) = ''; |
---|
| 10827 | for my $f ($checking_each_file ? @$bare_fnames : ("$tempdir/parts")) { |
---|
| 10828 | my($query) = $query_template; |
---|
| 10829 | if (!$checking_each_file) { # scanner can be given a directory name |
---|
| 10830 | $query =~ s[{}][$tempdir/parts]g; # replace {} with directory name |
---|
| 10831 | do_log(3,"Using ($av_name) on dir: $query"); |
---|
| 10832 | } else { # must check each file individually |
---|
| 10833 | # replace {}/* with directory name and file, and * with current file name |
---|
| 10834 | $query =~ s[ ({}/)? \* ][ $1 eq '' ? $f : "$tempdir/parts/$f" ]gesx; |
---|
| 10835 | do_log(3,"Using ($av_name) on file: $query"); |
---|
| 10836 | } |
---|
| 10837 | my($t_status,$t_output) = &$code($query, @_); |
---|
| 10838 | do_log(4,"ask_av ($av_name) result: $t_output"); |
---|
| 10839 | # braindead Perl: ""=~/x{0}/ serves as explicit default for an empty regexp |
---|
| 10840 | if (defined $sts_infected && ( |
---|
| 10841 | ref($sts_infected) eq 'ARRAY' ? (grep {$_==$t_status} @$sts_infected) |
---|
| 10842 | : ""=~/x{0}/ && $t_output=~/$sts_infected/m)) { # is infected |
---|
| 10843 | # test for infected first, in case both expressions match |
---|
| 10844 | $scan_status = 1; # 'true' indicates virus found, no errors |
---|
| 10845 | my(@t_virusnames) = ref($how_to_get_names) eq 'CODE' |
---|
| 10846 | ? &$how_to_get_names($t_output) |
---|
| 10847 | : ""=~/x{0}/ && $t_output=~/$how_to_get_names/gm; |
---|
| 10848 | @t_virusnames = map { defined $_ ? $_ : () } @t_virusnames; |
---|
| 10849 | push(@virusname, @t_virusnames); |
---|
| 10850 | $output .= $t_output . $eol; |
---|
| 10851 | do_log(2,"ask_av ($av_name): $f INFECTED: ".join(", ",@t_virusnames)); |
---|
| 10852 | } elsif (!defined($sts_clean)) { # clean, but inconclusive |
---|
| 10853 | # by convention: undef $sts_clean means result is inconclusive, |
---|
| 10854 | # file appears clean, but continue scanning with other av scanners, |
---|
| 10855 | # the current scanner does not want to vouch for it; useful for a |
---|
| 10856 | # scanner like jpeg checker which tests for one vulnerability only |
---|
| 10857 | do_log(3,"ask_av ($av_name): $f CLEAN, but inconclusive"); |
---|
| 10858 | } elsif (ref($sts_clean) eq 'ARRAY' |
---|
| 10859 | ? (grep {$_==$t_status} @$sts_clean) |
---|
| 10860 | : ""=~/x{0}/ && $t_output=~/$sts_clean/m) { # is clean |
---|
| 10861 | $scan_status = 0 if !$scan_status; # no viruses, no errors |
---|
| 10862 | do_log(3,"ask_av ($av_name): $f CLEAN"); |
---|
| 10863 | } else { |
---|
| 10864 | do_log(-2,"ask_av ($av_name) FAILED - unexpected result: $t_output"); |
---|
| 10865 | last; # error, bail out |
---|
| 10866 | } |
---|
| 10867 | } |
---|
| 10868 | if (!@$bare_fnames) { $scan_status = 0 } # no errors, no viruses |
---|
| 10869 | do_log(3,"$av_name result: clean") if defined($scan_status) && !$scan_status; |
---|
| 10870 | ($scan_status,$output,\@virusname); |
---|
| 10871 | } |
---|
| 10872 | |
---|
| 10873 | # Call a virus scanner and parse its output. |
---|
| 10874 | # Returns a triplet (or die in case of failure). |
---|
| 10875 | # The first element of the triplet is interpreted as follows: |
---|
| 10876 | # - true if virus found, |
---|
| 10877 | # - 0 if no viruses found, |
---|
| 10878 | # - undef if it did not complete its job; |
---|
| 10879 | # the second element is a string, the text as provided by the virus scanner; |
---|
| 10880 | # the third element is ref to a list of virus names found (if any). |
---|
| 10881 | # (it is guaranteed the list will be nonempty if virus was found) |
---|
| 10882 | # |
---|
| 10883 | sub run_av { |
---|
| 10884 | # first three args are prepended, not part of n-tuple |
---|
| 10885 | my($bare_fnames, # a ref to a list of filenames to scan (basenames) |
---|
| 10886 | $names_to_parts, # ref to a hash that maps base file names to parts object |
---|
| 10887 | $tempdir, # temporary directory |
---|
| 10888 | $av_name, $command, $args, |
---|
| 10889 | $sts_clean, # a ref to a list of status values, or a regexp |
---|
| 10890 | $sts_infected, # a ref to a list of status values, or a regexp |
---|
| 10891 | $how_to_get_names, # ref to sub, or a regexp to get list of virus names |
---|
| 10892 | $pre_code, $post_code, # routines to be invoked before and after av |
---|
| 10893 | ) = @_; |
---|
| 10894 | my($scan_status,$virusnames,$error_str); my($output) = ''; |
---|
| 10895 | &$pre_code(@_) if defined $pre_code; |
---|
| 10896 | if (ref($command) eq 'CODE') { |
---|
| 10897 | do_log(3,"Using $av_name: (built-in interface)"); |
---|
| 10898 | ($scan_status,$output,$virusnames) = &$command(@_); |
---|
| 10899 | } else { |
---|
| 10900 | my(@args) = split(' ',$args); |
---|
| 10901 | if (grep { m{^({}/)?\*\z} } @args) { # {}/* or *, list each file |
---|
| 10902 | # replace asterisks with bare file names (basenames) if alone or in {}/* |
---|
| 10903 | local($1); |
---|
| 10904 | @args = map { !m{^({}/)?\*\z} ? $_ |
---|
| 10905 | : map {$1.untaint($_)} @$bare_fnames } @args; |
---|
| 10906 | } |
---|
| 10907 | for (@args) { s[{}][$tempdir/parts]g } # replace {} with directory name |
---|
| 10908 | # NOTE: RAV does not like '</dev/null' in its command! |
---|
| 10909 | ll(3) && do_log(3, "Using ($av_name): " . join(' ',$command,@args)); |
---|
| 10910 | my($proc_fh,$pid) = run_command(undef, "&1", $command, @args); |
---|
| 10911 | while( defined($_ = $proc_fh->getline) ) { $output .= $_ } |
---|
| 10912 | my($err); $proc_fh->close or $err=$!; my($child_stat) = $?; |
---|
| 10913 | $error_str = exit_status_str($child_stat,$err); |
---|
| 10914 | my($retval) = WEXITSTATUS($child_stat); |
---|
| 10915 | local($1); chomp($output); my($output_trimmed) = $output; |
---|
| 10916 | $output_trimmed =~ s/\r\n/\n/gs; |
---|
| 10917 | $output_trimmed =~ s/([ \t\n\r])[ \t\n\r]{4,}/$1.../gs; |
---|
| 10918 | $output_trimmed = "..." . substr($output_trimmed,-800) |
---|
| 10919 | if length($output_trimmed) > 800; |
---|
| 10920 | do_log(3, "run_av: $command $error_str, $output_trimmed"); |
---|
| 10921 | # braindead Perl: ""=~/x{0}/ serves as explicit default for an empty regexp |
---|
| 10922 | if (!WIFEXITED($child_stat)) { |
---|
| 10923 | } elsif (defined $sts_infected && ( |
---|
| 10924 | ref($sts_infected) eq 'ARRAY' |
---|
| 10925 | ? (grep {$_==$retval} @$sts_infected) |
---|
| 10926 | : ""=~/x{0}/ && $output=~/$sts_infected/m)) { # is infected |
---|
| 10927 | # test for infected first, in case both expressions match |
---|
| 10928 | $virusnames = []; # get a list of virus names by parsing output |
---|
| 10929 | @$virusnames = ref($how_to_get_names) eq 'CODE' |
---|
| 10930 | ? &$how_to_get_names($output) |
---|
| 10931 | : ""=~/x{0}/ && $output=~/$how_to_get_names/gm; |
---|
| 10932 | @$virusnames = map { defined $_ ? $_ : () } @$virusnames; |
---|
| 10933 | $scan_status = 1; # 'true' indicates virus found |
---|
| 10934 | do_log(2,"run_av ($av_name): INFECTED: ".join(", ",@$virusnames)); |
---|
| 10935 | } elsif (!defined($sts_clean)) { # clean, but inconclusive |
---|
| 10936 | # by convention: undef $sts_clean means result is inconclusive, |
---|
| 10937 | # file appears clean, but continue scanning with other av scanners, |
---|
| 10938 | # the current scanner does not want to vouch for it; useful for a |
---|
| 10939 | # scanner like jpeg checker which tests for one vulnerability only |
---|
| 10940 | do_log(3,"run_av ($av_name): clean, but inconclusive"); |
---|
| 10941 | } elsif (ref($sts_clean) eq 'ARRAY' ? (grep {$_==$retval} @$sts_clean) |
---|
| 10942 | : ""=~/x{0}/ && $output=~/$sts_clean/m) { # is clean |
---|
| 10943 | $scan_status = 0; # 'false' (but defined) indicates no viruses |
---|
| 10944 | do_log(5,"run_av ($av_name): clean"); |
---|
| 10945 | } else { |
---|
| 10946 | $error_str = "unexpected $error_str, output=\"$output_trimmed\""; |
---|
| 10947 | } |
---|
| 10948 | $output = $output_trimmed if length($output) > 900; |
---|
| 10949 | } |
---|
| 10950 | &$post_code(@_) if defined $post_code; |
---|
| 10951 | $virusnames = [] if !defined $virusnames; |
---|
| 10952 | @$virusnames = (undef) if $scan_status && !@$virusnames; # nonnil |
---|
| 10953 | if (!defined($scan_status) && defined($error_str)) { |
---|
| 10954 | die "$command $error_str"; # die is more informative than return value |
---|
| 10955 | } |
---|
| 10956 | ($scan_status, $output, $virusnames); |
---|
| 10957 | } |
---|
| 10958 | |
---|
| 10959 | sub virus_scan($$$) { |
---|
| 10960 | my($tempdir,$firsttime,$parts_root) = @_; |
---|
| 10961 | my($scan_status,$output,@virusname,@detecting_scanners); |
---|
| 10962 | my($anyone_done); my($anyone_tried); |
---|
| 10963 | my($bare_fnames_ref,$names_to_parts); |
---|
| 10964 | my(@errors); my($j); my($tier) = 'primary'; |
---|
| 10965 | for my $av (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) { |
---|
| 10966 | next if !defined $av; |
---|
| 10967 | if ($av eq "\000") { # 'magic' separator between lists |
---|
| 10968 | last if $anyone_done; |
---|
| 10969 | do_log(-2,"WARN: all $tier virus scanners failed, considering backups"); |
---|
| 10970 | $tier = 'secondary'; next; |
---|
| 10971 | } |
---|
| 10972 | next if !ref $av || !defined $av->[1]; |
---|
| 10973 | if (!defined $bare_fnames_ref) { # first time: collect file names to scan |
---|
| 10974 | ($bare_fnames_ref,$names_to_parts) = |
---|
| 10975 | files_to_scan("$tempdir/parts",$parts_root); |
---|
| 10976 | do_log(2, "Not calling virus scanners, ". |
---|
| 10977 | "no files to scan in $tempdir/parts") if !@$bare_fnames_ref; |
---|
| 10978 | } |
---|
| 10979 | $anyone_tried++; my($this_status,$this_output,$this_vn); |
---|
| 10980 | if (!@$bare_fnames_ref) { # no files to scan? |
---|
| 10981 | ($this_status,$this_output,$this_vn) = (0, '', []); # declare clean |
---|
| 10982 | } else { # call virus scanner |
---|
| 10983 | eval { |
---|
| 10984 | ($this_status,$this_output,$this_vn) = |
---|
| 10985 | run_av($bare_fnames_ref,$names_to_parts,$tempdir, @$av); |
---|
| 10986 | }; |
---|
| 10987 | if ($@ ne '') { |
---|
| 10988 | my($err) = $@; chomp($err); |
---|
| 10989 | $err = "$av->[0] av-scanner FAILED: $err"; |
---|
| 10990 | do_log(-2,$err); push(@errors,$err); |
---|
| 10991 | $this_status = undef; |
---|
| 10992 | }; |
---|
| 10993 | } |
---|
| 10994 | $anyone_done++ if defined $this_status; |
---|
| 10995 | $j++; section_time("AV-scan-$j"); |
---|
| 10996 | if ($this_status) { # virus detected by this scanner |
---|
| 10997 | push(@detecting_scanners, $av->[0]); |
---|
| 10998 | if (!@virusname) { # store results of the first scanner detecting |
---|
| 10999 | @virusname = @$this_vn; |
---|
| 11000 | $scan_status = $this_status; $output = $this_output; |
---|
| 11001 | } |
---|
| 11002 | last if c('first_infected_stops_scan'); # stop now if we found a virus? |
---|
| 11003 | } elsif (!defined($scan_status)) { # tentatively keep regardless of status |
---|
| 11004 | $scan_status = $this_status; $output = $this_output; |
---|
| 11005 | } |
---|
| 11006 | } |
---|
| 11007 | if (@virusname && @detecting_scanners) { |
---|
| 11008 | my(@ds) = @detecting_scanners; for (@ds) { s/,/;/ } # facilitates parsing |
---|
| 11009 | ll(2) && do_log(2, sprintf("virus_scan: (%s), detected by %d scanners: %s", |
---|
| 11010 | join(', ',@virusname), scalar(@ds), join(', ',@ds))); |
---|
| 11011 | } |
---|
| 11012 | $output =~ s{\Q$tempdir\E/parts/?}{}gs if defined $output; # hide path info |
---|
| 11013 | if (!$anyone_tried) { die "NO VIRUS SCANNERS AVAILABLE\n" } |
---|
| 11014 | elsif (!$anyone_done) |
---|
| 11015 | { die ("ALL VIRUS SCANNERS FAILED: ".join("; ",@errors)."\n") } |
---|
| 11016 | ($scan_status, $output, \@virusname, \@detecting_scanners); # return a quad |
---|
| 11017 | } |
---|
| 11018 | |
---|
| 11019 | # return a ref to a list of files to be scanned in a given directory |
---|
| 11020 | sub files_to_scan($$) { |
---|
| 11021 | my($dir,$parts_root) = @_; |
---|
| 11022 | my($names_to_parts) = {}; # a hash that maps base file names |
---|
| 11023 | # to Amavis::Unpackers::Part object |
---|
| 11024 | for (my($part), my(@unvisited)=($parts_root); |
---|
| 11025 | @unvisited and $part=shift(@unvisited); |
---|
| 11026 | push(@unvisited,@{$part->children})) |
---|
| 11027 | { # traverse decomposed parts tree breadth-first, match it to actual files |
---|
| 11028 | $names_to_parts->{$part->base_name} = $part if $part ne $parts_root; |
---|
| 11029 | } |
---|
| 11030 | local(*DIR); my($f); my($bare_fnames_ref) = []; my(%bare_fnames); |
---|
| 11031 | opendir(DIR, $dir) or die "Can't open directory $dir: $!"; |
---|
| 11032 | # traverse parts directory and check for actual files |
---|
| 11033 | while (defined($f = readdir(DIR))) { |
---|
| 11034 | my($fname) = "$dir/$f"; |
---|
| 11035 | my($errn) = lstat($fname) ? 0 : 0+$!; |
---|
| 11036 | next if $errn == ENOENT; |
---|
| 11037 | if ($errn) { die "files_to_scan: file $fname inaccessible: $!" } |
---|
| 11038 | if (!-r _) { # attempting to gain read access to the file |
---|
| 11039 | do_log(3,"files_to_scan: attempting to gain read access to $fname"); |
---|
| 11040 | chmod(0750,untaint($fname)) |
---|
| 11041 | or die "files_to_scan: Can't change protection on $fname: $!"; |
---|
| 11042 | $errn = lstat($fname) ? 0 : 0+$!; |
---|
| 11043 | if ($errn) { die "files_to_scan: file $fname inaccessible: $!" } |
---|
| 11044 | if (!-r _) { die "files_to_scan: file $fname not readable" } |
---|
| 11045 | } |
---|
| 11046 | next if ($f eq '.' || $f eq '..') && -d _; # this or the parent directory |
---|
| 11047 | if (!-f _ || !exists $names_to_parts->{$f}) { # nonregular f. or unexpected |
---|
| 11048 | my($what) = -l _ ? 'symlink' : -d _ ? 'directory' : -f _ ? 'file' |
---|
| 11049 | : 'non-regular file'; |
---|
| 11050 | my($msg) = "removing unexpected $what $fname"; |
---|
| 11051 | $msg .= ", it has no corresponding parts object" |
---|
| 11052 | if !exists $names_to_parts->{$f}; |
---|
| 11053 | do_log(-1, "WARN: files_to_scan: ".$msg); |
---|
| 11054 | if (-d _) { rmdir_recursively(untaint($fname)) } |
---|
| 11055 | else { unlink(untaint($fname)) or die "Can't delete $what $fname: $!" } |
---|
| 11056 | } elsif (-z _) { |
---|
| 11057 | # empty file |
---|
| 11058 | } else { |
---|
| 11059 | if ($f !~ /^[A-Za-z0-9_.-]+\z/s) |
---|
| 11060 | {do_log(-1,"WARN: files_to_scan: unexpected/suspicious file name: $f")} |
---|
| 11061 | push(@$bare_fnames_ref,$f); $bare_fnames{$f} = 1; |
---|
| 11062 | } |
---|
| 11063 | } |
---|
| 11064 | closedir(DIR) or die "Can't close directory $dir: $!"; |
---|
| 11065 | # remove entries from %$names_to_parts that have no corresponding files |
---|
| 11066 | my($fname,$part); |
---|
| 11067 | while ( ($fname,$part) = each %$names_to_parts ) { |
---|
| 11068 | next if exists $bare_fnames{$fname}; |
---|
| 11069 | if ($part->exists) { |
---|
| 11070 | my($type_short) = $part->type_short; |
---|
| 11071 | ll(4) && do_log(4,sprintf( |
---|
| 11072 | "files_to_scan: info: part %s (%s) no longer present", |
---|
| 11073 | $fname, (!ref $type_short ? $type_short : join(', ',@$type_short)) )); |
---|
| 11074 | } |
---|
| 11075 | delete $names_to_parts->{$fname}; # delete is allowed for the current elem. |
---|
| 11076 | } |
---|
| 11077 | ($bare_fnames_ref, $names_to_parts); |
---|
| 11078 | } |
---|
| 11079 | |
---|
| 11080 | 1; |
---|
| 11081 | |
---|
| 11082 | __DATA__ |
---|
| 11083 | # |
---|
| 11084 | package Amavis::SpamControl; |
---|
| 11085 | use strict; |
---|
| 11086 | use re 'taint'; |
---|
| 11087 | |
---|
| 11088 | BEGIN { |
---|
| 11089 | use Exporter (); |
---|
| 11090 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 11091 | $VERSION = '2.034'; |
---|
| 11092 | @ISA = qw(Exporter); |
---|
| 11093 | } |
---|
| 11094 | use FileHandle; |
---|
| 11095 | use Mail::SpamAssassin; |
---|
| 11096 | |
---|
| 11097 | BEGIN { |
---|
| 11098 | import Amavis::Conf qw(:platform :sa $daemon_user c cr ca); |
---|
| 11099 | import Amavis::Util qw(ll do_log retcode exit_status_str run_command |
---|
| 11100 | prolong_timer); |
---|
| 11101 | import Amavis::rfc2821_2822_Tools; |
---|
| 11102 | import Amavis::Timing qw(section_time); |
---|
| 11103 | import Amavis::Lookup qw(lookup); |
---|
| 11104 | } |
---|
| 11105 | |
---|
| 11106 | use subs @EXPORT_OK; |
---|
| 11107 | |
---|
| 11108 | use vars qw($spamassassin_obj); |
---|
| 11109 | |
---|
| 11110 | # called at startup, before the main fork |
---|
| 11111 | sub init() { |
---|
| 11112 | do_log(1, "SpamControl: initializing Mail::SpamAssassin"); |
---|
| 11113 | my($saved_umask) = umask; |
---|
| 11114 | $spamassassin_obj = Mail::SpamAssassin->new({ |
---|
| 11115 | debug => $sa_debug, |
---|
| 11116 | save_pattern_hits => $sa_debug, |
---|
| 11117 | dont_copy_prefs => 1, |
---|
| 11118 | local_tests_only => $sa_local_tests_only, |
---|
| 11119 | home_dir_for_helpers => $helpers_home, |
---|
| 11120 | stop_at_threshold => 0, |
---|
| 11121 | # DEF_RULES_DIR => '/usr/local/share/spamassassin', |
---|
| 11122 | # LOCAL_RULES_DIR => '/etc/mail/spamassassin', |
---|
| 11123 | #see man Mail::SpamAssassin for other options |
---|
| 11124 | }); |
---|
| 11125 | # $Mail::SpamAssassin::DEBUG->{rbl}=-3; |
---|
| 11126 | # $Mail::SpamAssassin::DEBUG->{dcc}=-3; |
---|
| 11127 | # $Mail::SpamAssassin::DEBUG->{pyzor}=-3; |
---|
| 11128 | # $Mail::SpamAssassin::DEBUG->{bayes}=-3; |
---|
| 11129 | # $Mail::SpamAssassin::DEBUG->{rulesrun}=4+64; |
---|
| 11130 | if ($sa_auto_whitelist && Mail::SpamAssassin::Version() < 3) { |
---|
| 11131 | do_log(1, "SpamControl: turning on SA auto-whitelisting (AWL)"); |
---|
| 11132 | # create a factory for the persistent address list |
---|
| 11133 | my($addrlstfactory) = Mail::SpamAssassin::DBBasedAddrList->new; |
---|
| 11134 | $spamassassin_obj->set_persistent_address_list_factory($addrlstfactory); |
---|
| 11135 | } |
---|
| 11136 | $spamassassin_obj->compile_now; # ensure all modules etc. are preloaded |
---|
| 11137 | alarm(0); # seems like SA forgets to clear alarm in some cases |
---|
| 11138 | umask($saved_umask); # restore our umask, SA clobbered it |
---|
| 11139 | do_log(1, "SpamControl: done"); |
---|
| 11140 | } |
---|
| 11141 | |
---|
| 11142 | # check envelope sender if white or blacklisted by each recipient; |
---|
| 11143 | # Saves the result in recip_blacklisted_sender and recip_whitelisted_sender |
---|
| 11144 | # properties of each recipient object. |
---|
| 11145 | # |
---|
| 11146 | sub white_black_list($$$$$) { |
---|
| 11147 | my($conn,$msginfo,$sql_wblist,$user_id_sql,$ldap_policy) = @_; |
---|
| 11148 | my($any_w)=0; my($any_b)=0; my($all)=1; my($wr,$br); |
---|
| 11149 | my($sender) = $msginfo->sender; |
---|
| 11150 | do_log(4,"wbl: checking sender <$sender>"); |
---|
| 11151 | for my $r (@{$msginfo->per_recip_data}) { |
---|
| 11152 | next if $r->recip_done; # already dealt with |
---|
| 11153 | my($found,$wb,$boost); my($recip) = $r->recip_addr; |
---|
| 11154 | my($user_id_ref,$mk_ref) = !defined $sql_wblist ? ([],[]) |
---|
| 11155 | : lookup(1,$recip,$user_id_sql); |
---|
| 11156 | do_log(5,"wbl: (SQL) recip <$recip>, ".scalar(@$user_id_ref)." matches") |
---|
| 11157 | if defined $sql_wblist && ll(5); |
---|
| 11158 | for my $ind (0..$#{$user_id_ref}) { # for ALL SQL sets matching the recip |
---|
| 11159 | my($user_id) = $user_id_ref->[$ind]; my($mkey); |
---|
| 11160 | ($wb,$mkey) = lookup(0,$sender, |
---|
| 11161 | Amavis::Lookup::SQLfield->new($sql_wblist,'wb','S',$user_id) ); |
---|
| 11162 | do_log(4,"wbl: (SQL) recip <$recip>, rid=$user_id, got: \"$wb\""); |
---|
| 11163 | if (!defined($wb)) { # NULL field or no match: remains undefined |
---|
| 11164 | } elsif ($wb =~ /^ *([+-]?\d+(?:\.\d*)?) *\z/) { # numeric |
---|
| 11165 | my($val) = 0+$1; # penalty points to be added to the score |
---|
| 11166 | $boost += $val; |
---|
| 11167 | ll(2) && do_log(2,sprintf( |
---|
| 11168 | "wbl: (SQL) soft-%slisted (%s) sender <%s> => <%s> (rid=%s)", |
---|
| 11169 | ($val<0?'white':'black'), $val, $sender, $recip, $user_id)); |
---|
| 11170 | $wb = undef; # not hard- white or blacklisting |
---|
| 11171 | } elsif ($wb =~ /^[ \000]*\z/) { # neutral, stops the search |
---|
| 11172 | $found++; $wb = 0; |
---|
| 11173 | do_log(5,"wbl: (SQL) recip <$recip> is neutral to sender <$sender>"); |
---|
| 11174 | } elsif ($wb =~ /^([BbNnFf])[ ]*\z/) { # blacklisted (B, N, F) |
---|
| 11175 | $found++; $wb = -1; $any_b++; $br = $recip; |
---|
| 11176 | $r->recip_blacklisted_sender(1); |
---|
| 11177 | do_log(5,"wbl: (SQL) recip <$recip> blacklisted sender <$sender>"); |
---|
| 11178 | } else { # whitelisted (W, Y, T) or anything else |
---|
| 11179 | if ($wb =~ /^([WwYyTt])[ ]*\z/) { |
---|
| 11180 | do_log(5, "wbl: (SQL) recip <$recip> whitelisted sender <$sender>"); |
---|
| 11181 | } else { |
---|
| 11182 | do_log(-1,"wbl: (SQL) recip <$recip> whitelisted sender <$sender>, ". |
---|
| 11183 | "unexpected wb field value: \"$wb\""); |
---|
| 11184 | } |
---|
| 11185 | $found++; $wb = +1; $any_w++; $wr = $recip; |
---|
| 11186 | $r->recip_whitelisted_sender(1); |
---|
| 11187 | } |
---|
| 11188 | last if $found; |
---|
| 11189 | } |
---|
| 11190 | if (!$found && defined($ldap_policy)) { |
---|
| 11191 | my($wblist); |
---|
| 11192 | my($keys_ref,$rhs_ref) = make_query_keys($sender,0,0); |
---|
| 11193 | my(@keys) = @$keys_ref; |
---|
| 11194 | do_log(5,sprintf("wbl: (LDAP) query keys: %s", |
---|
| 11195 | join(', ',map{"\"$_\""}@keys))); |
---|
| 11196 | $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new($ldap_policy,'amavisBlacklistSender','L-')); |
---|
| 11197 | for my $key (@keys) { |
---|
| 11198 | if (grep {/^\Q$key\E\z/i} @$wblist) { |
---|
| 11199 | $found++; $wb = -1; $br = $recip; $any_b++; |
---|
| 11200 | $r->recip_blacklisted_sender(1); |
---|
| 11201 | do_log(5,"wbl: (LDAP) recip <$recip> blacklisted sender <$sender>"); |
---|
| 11202 | } |
---|
| 11203 | } |
---|
| 11204 | $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new($ldap_policy,'amavisWhitelistSender','L-')); |
---|
| 11205 | for my $key (@keys) { |
---|
| 11206 | if (grep {/^\Q$key\E\z/i} @$wblist) { |
---|
| 11207 | $found++; $wb = +1; $wr = $recip; $any_w++; |
---|
| 11208 | $r->recip_whitelisted_sender(1); |
---|
| 11209 | do_log(5,"wbl: (LDAP) recip <$recip> whitelisted sender <$sender>"); |
---|
| 11210 | } |
---|
| 11211 | } |
---|
| 11212 | } |
---|
| 11213 | if (!$found) { # fall back to static lookups if no match |
---|
| 11214 | # sender can be both white- and blacklisted at the same time |
---|
| 11215 | my($val); my($r_ref,$mk_ref,@t); |
---|
| 11216 | |
---|
| 11217 | # NOTE on the specifics of $per_recip_blacklist_sender_lookup_tables : |
---|
| 11218 | # the $r_ref below is supposed to be a ref to a single lookup table |
---|
| 11219 | # for compatibility with pre-2.0 versions of amavisd-new; |
---|
| 11220 | # Note that this is different from @score_sender_maps, which is |
---|
| 11221 | # supposed to contain a ref to a _list_ of lookup tables as a result |
---|
| 11222 | # of the first-level lookup (on the recipient address as a key). |
---|
| 11223 | # |
---|
| 11224 | ($r_ref,$mk_ref) = lookup(0,$recip, |
---|
| 11225 | Amavis::Lookup::Label->new("blacklist_recip<$recip>"), |
---|
| 11226 | cr('per_recip_blacklist_sender_lookup_tables')); |
---|
| 11227 | @t = ( (defined $r_ref ? $r_ref : ()), @{ca('blacklist_sender_maps')} ); |
---|
| 11228 | $val = lookup(0,$sender, |
---|
| 11229 | Amavis::Lookup::Label->new("blacklist_sender<$sender>"), |
---|
| 11230 | @t) if @t; |
---|
| 11231 | if ($val) { |
---|
| 11232 | $found++; $wb = -1; $br = $recip; $any_b++; |
---|
| 11233 | $r->recip_blacklisted_sender(1); |
---|
| 11234 | do_log(5,"wbl: recip <$recip> blacklisted sender <$sender>"); |
---|
| 11235 | } |
---|
| 11236 | # similar for whitelists: |
---|
| 11237 | ($r_ref,$mk_ref) = lookup(0,$recip, |
---|
| 11238 | Amavis::Lookup::Label->new("whitelist_recip<$recip>"), |
---|
| 11239 | cr('per_recip_whitelist_sender_lookup_tables')); |
---|
| 11240 | @t = ( (defined $r_ref ? $r_ref : ()), @{ca('whitelist_sender_maps')} ); |
---|
| 11241 | $val = lookup(0,$sender, |
---|
| 11242 | Amavis::Lookup::Label->new("whitelist_sender<$sender>"), |
---|
| 11243 | @t) if @t; |
---|
| 11244 | if ($val) { |
---|
| 11245 | $found++; $wb = +1; $wr = $recip; $any_w++; |
---|
| 11246 | $r->recip_whitelisted_sender(1); |
---|
| 11247 | do_log(5,"wbl: recip <$recip> whitelisted sender <$sender>"); |
---|
| 11248 | } |
---|
| 11249 | } |
---|
| 11250 | if (!defined($boost)) { # static lookups if no match |
---|
| 11251 | # note the first argument of lookup() is true, requesting ALL matches |
---|
| 11252 | my($r_ref,$mk_ref) = lookup(1,$recip, |
---|
| 11253 | Amavis::Lookup::Label->new("score_recip<$recip>"), |
---|
| 11254 | @{ca('score_sender_maps')}); |
---|
| 11255 | for my $j (0..$#{$r_ref}) { # for ALL tables matching the recipient |
---|
| 11256 | my($val,$key) = lookup(0,$sender, |
---|
| 11257 | Amavis::Lookup::Label->new("score_sender<$sender>"), |
---|
| 11258 | @{$r_ref->[$j]} ); |
---|
| 11259 | if ($val != 0) { |
---|
| 11260 | $boost += $val; |
---|
| 11261 | ll(2) && do_log(2, |
---|
| 11262 | sprintf("wbl: soft-%slisted (%s) sender <%s> => <%s>, ". |
---|
| 11263 | "recip_key=\"%s\"", ($val<0?'white':'black'), |
---|
| 11264 | $val, $sender, $recip, $mk_ref->[$j])); |
---|
| 11265 | } |
---|
| 11266 | } |
---|
| 11267 | } |
---|
| 11268 | $r->recip_score_boost($boost) if defined $boost; |
---|
| 11269 | $all = 0 if !$wb; |
---|
| 11270 | } |
---|
| 11271 | if (!ll(2)) { |
---|
| 11272 | # don't bother preparing log report which will not be printed |
---|
| 11273 | } else { |
---|
| 11274 | my($msg) = ''; |
---|
| 11275 | if ($all && $any_w && !$any_b) { $msg = "whitelisted" } |
---|
| 11276 | elsif ($all && $any_b && !$any_w) { $msg = "blacklisted" } |
---|
| 11277 | elsif ($all) { $msg = "black or whitelisted by all recips" } |
---|
| 11278 | elsif ($any_b || $any_w) { |
---|
| 11279 | $msg .= "whitelisted by ".($any_w>1?"$any_w recips, ":"$wr, ") if $any_w; |
---|
| 11280 | $msg .= "blacklisted by ".($any_b>1?"$any_b recips, ":"$br, ") if $any_b; |
---|
| 11281 | $msg .= "but not by all,"; |
---|
| 11282 | } |
---|
| 11283 | do_log(2,"wbl: $msg sender <$sender>") if $msg ne ''; |
---|
| 11284 | } |
---|
| 11285 | ($any_w+$any_b, $all); |
---|
| 11286 | } |
---|
| 11287 | |
---|
| 11288 | # - returns true if spam detected, |
---|
| 11289 | # - returns 0 if no spam found, |
---|
| 11290 | # - throws exception (die) in case of errors, |
---|
| 11291 | # or just returns undef if it did not complete its jobs |
---|
| 11292 | # |
---|
| 11293 | sub spam_scan($$) { |
---|
| 11294 | my($conn,$msginfo) = @_; |
---|
| 11295 | my($spam_level, $spam_status, $spam_report); my(@lines); |
---|
| 11296 | my($hdr_edits) = $msginfo->header_edits; |
---|
| 11297 | if (!$hdr_edits) { |
---|
| 11298 | $hdr_edits = Amavis::Out::EditHeader->new; |
---|
| 11299 | $msginfo->header_edits($hdr_edits); |
---|
| 11300 | } |
---|
| 11301 | my($dspam_signature,$dspam_result,$dspam_fname); |
---|
| 11302 | push(@lines, sprintf("Return-Path: %s\n", # fake a local delivery agent |
---|
| 11303 | qquote_rfc2821_local($msginfo->sender))); |
---|
| 11304 | push(@lines, sprintf("X-Envelope-To: %s\n", |
---|
| 11305 | join(",\n ",qquote_rfc2821_local(@{$msginfo->recips})))); |
---|
| 11306 | my($fh) = $msginfo->mail_text; |
---|
| 11307 | my($mbsl) = c('sa_mail_body_size_limit'); |
---|
| 11308 | if ( defined $mbsl && |
---|
| 11309 | ($msginfo->orig_body_size > $mbsl || |
---|
| 11310 | $msginfo->orig_header_size + 1 + $msginfo->orig_body_size |
---|
| 11311 | > 5*1024 + $mbsl) |
---|
| 11312 | ) { |
---|
| 11313 | do_log(1,"spam_scan: not wasting time on SA, message ". |
---|
| 11314 | "longer than $mbsl bytes: ". |
---|
| 11315 | $msginfo->orig_header_size .'+'. $msginfo->orig_body_size); |
---|
| 11316 | } else { |
---|
| 11317 | if ($dspam eq '') { |
---|
| 11318 | do_log(5,"spam_scan: DSPAM not available, skipping it"); |
---|
| 11319 | } else { |
---|
| 11320 | # pass the mail to DSPAM, extract its result headers and feed them to SA |
---|
| 11321 | $dspam_fname = $msginfo->mail_tempdir . '/dspam.msg'; |
---|
| 11322 | my($dspam_fh) = IO::File->new; # will receive output from DSPAM |
---|
| 11323 | $dspam_fh->open($dspam_fname,'>',0640) |
---|
| 11324 | or die "Can't create file $dspam_fname: $!"; |
---|
| 11325 | $fh->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
| 11326 | my($proc_fh,$pid) = run_command('&'.fileno($fh), "&1", $dspam, |
---|
| 11327 | qw(--stdout --deliver=spam,innocent |
---|
| 11328 | --mode=tum --feature=chained,noise |
---|
| 11329 | --enable-signature-headers |
---|
| 11330 | --user), $daemon_user, |
---|
| 11331 | ); # --mode=teft |
---|
| 11332 | # qw(--stdout --deliver-spam) # dspam < 3.0 |
---|
| 11333 | # keep X-DSPAM-*, ignore other changes e.g. Content-Transfer-Encoding |
---|
| 11334 | my($all_local) = !grep { !lookup(0,$_,@{ca('local_domains_maps')}) } |
---|
| 11335 | @{$msginfo->recips}; |
---|
| 11336 | my($first_line); |
---|
| 11337 | while (defined($_ = $proc_fh->getline)) { # scan mail header from DSPAM |
---|
| 11338 | $dspam_fh->print($_) or die "Can't write to $dspam_fname: $!"; |
---|
| 11339 | if (!defined($first_line)) |
---|
| 11340 | { $first_line = $_; do_log(5,"spam_scan: from DSPAM: $first_line") } |
---|
| 11341 | last if $_ eq $eol; |
---|
| 11342 | local($1,$2); |
---|
| 11343 | if (/^(X-DSPAM[^:]*):[ \t]*(.*)$/) { # note: does not handle folding |
---|
| 11344 | my($hh,$hb) = ($1,$2); |
---|
| 11345 | $dspam_signature = $hb if /^X-DSPAM-Signature:/i; |
---|
| 11346 | $dspam_result = $hb if /^X-DSPAM-Result:/i; |
---|
| 11347 | do_log(3,$_); push(@lines,$_); # store header in array passed to SA |
---|
| 11348 | # add DSPAM header fields to passed mail |
---|
| 11349 | $hdr_edits->append_header($hh,$hb) if $all_local; |
---|
| 11350 | } |
---|
| 11351 | } |
---|
| 11352 | while ($proc_fh->read($_,16384) > 0) { # copy mail body from DSPAM |
---|
| 11353 | $dspam_fh->print($_) or die "Can't write to $dspam_fname: $!"; |
---|
| 11354 | } |
---|
| 11355 | my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); |
---|
| 11356 | $dspam_fh->close or die "Can't close $dspam_fname: $!"; |
---|
| 11357 | do_log(-1,sprintf("WARN: DSPAM problem, %s, result=%s", |
---|
| 11358 | exit_status_str($?,$err), $first_line) |
---|
| 11359 | ) if $retval || !defined $first_line; |
---|
| 11360 | do_log(4,"spam_scan: DSPAM gave: $dspam_signature, $dspam_result"); |
---|
| 11361 | section_time('DSPAM'); |
---|
| 11362 | } |
---|
| 11363 | # read mail into memory in preparation for SpamAssasin |
---|
| 11364 | my($body_lines) = 0; |
---|
| 11365 | $fh->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
| 11366 | while (<$fh>) { push(@lines,$_); last if $_ eq $eol } # header |
---|
| 11367 | while (<$fh>) { push(@lines,$_); $body_lines++ } # body |
---|
| 11368 | section_time('SA msg read'); |
---|
| 11369 | |
---|
| 11370 | my($sa_required, $sa_tests); |
---|
| 11371 | my($saved_umask) = umask; |
---|
| 11372 | my($remaining_time) = alarm(0); # check how much time is left |
---|
| 11373 | eval { |
---|
| 11374 | # NOTE ON TIMEOUTS: SpamAssassin may use timer for its own purpose, |
---|
| 11375 | # disabling it before returning. It seems it only uses timer when |
---|
| 11376 | # external tests are enabled, so in order for our timeout to be |
---|
| 11377 | # useful, $sa_local_tests_only needs to be true (e.g. 1). |
---|
| 11378 | local $SIG{ALRM} = sub { |
---|
| 11379 | my($s) = Carp::longmess("SA TIMED OUT, backtrace:"); |
---|
| 11380 | # crop at some rather arbitrary limit |
---|
| 11381 | if (length($s) > 900) { $s = substr($s,0,900-3) . "..." } |
---|
| 11382 | do_log(-1,$s); |
---|
| 11383 | }; |
---|
| 11384 | # prepared to wait no more than n seconds |
---|
| 11385 | alarm($sa_timeout) if $sa_timeout > 0; |
---|
| 11386 | my($mail_obj); my($sa_version) = Mail::SpamAssassin::Version(); |
---|
| 11387 | do_log(5,"calling SA parse, SA version $sa_version"); |
---|
| 11388 | # *** note that $sa_version could be 3.0.1, which is not really numeric! |
---|
| 11389 | if ($sa_version >= 3) { |
---|
| 11390 | $mail_obj = $spamassassin_obj->parse(\@lines); |
---|
| 11391 | } else { # 2.63 or earlier |
---|
| 11392 | $mail_obj = Mail::SpamAssassin::NoMailAudit->new(data => \@lines, |
---|
| 11393 | add_From_line => 0); |
---|
| 11394 | } |
---|
| 11395 | section_time('SA parse'); |
---|
| 11396 | do_log(4,"CALLING SA check"); |
---|
| 11397 | my($per_msg_status); |
---|
| 11398 | { local($1,$2,$3,$4,$5,$6); # avoid Perl 5.8.0 bug, $1 gets tainted |
---|
| 11399 | $per_msg_status = $spamassassin_obj->check($mail_obj); |
---|
| 11400 | } |
---|
| 11401 | my($rem_t) = alarm(0); |
---|
| 11402 | do_log(4,"RETURNED FROM SA check, time left: $rem_t s"); |
---|
| 11403 | |
---|
| 11404 | { local($1,$2,$3,$4); # avoid Perl 5.8.0..5.8.3...? taint bug |
---|
| 11405 | $spam_level = $per_msg_status->get_hits; |
---|
| 11406 | $sa_required = $per_msg_status->get_required_hits; # not used |
---|
| 11407 | $sa_tests = $per_msg_status->get_names_of_tests_hit; |
---|
| 11408 | $spam_report = $per_msg_status->get_report; # taints $1 and $2 ! |
---|
| 11409 | |
---|
| 11410 | # example of how to gather aditional information from SA: |
---|
| 11411 | # my($trusted) = $per_msg_status->_get_tag('RELAYSTRUSTED'); |
---|
| 11412 | # $hdr_edits->append_header('X-TESTING',$trusted); |
---|
| 11413 | |
---|
| 11414 | #Experimental, unfinished: |
---|
| 11415 | # $per_msg_status->rewrite_mail; |
---|
| 11416 | # my($entity) = nomailaudit_to_mime_entity($mail_obj); |
---|
| 11417 | |
---|
| 11418 | $per_msg_status->finish(); |
---|
| 11419 | } |
---|
| 11420 | }; |
---|
| 11421 | section_time('SA check'); |
---|
| 11422 | umask($saved_umask); # SA changes umask to 0077 |
---|
| 11423 | prolong_timer('spam_scan_SA', $remaining_time); # restart the timer |
---|
| 11424 | if ($@ ne '') { # SA timed out? |
---|
| 11425 | chomp($@); |
---|
| 11426 | die "$@\n" if $@ ne "timed out"; |
---|
| 11427 | } |
---|
| 11428 | $sa_tests =~ s/,\s*/,/g; $spam_status = "tests=".$sa_tests; |
---|
| 11429 | |
---|
| 11430 | if ($dspam ne '' && defined $spam_level) { # DSPAM auto-learn |
---|
| 11431 | my($eat,@options); |
---|
| 11432 | @options = (qw(--stdout --mode=tum --user), $daemon_user); # --mode=teft |
---|
| 11433 | if ( $spam_level > 5.0 && $dspam_result eq 'Innocent') { |
---|
| 11434 | $eat = 'SPAM'; push(@options, qw(--class=spam --source=error)); |
---|
| 11435 | # @options = qw(--stdout --addspam); # dspam < 3.0 |
---|
| 11436 | } |
---|
| 11437 | elsif ($spam_level < 1.0 && $dspam_result eq 'Spam') { |
---|
| 11438 | $eat = 'HAM'; push(@options, qw(--class=innocent --source=error)); |
---|
| 11439 | # @options = qw(--stdout --falsepositive); # dspam < 3.0 |
---|
| 11440 | } |
---|
| 11441 | if (defined $eat && $dspam_signature ne '') { |
---|
| 11442 | do_log(2,"DSPAM learn $eat ($spam_level), $dspam_signature"); |
---|
| 11443 | my($proc_fh,$pid) = run_command($dspam_fname, "&1", $dspam, @options); |
---|
| 11444 | while (defined($_ = $proc_fh->getline)) {} # consume possible output |
---|
| 11445 | # my($output) = join('', $proc_fh->getlines); # consume possible output |
---|
| 11446 | my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); |
---|
| 11447 | # do_log(-1,"DSPAM learn $eat response:".$output) if $output ne ''; |
---|
| 11448 | $retval==0 or die ("DSPAM learn $eat FAILED: ".exit_status_str($?,$err)); |
---|
| 11449 | section_time('DSPAM learn'); |
---|
| 11450 | } |
---|
| 11451 | } |
---|
| 11452 | } |
---|
| 11453 | if (defined $dspam_fname) { |
---|
| 11454 | if (($spam_level > 5.0 ? 1 : 0) != ($dspam_result eq 'Spam' ? 1 : 0)) |
---|
| 11455 | { do_log(2,"DSPAM: different opinions: $dspam_result, $spam_level") } |
---|
| 11456 | unlink($dspam_fname) or die "Can't delete file $dspam_fname: $!"; |
---|
| 11457 | } |
---|
| 11458 | do_log(3,"spam_scan: hits=$spam_level $spam_status"); |
---|
| 11459 | ($spam_level, $spam_status, $spam_report); |
---|
| 11460 | } |
---|
| 11461 | |
---|
| 11462 | #sub nomailaudit_to_mime_entity($) { |
---|
| 11463 | # my($mail_obj) = @_; # expect a Mail::SpamAssassin::MsgContainer object |
---|
| 11464 | # my(@m_hdr) = $mail_obj->header; # in array context returns array of lines |
---|
| 11465 | # my($m_body) = $mail_obj->body; # returns array ref |
---|
| 11466 | # my($entity); |
---|
| 11467 | # # make sure _our_ source line number is reported in case of failure |
---|
| 11468 | # eval {$entity = MIME::Entity->build( |
---|
| 11469 | # Type => 'text/plain', Encoding => '-SUGGEST', |
---|
| 11470 | # Data => $m_body); 1} or do {chomp($@); die $@}; |
---|
| 11471 | # my($head) = $entity->head; |
---|
| 11472 | # # insert header fields from template into MIME::Head entity |
---|
| 11473 | # for my $hdr_line (@m_hdr) { |
---|
| 11474 | # # make sure _our_ source line number is reported in case of failure |
---|
| 11475 | # eval {$head->replace($fhead,$fbody); 1} or do {chomp($@); die $@}; |
---|
| 11476 | # } |
---|
| 11477 | # $entity; # return the built MIME::Entity |
---|
| 11478 | #} |
---|
| 11479 | |
---|
| 11480 | 1; |
---|
| 11481 | |
---|
| 11482 | __DATA__ |
---|
| 11483 | # |
---|
| 11484 | package Amavis::Unpackers; |
---|
| 11485 | use strict; |
---|
| 11486 | use re 'taint'; |
---|
| 11487 | |
---|
| 11488 | BEGIN { |
---|
| 11489 | use Exporter (); |
---|
| 11490 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
| 11491 | $VERSION = '2.034'; |
---|
| 11492 | @ISA = qw(Exporter); |
---|
| 11493 | %EXPORT_TAGS = (); |
---|
| 11494 | @EXPORT = (); |
---|
| 11495 | @EXPORT_OK = qw(&init &decompose_part &determine_file_types); |
---|
| 11496 | } |
---|
| 11497 | use Errno qw(ENOENT EACCES); |
---|
| 11498 | use File::Basename qw(basename); |
---|
| 11499 | use Convert::TNEF; |
---|
| 11500 | use Convert::UUlib qw(:constants); |
---|
| 11501 | use Compress::Zlib; |
---|
| 11502 | use Archive::Tar; |
---|
| 11503 | use Archive::Zip qw(:CONSTANTS :ERROR_CODES); |
---|
| 11504 | use File::Copy; |
---|
| 11505 | |
---|
| 11506 | BEGIN { |
---|
| 11507 | import Amavis::Util qw(untaint min max ll do_log retcode exit_status_str |
---|
| 11508 | snmp_count prolong_timer sanitize_str run_command |
---|
| 11509 | rmdir_recursively); |
---|
| 11510 | import Amavis::Conf qw(:platform :confvars :unpack c cr ca); |
---|
| 11511 | import Amavis::Timing qw(section_time); |
---|
| 11512 | import Amavis::Lookup qw(lookup); |
---|
| 11513 | import Amavis::Unpackers::MIME qw(mime_decode); |
---|
| 11514 | import Amavis::Unpackers::NewFilename qw(consumed_bytes); |
---|
| 11515 | } |
---|
| 11516 | |
---|
| 11517 | use subs @EXPORT_OK; |
---|
| 11518 | |
---|
| 11519 | # recursively descend into a directory $dir containing potentially unsafe |
---|
| 11520 | # files with unpredictable names, soft links, etc., rename each regular |
---|
| 11521 | # nonempty file to directory $outdir giving it a generated name, |
---|
| 11522 | # and discard all the rest, including the directory $dir. |
---|
| 11523 | # Return a pair: number of bytes that 'sanitized' files now occupy, |
---|
| 11524 | # and a number of parts objects created. |
---|
| 11525 | # |
---|
| 11526 | sub flatten_and_tidy_dir($$$;$$); # prototype |
---|
| 11527 | sub flatten_and_tidy_dir($$$;$$) { |
---|
| 11528 | my($dir, $outdir, $parent_obj, $item_num_offset, $orig_names) = @_; |
---|
| 11529 | do_log(4, "flatten_and_tidy_dir: processing directory \"$dir\""); |
---|
| 11530 | my($cnt_r,$cnt_u) = (0,0); my($consumed_bytes) = 0; |
---|
| 11531 | local(*DIR); my($f); |
---|
| 11532 | chmod(0750, $dir) or die "Can't change protection of \"$dir\": $!"; |
---|
| 11533 | opendir(DIR, $dir) or die "Can't open directory \"$dir\": $!"; |
---|
| 11534 | my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement; |
---|
| 11535 | while (defined($f = readdir(DIR))) { |
---|
| 11536 | my($msg); my($fname) = "$dir/$f"; |
---|
| 11537 | my($errn) = lstat($fname) ? 0 : 0+$!; |
---|
| 11538 | if ($errn == ENOENT) { $msg = "does not exist" } |
---|
| 11539 | elsif ($errn) { $msg = "inaccessible: $!" } |
---|
| 11540 | if (defined $msg) { die "flatten_and_tidy_dir: \"$fname\" $msg," } |
---|
| 11541 | next if ($f eq '.' || $f eq '..') && -d _; |
---|
| 11542 | my($newpart_obj) = Amavis::Unpackers::Part->new($outdir,$parent_obj); |
---|
| 11543 | $item_num++; |
---|
| 11544 | $newpart_obj->mime_placement(sprintf("%s/%d",$parent_placement, |
---|
| 11545 | $item_num+$item_num_offset) ); |
---|
| 11546 | # save tainted original member name if available, or a tainted file name |
---|
| 11547 | my($original_name) = !ref($orig_names) ? undef : $orig_names->{$f}; |
---|
| 11548 | $newpart_obj->name_declared(defined $original_name ? $original_name : $f); |
---|
| 11549 | # untaint, but if $dir happens to still be tainted, we want to know and die |
---|
| 11550 | $fname = $dir.'/'.untaint($f); |
---|
| 11551 | if (-d _) { |
---|
| 11552 | $newpart_obj->attributes_add('D'); |
---|
| 11553 | my($bytes,$cnt) = flatten_and_tidy_dir($fname, $outdir, $parent_obj, |
---|
| 11554 | $item_num+$item_num_offset, $orig_names); |
---|
| 11555 | $consumed_bytes += $bytes; $item_num += $cnt; |
---|
| 11556 | } elsif (-l _) { |
---|
| 11557 | $cnt_u++; $newpart_obj->attributes_add('L'); |
---|
| 11558 | unlink($fname) or die "Can't remove soft link \"$fname\": $!"; |
---|
| 11559 | } elsif (!-f _) { |
---|
| 11560 | do_log(4, "flatten_and_tidy_dir: NONREGULAR FILE \"$fname\""); |
---|
| 11561 | $cnt_u++; $newpart_obj->attributes_add('S'); |
---|
| 11562 | unlink($fname) or die "Can't remove nonregular file \"$fname\": $!"; |
---|
| 11563 | } elsif (-z _) { |
---|
| 11564 | $cnt_u++; |
---|
| 11565 | unlink($fname) or die "Can't remove empty file \"$fname\": $!"; |
---|
| 11566 | } else { |
---|
| 11567 | chmod(0750, $fname) |
---|
| 11568 | or die "Can't change protection of file \"$fname\": $!"; |
---|
| 11569 | my($size) = 0 + (-s _); |
---|
| 11570 | $newpart_obj->size($size); |
---|
| 11571 | $consumed_bytes += $size; |
---|
| 11572 | my($newpart) = $newpart_obj->full_name; |
---|
| 11573 | ll(5) && do_log(5, |
---|
| 11574 | sprintf("flatten_and_tidy_dir: renaming \"%s\"%s to %s", $fname, |
---|
| 11575 | !defined $original_name ? '' : " ($original_name)", $newpart)); |
---|
| 11576 | $cnt_r++; |
---|
| 11577 | rename($fname, $newpart) |
---|
| 11578 | or die "Can't rename \"$fname\" to $newpart: $!"; |
---|
| 11579 | } |
---|
| 11580 | } |
---|
| 11581 | closedir(DIR) or die "Can't close directory \"$dir\": $!"; |
---|
| 11582 | rmdir($dir) or die "Can't remove directory \"$dir\": $!"; |
---|
| 11583 | section_time("ren$cnt_r-unl$cnt_u-files$item_num"); |
---|
| 11584 | ($consumed_bytes, $item_num); |
---|
| 11585 | } |
---|
| 11586 | |
---|
| 11587 | # call 'file(1)' utility for each part, |
---|
| 11588 | # and associate (save) full and short types with each part |
---|
| 11589 | # |
---|
| 11590 | sub determine_file_types($$) { |
---|
| 11591 | my($tempdir, $partslist_ref) = @_; |
---|
| 11592 | $file ne '' or die "Unix utility file(1) not available, but is needed"; |
---|
| 11593 | my($cwd) = "$tempdir/parts"; |
---|
| 11594 | my(@part_list) = grep { $_->exists } @$partslist_ref; |
---|
| 11595 | if (!@part_list) { do_log(5, "no parts, file(1) not called") } |
---|
| 11596 | else { |
---|
| 11597 | local($1,$2); # avoid Perl taint bug (5.8.3), $cwd and $arg are not tainted |
---|
| 11598 | # but $arg becomes tainted because $1 is tainted from before |
---|
| 11599 | my(@file_list); |
---|
| 11600 | for my $part (@part_list) { |
---|
| 11601 | my($arg) = $part->full_name; |
---|
| 11602 | $arg =~ s{^\Q$cwd\E/(.*)\z}{$1}s; # remove cwd if possible |
---|
| 11603 | push(@file_list, $arg); |
---|
| 11604 | } |
---|
| 11605 | chdir($cwd) or die "Can't chdir to $cwd: $!"; |
---|
| 11606 | my($proc_fh,$pid) = run_command(undef, "&1", $file, @file_list); |
---|
| 11607 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
| 11608 | local($_); my($index) = 0; |
---|
| 11609 | while (defined($_ = $proc_fh->getline)) { |
---|
| 11610 | chomp; |
---|
| 11611 | do_log(5, "result line from file(1): ".$_); |
---|
| 11612 | if ($index > $#file_list) { |
---|
| 11613 | do_log(-1, "NOTICE: Skipping extra output from file(1): $_"); |
---|
| 11614 | } else { |
---|
| 11615 | my($part) = $part_list[$index]; # walk through @part_list in sync |
---|
| 11616 | my($expect) = $file_list[$index]; # walk through @file_list in sync |
---|
| 11617 | if (!/^(\Q$expect\E):[ \t]*(.*)\z/s) { # split file name from type |
---|
| 11618 | do_log(-1,"NOTICE: Skipping bad output from file(1) ". |
---|
| 11619 | "at [$index, $expect], got: $_"); |
---|
| 11620 | } else { |
---|
| 11621 | my($type_short); my($actual_name) = $1; my($type_long) = $2; |
---|
| 11622 | $type_short = lookup(0,$type_long,@map_full_type_to_short_type_maps); |
---|
| 11623 | do_log(4, sprintf("File-type of %s: %s%s", |
---|
| 11624 | $part->base_name, $type_long, |
---|
| 11625 | (!defined $type_short ? '' |
---|
| 11626 | : !ref $type_short ? "; ($type_short)" |
---|
| 11627 | : '; (' . join(', ',@$type_short) . ')' |
---|
| 11628 | ) )); |
---|
| 11629 | $part->type_long($type_long); $part->type_short($type_short); |
---|
| 11630 | $part->attributes_add('C') |
---|
| 11631 | if !ref($type_short) ? $type_short eq 'pgp' # encrypted? |
---|
| 11632 | : grep {$_ eq 'pgp'} @$type_short; |
---|
| 11633 | $index++; |
---|
| 11634 | } |
---|
| 11635 | } |
---|
| 11636 | } |
---|
| 11637 | if ($index < @part_list) { |
---|
| 11638 | die sprintf("parsing file(1) results - missing last %d results", |
---|
| 11639 | @part_list - $index); |
---|
| 11640 | } |
---|
| 11641 | my($err); $proc_fh->close or $err = $!; |
---|
| 11642 | $?==0 or die ("'file' utility ($file) failed, ".exit_status_str($?,$err)); |
---|
| 11643 | section_time(sprintf('get-file-type%d', scalar(@part_list))); |
---|
| 11644 | } |
---|
| 11645 | } |
---|
| 11646 | |
---|
| 11647 | sub decompose_mail($$) { |
---|
| 11648 | my($tempdir,$file_generator_object) = @_; |
---|
| 11649 | |
---|
| 11650 | my($hold); my(@parts); my($depth) = 1; my($any_undecipherable) = 0; |
---|
| 11651 | my($which_section) = "parts_decode"; |
---|
| 11652 | # fetch all not-yet-visited part names, and start a new cycle |
---|
| 11653 | TIER: |
---|
| 11654 | while (@parts = @{$file_generator_object->parts_list}) { |
---|
| 11655 | if ($MAXLEVELS && $depth > $MAXLEVELS) { |
---|
| 11656 | $hold = "Maximum decoding depth ($MAXLEVELS) exceeded"; |
---|
| 11657 | last; |
---|
| 11658 | } |
---|
| 11659 | $file_generator_object->parts_list_reset; # new names cycle |
---|
| 11660 | # clip to avoid very long log entries |
---|
| 11661 | my(@chopped_parts) = @parts > 5 ? @parts[0..4] : @parts; |
---|
| 11662 | ll(4) && do_log(4,sprintf("decode_parts: level=%d, #parts=%d : %s", |
---|
| 11663 | $depth, scalar(@parts), |
---|
| 11664 | join(', ', (map { $_->base_name } @chopped_parts), |
---|
| 11665 | (@chopped_parts >= @parts ? () : "...")) )); |
---|
| 11666 | for my $part (@parts) { # test for existence of all expected files |
---|
| 11667 | my($fname) = $part->full_name; |
---|
| 11668 | my($errn) = lstat($fname) ? 0 : 0+$!; |
---|
| 11669 | if ($errn == ENOENT) { |
---|
| 11670 | $part->exists(0); |
---|
| 11671 | # $part->type_short('no-file') if !defined $part->type_short; |
---|
| 11672 | } elsif ($errn) { |
---|
| 11673 | die "decompose_mail: inaccessible file $fname: $!"; |
---|
| 11674 | } elsif (!-f _) { # not a regular file |
---|
| 11675 | my($what) = -l _ ? 'symlink' : -d _ ? 'directory' : 'non-regular file'; |
---|
| 11676 | do_log(-1, "WARN: decompose_mail: removing unexpected $what $fname"); |
---|
| 11677 | if (-d _) { rmdir_recursively($fname) } |
---|
| 11678 | else { unlink($fname) or die "Can't delete $what $fname: $!" } |
---|
| 11679 | $part->exists(0); |
---|
| 11680 | $part->type_short(-l _ ? 'symlink' : -d _ ? 'dir' : 'special') |
---|
| 11681 | if !defined $part->type_short; |
---|
| 11682 | } elsif (-z _) { # empty file |
---|
| 11683 | unlink($fname) or die "Can't remove \"$fname\": $!"; |
---|
| 11684 | $part->exists(0); |
---|
| 11685 | $part->type_short('empty') if !defined $part->type_short; |
---|
| 11686 | $part->type_long('empty') if !defined $part->type_long; |
---|
| 11687 | } else { |
---|
| 11688 | $part->exists(1); |
---|
| 11689 | } |
---|
| 11690 | } |
---|
| 11691 | determine_file_types($tempdir, \@parts); |
---|
| 11692 | for my $part (@parts) { |
---|
| 11693 | if ($part->exists && !defined($hold)) |
---|
| 11694 | { $hold = decompose_part($part, $tempdir) } |
---|
| 11695 | $any_undecipherable++ if grep {$_ eq 'U'} @{ $part->attributes || [] }; |
---|
| 11696 | } |
---|
| 11697 | last TIER if defined $hold; |
---|
| 11698 | $depth++; |
---|
| 11699 | } |
---|
| 11700 | section_time($which_section); prolong_timer($which_section); |
---|
| 11701 | ($hold, $any_undecipherable); |
---|
| 11702 | } |
---|
| 11703 | |
---|
| 11704 | # Decompose the part |
---|
| 11705 | sub decompose_part($$) { |
---|
| 11706 | my($part, $tempdir) = @_; |
---|
| 11707 | my($hold); |
---|
| 11708 | my($none_called); |
---|
| 11709 | # possible return values from eval: |
---|
| 11710 | # 0 - truly atomic, or unknown or archiver failure; consider atomic |
---|
| 11711 | # 1 - some archiver format, successfully unpacked, result replaces original |
---|
| 11712 | # 2 - probably unpacked, but keep the original (eg self-extracting archive) |
---|
| 11713 | my($sts) = eval { |
---|
| 11714 | my($type_short) = $part->type_short; |
---|
| 11715 | my(@ts) = !defined $type_short ? () |
---|
| 11716 | : !ref $type_short ? ($type_short) : @$type_short; |
---|
| 11717 | return 0 if !@ts; # consider atomic if unknown (returns from eval) |
---|
| 11718 | snmp_count("OpsDecType-".join('.',@ts)); |
---|
| 11719 | |
---|
| 11720 | grep(/^mail\z/,@ts) && return do {mime_decode($part,$tempdir,$part); 2}; |
---|
| 11721 | grep(/^(asc|uue|hqx|ync)\z/,@ts) && return do_ascii($part,$tempdir); |
---|
| 11722 | grep(/^F\z/,@ts) && defined $unfreeze |
---|
| 11723 | && return do_uncompress($part,$tempdir,$unfreeze); |
---|
| 11724 | grep(/^Z\z/,@ts) && defined $uncompress |
---|
| 11725 | && return do_uncompress($part,$tempdir,$uncompress); |
---|
| 11726 | grep(/^bz2?\z/,@ts) && defined $bzip2 |
---|
| 11727 | && return do_uncompress($part,$tempdir,"$bzip2 -d"); |
---|
| 11728 | grep(/^gz\z/,@ts) && defined $gzip |
---|
| 11729 | && return do_uncompress($part,$tempdir,"$gzip -d"); |
---|
| 11730 | grep(/^gz\z/,@ts) && return do_gunzip($part,$tempdir); # fallback |
---|
| 11731 | grep(/^lzo\z/,@ts) && defined $lzop |
---|
| 11732 | && return do_uncompress($part,$tempdir,"$lzop -d"); |
---|
| 11733 | grep(/^rpm\z/,@ts) && defined $rpm2cpio && (defined $pax || defined $cpio) |
---|
| 11734 | && return do_uncompress($part,$tempdir,$rpm2cpio); |
---|
| 11735 | grep(/^(cpio|tar)\z/,@ts) && (defined $pax || defined $cpio) |
---|
| 11736 | && return do_pax_cpio($part,$tempdir, $pax || $cpio); |
---|
| 11737 | grep(/^tar\z/,@ts) && return do_tar($part,$tempdir); # fallback |
---|
| 11738 | grep(/^deb\z/,@ts) && defined $ar |
---|
| 11739 | && return do_ar($part,$tempdir); |
---|
| 11740 | # grep(/^a\z/,@ts) && defined $ar # unpacking .a seems like overkill |
---|
| 11741 | # && return do_ar($part,$tempdir); |
---|
| 11742 | grep(/^zip\z/,@ts) && return do_unzip($part,$tempdir); |
---|
| 11743 | grep(/^rar\z/,@ts) && defined $unrar |
---|
| 11744 | && return do_unrar($part,$tempdir); |
---|
| 11745 | grep(/^(lha|lzh)\z/,@ts) && defined $lha |
---|
| 11746 | && return do_lha($part,$tempdir); |
---|
| 11747 | grep(/^arc\z/,@ts) && defined $arc |
---|
| 11748 | && return do_arc($part,$tempdir); |
---|
| 11749 | grep(/^arj\z/,@ts) && defined $unarj |
---|
| 11750 | && return do_unarj($part,$tempdir); |
---|
| 11751 | grep(/^zoo\z/,@ts) && defined $zoo |
---|
| 11752 | && return do_zoo($part,$tempdir); |
---|
| 11753 | grep(/^cab\z/,@ts) && defined $cabextract |
---|
| 11754 | && return do_cabextract($part,$tempdir); |
---|
| 11755 | grep(/^doc\z/,@ts) && defined $ripole |
---|
| 11756 | && return do_ole($part,$tempdir); |
---|
| 11757 | grep(/^tnef\z/,@ts) && return do_tnef($part,$tempdir); |
---|
| 11758 | grep(/^exe\z/,@ts) && return do_executable($part,$tempdir); |
---|
| 11759 | |
---|
| 11760 | # Falling through (e.g. HTML) - no match, consider atomic |
---|
| 11761 | $none_called = 1; |
---|
| 11762 | return 0; # returns from eval |
---|
| 11763 | }; |
---|
| 11764 | if ($@ ne '') { |
---|
| 11765 | chomp($@); |
---|
| 11766 | if ($@ =~ /^Exceeded storage quota/ || |
---|
| 11767 | $@ =~ /^Maximum number of files.*exceeded/) { $hold = $@ } |
---|
| 11768 | else { |
---|
| 11769 | do_log(-1,sprintf("Decoding of %s (%s) failed, leaving it unpacked: %s", |
---|
| 11770 | $part->base_name, $part->type_long, $@)); |
---|
| 11771 | } |
---|
| 11772 | $sts = 2; |
---|
| 11773 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; # just in case |
---|
| 11774 | } |
---|
| 11775 | if ($sts == 1 && lookup(0,$part->type_long, @keep_decoded_original_maps)) { |
---|
| 11776 | # don't trust this file type or unpacker, |
---|
| 11777 | # keep both the original and the unpacked file |
---|
| 11778 | ll(4) && do_log(4,sprintf("file type is %s, retain original %s", |
---|
| 11779 | $part->type_long, $part->base_name)); |
---|
| 11780 | $sts = 2; |
---|
| 11781 | } |
---|
| 11782 | if ($sts == 1) { |
---|
| 11783 | ll(5) && do_log(5, "decompose_part: deleting ".$part->full_name); |
---|
| 11784 | unlink($part->full_name) |
---|
| 11785 | or die sprintf("Can't unlink %s: %s", $part->full_name, $!); |
---|
| 11786 | } |
---|
| 11787 | ll(4) && do_log(4,sprintf("decompose_part: %s - %s", $part->base_name, |
---|
| 11788 | ['atomic','archive, unpacked','source retained']->[$sts])); |
---|
| 11789 | section_time('decompose_part') unless $none_called; |
---|
| 11790 | $hold; |
---|
| 11791 | } |
---|
| 11792 | |
---|
| 11793 | # |
---|
| 11794 | # Uncompression/unarchiving routines |
---|
| 11795 | # Possible return codes: |
---|
| 11796 | # 0 - truly atomic, or unknown or archiver failure; consider atomic |
---|
| 11797 | # 1 - some archiver format, successfully unpacked, result replaces original |
---|
| 11798 | # 2 - probably unpacked, but keep the original (eg self-extracting archive) |
---|
| 11799 | |
---|
| 11800 | # if ASCII text, try multiple decoding methods as provided by UUlib |
---|
| 11801 | # (uuencoded, xxencoded, BinHex, yEnc, Base64, Quoted-Printable) |
---|
| 11802 | sub do_ascii($$) { |
---|
| 11803 | my($part, $tempdir) = @_; |
---|
| 11804 | |
---|
| 11805 | snmp_count('OpsDecByUUlibAttempt'); |
---|
| 11806 | my($envtmpdir_changed); |
---|
| 11807 | # prevent uunconc.c/UUDecode() from trying to create temp file in '/' |
---|
| 11808 | if ($ENV{TMPDIR} eq '') { $ENV{TMPDIR} = $TEMPBASE; $envtmpdir_changed = 1 } |
---|
| 11809 | |
---|
| 11810 | my($any_errors,$any_decoded); |
---|
| 11811 | eval { # must not go away without calling Convert::UUlib::CleanUp! |
---|
| 11812 | my($sts,$count); |
---|
| 11813 | $sts = Convert::UUlib::Initialize(); |
---|
| 11814 | $sts==RET_OK or die "Convert::UUlib::Initialize failed: " |
---|
| 11815 | . Convert::UUlib::strerror($sts); |
---|
| 11816 | my($uulib_version) = Convert::UUlib::GetOption(OPT_VERSION); |
---|
| 11817 | !Convert::UUlib::SetOption(OPT_IGNMODE,1) or die "bad uulib OPT_IGNMODE"; |
---|
| 11818 | # !Convert::UUlib::SetOption(OPT_DESPERATE,1) or die "bad uulib OPT_DESPERATE"; |
---|
| 11819 | ($sts, $count) = Convert::UUlib::LoadFile($part->full_name); |
---|
| 11820 | if ($sts != RET_OK) { |
---|
| 11821 | my($errmsg) = Convert::UUlib::strerror($sts) . ": $!"; |
---|
| 11822 | $errmsg .= ", (???" |
---|
| 11823 | . Convert::UUlib::strerror(Convert::UUlib::GetOption(OPT_ERRNO))."???)" |
---|
| 11824 | if $sts == RET_IOERR; |
---|
| 11825 | die "Convert::UUlib::LoadFile (uulib V$uulib_version) failed: $errmsg"; |
---|
| 11826 | } |
---|
| 11827 | ll(4) && do_log(4,sprintf( |
---|
| 11828 | "do_ascii: Decoding part %s (%d items), uulib V%s", |
---|
| 11829 | $part->base_name, $count, $uulib_version)); |
---|
| 11830 | my($uu); |
---|
| 11831 | my($item_num) = 0; my($parent_placement) = $part->mime_placement; |
---|
| 11832 | for (my($j) = 0; $uu = Convert::UUlib::GetFileListItem($j); $j++) { |
---|
| 11833 | $item_num++; |
---|
| 11834 | ll(4) && do_log(4,sprintf( |
---|
| 11835 | "do_ascii(%d): state=0x%02x, enc=%s%s, est.size=%s, name=%s", |
---|
| 11836 | $j, $uu->state, Convert::UUlib::strencoding($uu->uudet), |
---|
| 11837 | ($uu->mimetype ne '' ? ", mimetype=" . $uu->mimetype : ''), |
---|
| 11838 | $uu->size, $uu->filename)); |
---|
| 11839 | if (!($uu->state & FILE_OK)) { |
---|
| 11840 | $any_errors++; |
---|
| 11841 | do_log(1,"do_ascii: Convert::UUlib info: $j not decodable, ".$uu->state); |
---|
| 11842 | } else { |
---|
| 11843 | my($newpart_obj)=Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
| 11844 | $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
| 11845 | $newpart_obj->name_declared($uu->filename); |
---|
| 11846 | my($newpart) = $newpart_obj->full_name; |
---|
| 11847 | $! = undef; |
---|
| 11848 | $sts = $uu->decode($newpart); # decode to file $newpart |
---|
| 11849 | my($err_decode) = "$!"; |
---|
| 11850 | chmod(0750, $newpart) or $! == ENOENT # chmod, don't panic if no file |
---|
| 11851 | or die "Can't change protection of \"$newpart\": $!"; |
---|
| 11852 | my($statmsg); |
---|
| 11853 | my($errn) = lstat($newpart) ? 0 : 0+$!; |
---|
| 11854 | if ($errn == ENOENT) { $statmsg = "does not exist" } |
---|
| 11855 | elsif ($errn) { $statmsg = "inaccessible: $!" } |
---|
| 11856 | elsif ( -l _) { $statmsg = "is a symlink" } |
---|
| 11857 | elsif ( -d _) { $statmsg = "is a directory" } |
---|
| 11858 | elsif (!-f _) { $statmsg = "not a regular file" } |
---|
| 11859 | if (defined $statmsg) { $statmsg = "; file status: $newpart $statmsg" } |
---|
| 11860 | my($size) = 0 + (-s _); |
---|
| 11861 | $newpart_obj->size($size); |
---|
| 11862 | consumed_bytes($size, 'do_ascii'); |
---|
| 11863 | if ($sts == RET_OK && $errn==0) { |
---|
| 11864 | $any_decoded++; |
---|
| 11865 | do_log(4,"do_ascii: RET_OK" . $statmsg) if defined $statmsg; |
---|
| 11866 | } elsif ($sts == RET_NODATA || $sts == RET_NOEND) { |
---|
| 11867 | $any_errors++; |
---|
| 11868 | do_log(-1,"do_ascii: Convert::UUlib error: " |
---|
| 11869 | . Convert::UUlib::strerror($sts) . $statmsg); |
---|
| 11870 | } else { |
---|
| 11871 | $any_errors++; |
---|
| 11872 | my($errmsg) = Convert::UUlib::strerror($sts) . ":: $err_decode"; |
---|
| 11873 | $errmsg .= ", " . Convert::UUlib::strerror( |
---|
| 11874 | Convert::UUlib::GetOption(OPT_ERRNO) ) if $sts == RET_IOERR; |
---|
| 11875 | die ("Convert::UUlib failed: " . $errmsg . $statmsg); |
---|
| 11876 | } |
---|
| 11877 | } |
---|
| 11878 | } |
---|
| 11879 | }; |
---|
| 11880 | my($eval_stat) = $@; |
---|
| 11881 | Convert::UUlib::CleanUp(); |
---|
| 11882 | snmp_count('OpsDecByUUlib') if $any_decoded; |
---|
| 11883 | delete $ENV{TMPDIR} if $envtmpdir_changed; # restore |
---|
| 11884 | if ($eval_stat ne '') { chomp($eval_stat); die "do_ascii: $eval_stat\n" } |
---|
| 11885 | ($any_decoded && !$any_errors) ? 1 : $any_errors ? 2 : 0; |
---|
| 11886 | } |
---|
| 11887 | |
---|
| 11888 | # use Archive-Zip |
---|
| 11889 | sub do_unzip($$) { |
---|
| 11890 | my($part, $tempdir) = @_; |
---|
| 11891 | |
---|
| 11892 | ll(4) && do_log(4, "Unzipping " . $part->base_name); |
---|
| 11893 | snmp_count('OpsDecByArZipAttempt'); |
---|
| 11894 | my($zip) = Archive::Zip->new; |
---|
| 11895 | my(@err_nm) = qw(AZ_OK AZ_STREAM_END AZ_ERROR AZ_FORMAT_ERROR AZ_IO_ERROR); |
---|
| 11896 | |
---|
| 11897 | # Need to set up a temporary minimal error handler |
---|
| 11898 | # because we now test inside do_unzip whether the $part |
---|
| 11899 | # in question is a zip archive |
---|
| 11900 | Archive::Zip::setErrorHandler(sub { return 5 }); |
---|
| 11901 | my($sts) = $zip->read($part->full_name); |
---|
| 11902 | Archive::Zip::setErrorHandler(sub { die @_ }); |
---|
| 11903 | if ($sts != AZ_OK) { |
---|
| 11904 | do_log(4, "do_unzip: not a zip: $err_nm[$sts] ($sts)"); |
---|
| 11905 | return 0; |
---|
| 11906 | } |
---|
| 11907 | my($any_unsupp_compmeth,$any_zero_length); |
---|
| 11908 | my($encryptedcount,$extractedcount) = (0,0); |
---|
| 11909 | my($item_num) = 0; my($parent_placement) = $part->mime_placement; |
---|
| 11910 | for my $mem ($zip->members()) { |
---|
| 11911 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
| 11912 | $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
| 11913 | $newpart_obj->name_declared($mem->fileName); |
---|
| 11914 | my($compmeth) = $mem->compressionMethod; |
---|
| 11915 | if ($compmeth != COMPRESSION_DEFLATED && $compmeth != COMPRESSION_STORED) { |
---|
| 11916 | $any_unsupp_compmeth = $compmeth; |
---|
| 11917 | $newpart_obj->attributes_add('U'); |
---|
| 11918 | } elsif ($mem->isEncrypted) { |
---|
| 11919 | $encryptedcount++; |
---|
| 11920 | $newpart_obj->attributes_add('U','C'); |
---|
| 11921 | } elsif ($mem->isDirectory) { |
---|
| 11922 | $newpart_obj->attributes_add('D'); |
---|
| 11923 | } else { |
---|
| 11924 | # want to read uncompressed - set to COMPRESSION_STORED |
---|
| 11925 | my($oldc) = $mem->desiredCompressionMethod(COMPRESSION_STORED); |
---|
| 11926 | $sts = $mem->rewindData(); |
---|
| 11927 | $sts == AZ_OK or die sprintf("%s: error rew. member data: %s (%s)", |
---|
| 11928 | $part->base_name, $err_nm[$sts], $sts); |
---|
| 11929 | my($newpart) = $newpart_obj->full_name; |
---|
| 11930 | my($outpart) = IO::File->new; |
---|
| 11931 | $outpart->open($newpart,'>') or die "Can't create file $newpart: $!"; |
---|
| 11932 | binmode($outpart) or die "Can't set file $newpart to binmode: $!"; |
---|
| 11933 | my($size) = 0; |
---|
| 11934 | while ($sts == AZ_OK) { |
---|
| 11935 | my($buf_ref); |
---|
| 11936 | ($buf_ref, $sts) = $mem->readChunk(); |
---|
| 11937 | $sts == AZ_OK || $sts == AZ_STREAM_END |
---|
| 11938 | or die sprintf("%s: error reading member: %s (%s)", |
---|
| 11939 | $part->base_name, $err_nm[$sts], $sts); |
---|
| 11940 | my($buf_len) = length($$buf_ref); |
---|
| 11941 | if ($buf_len > 0) { |
---|
| 11942 | $size += $buf_len; |
---|
| 11943 | $outpart->print($$buf_ref) or die "Can't write to $newpart: $!"; |
---|
| 11944 | consumed_bytes($buf_len, 'do_unzip'); |
---|
| 11945 | } |
---|
| 11946 | } |
---|
| 11947 | $any_zero_length = 1 if $size == 0; |
---|
| 11948 | $newpart_obj->size($size); |
---|
| 11949 | $outpart->close or die "Can't close $newpart: $!"; |
---|
| 11950 | $mem->desiredCompressionMethod($oldc); |
---|
| 11951 | $mem->endRead(); |
---|
| 11952 | $extractedcount++; |
---|
| 11953 | } |
---|
| 11954 | } |
---|
| 11955 | snmp_count('OpsDecByArZip'); |
---|
| 11956 | my($retval) = 1; |
---|
| 11957 | if ($any_unsupp_compmeth) { |
---|
| 11958 | $retval = 2; |
---|
| 11959 | do_log(-1, sprintf("do_unzip: %s, unsupported compr. method: %s", |
---|
| 11960 | $part->base_name, $any_unsupp_compmeth)); |
---|
| 11961 | } elsif ($any_zero_length) { # possible zip vulnerability exploit |
---|
| 11962 | $retval = 2; |
---|
| 11963 | do_log(1, sprintf("do_unzip: %s, zero length members, archive retained", |
---|
| 11964 | $part->base_name)); |
---|
| 11965 | } elsif ($encryptedcount) { |
---|
| 11966 | $retval = 2; |
---|
| 11967 | do_log(1, sprintf( |
---|
| 11968 | "do_unzip: %s, %d members are encrypted, %s extracted, archive retained", |
---|
| 11969 | $part->base_name, $encryptedcount, |
---|
| 11970 | !$extractedcount ? 'none' : $extractedcount)); |
---|
| 11971 | } |
---|
| 11972 | $retval; |
---|
| 11973 | } |
---|
| 11974 | |
---|
| 11975 | # use external decompressor program from the gzip/bzip2/compress family |
---|
| 11976 | # (there *is* a perl module for bzip2, but is not ready for prime time) |
---|
| 11977 | sub do_uncompress($$$) { |
---|
| 11978 | my($part, $tempdir, $decompressor) = @_; |
---|
| 11979 | ll(4) && do_log(4,sprintf("do_uncompress %s by %s", |
---|
| 11980 | $part->base_name,$decompressor)); |
---|
| 11981 | my($decompressor_name) = basename((split(' ',$decompressor))[0]); |
---|
| 11982 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
| 11983 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
| 11984 | $newpart_obj->mime_placement($part->mime_placement."/1"); |
---|
| 11985 | my($newpart) = $newpart_obj->full_name; |
---|
| 11986 | my($type_short, $name_declared) = ($part->type_short, $part->name_declared); |
---|
| 11987 | my(@rn); # collect recommended file names |
---|
| 11988 | push(@rn,$1) |
---|
| 11989 | if $part->type_long =~ /^\S+\s+compressed data, was "(.+)"(\z|, from\b)/; |
---|
| 11990 | for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) { |
---|
| 11991 | next if $name_d eq ''; |
---|
| 11992 | my($name) = $name_d; |
---|
| 11993 | for (!ref $type_short ? ($type_short) : @$type_short) { |
---|
| 11994 | /^F\z/ and $name=~s/\.F\z//; |
---|
| 11995 | /^Z\z/ and $name=~s/\.Z\z// || $name=~s/\.tg?z\z/.tar/; |
---|
| 11996 | /^gz\z/ and $name=~s/\.gz\z// || $name=~s/\.tgz\z/.tar/; |
---|
| 11997 | /^bz\z/ and $name=~s/\.bz\z// || $name=~s/\.tbz\z/.tar/; |
---|
| 11998 | /^bz2\z/ and $name=~s/\.bz2?\z// || $name=~s/\.tbz\z/.tar/; |
---|
| 11999 | /^lzo\z/ and $name=~s/\.lzo\z//; |
---|
| 12000 | /^rpm\z/ and $name=~s/\.rpm\z/.cpio/; |
---|
| 12001 | } |
---|
| 12002 | push(@rn,$name) if !grep { $_ eq $name } @rn; |
---|
| 12003 | } |
---|
| 12004 | $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn; |
---|
| 12005 | my($proc_fh,$pid) = |
---|
| 12006 | run_command($part->full_name, undef, split(' ',$decompressor)); |
---|
| 12007 | my($rv,$rerr) = run_command_copy($newpart,$proc_fh); |
---|
| 12008 | if ($rv) { |
---|
| 12009 | # unlink($newpart) or die "Can't unlink $newpart: $!"; |
---|
| 12010 | die sprintf('Error running decompressor %s on %s, %s', |
---|
| 12011 | $decompressor, $part->base_name, exit_status_str($rv,$rerr)); |
---|
| 12012 | } |
---|
| 12013 | 1; |
---|
| 12014 | } |
---|
| 12015 | |
---|
| 12016 | # use Zlib to inflate |
---|
| 12017 | sub do_gunzip($$) { |
---|
| 12018 | my($part, $tempdir) = @_; |
---|
| 12019 | do_log(4, "Inflating gzip archive " . $part->base_name); |
---|
| 12020 | snmp_count('OpsDecByZlib'); |
---|
| 12021 | my($gz) = gzopen($part->full_name, "rb") |
---|
| 12022 | or die sprintf("do_gunzip: Error opening %s: %s", |
---|
| 12023 | $part->full_name, $gzerrno); |
---|
| 12024 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
| 12025 | $newpart_obj->mime_placement($part->mime_placement."/1"); |
---|
| 12026 | my($newpart) = $newpart_obj->full_name; |
---|
| 12027 | my($outpart) = IO::File->new; |
---|
| 12028 | $outpart->open($newpart,'>') or die "Can't create file $newpart: $!"; |
---|
| 12029 | binmode($outpart) or die "Can't set file $newpart to binmode: $!"; |
---|
| 12030 | my($buffer); my($size) = 0; |
---|
| 12031 | while ($gz->gzread($buffer) > 0) { |
---|
| 12032 | $outpart->print($buffer) or die "Can't write to $newpart: $!"; |
---|
| 12033 | $size += length($buffer); |
---|
| 12034 | consumed_bytes(length($buffer), 'do_gunzip'); |
---|
| 12035 | } |
---|
| 12036 | $newpart_obj->size($size); |
---|
| 12037 | $outpart->close or die "Can't close $newpart: $!"; |
---|
| 12038 | my(@rn); # collect recommended file name |
---|
| 12039 | my($name_declared) = $part->name_declared; |
---|
| 12040 | for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) { |
---|
| 12041 | next if $name_d eq ''; |
---|
| 12042 | my($name) = $name_d; |
---|
| 12043 | $name=~s/\.(gz|Z)\z// || $name=~s/\.tgz\z/.tar/; |
---|
| 12044 | push(@rn,$name) if !grep { $_ eq $name } @rn; |
---|
| 12045 | } |
---|
| 12046 | $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn; |
---|
| 12047 | if ($gzerrno != Z_STREAM_END) { |
---|
| 12048 | do_log(-1,sprintf("do_gunzip: Error reading %s: %s", |
---|
| 12049 | $part->full_name, $gzerrno)); |
---|
| 12050 | unlink($newpart) or die "Can't unlink $newpart: $!"; |
---|
| 12051 | $newpart_obj->size(undef); |
---|
| 12052 | $gz->gzclose(); |
---|
| 12053 | return 0; |
---|
| 12054 | } |
---|
| 12055 | $gz->gzclose(); |
---|
| 12056 | 1; |
---|
| 12057 | } |
---|
| 12058 | |
---|
| 12059 | # untar any tar archives with Archive-Tar, extract each file individually |
---|
| 12060 | sub do_tar($$) { |
---|
| 12061 | my($part, $tempdir) = @_; |
---|
| 12062 | snmp_count('OpsDecByArTar'); |
---|
| 12063 | # Work around bug in Archive-Tar |
---|
| 12064 | my $tar = eval { Archive::Tar->new($part->full_name) }; |
---|
| 12065 | if (!defined($tar)) { |
---|
| 12066 | chomp($@); |
---|
| 12067 | do_log(4, sprintf("Faulty archive %s: %s", $part->full_name, $@)); |
---|
| 12068 | return 0; |
---|
| 12069 | } |
---|
| 12070 | do_log(4,"Untarring ".$part->base_name); |
---|
| 12071 | my($item_num) = 0; my($parent_placement) = $part->mime_placement; |
---|
| 12072 | my(@list) = $tar->list_files(); |
---|
| 12073 | for (@list) { |
---|
| 12074 | next if /\/\z/; # ignore directories |
---|
| 12075 | # this is bad (reads whole file into scalar) |
---|
| 12076 | # need some error handling, too |
---|
| 12077 | my $data = $tar->get_content($_); |
---|
| 12078 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
| 12079 | $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
| 12080 | my($newpart) = $newpart_obj->full_name; |
---|
| 12081 | my($outpart) = IO::File->new; |
---|
| 12082 | $outpart->open($newpart,'>') or die "Can't create file $newpart: $!"; |
---|
| 12083 | binmode($outpart) or die "Can't set file $newpart to binmode: $!"; |
---|
| 12084 | $outpart->print($data) or die "Can't write to $newpart: $!"; |
---|
| 12085 | $newpart_obj->size(length($data)); |
---|
| 12086 | consumed_bytes(length($data), 'do_tar'); |
---|
| 12087 | $outpart->close or die "Can't close $newpart: $!"; |
---|
| 12088 | } |
---|
| 12089 | 1; |
---|
| 12090 | } |
---|
| 12091 | |
---|
| 12092 | # use external program to expand RAR archives |
---|
| 12093 | sub do_unrar($$) { |
---|
| 12094 | my($part, $tempdir) = @_; |
---|
| 12095 | ll(4) && do_log(4, "Attempting to expand RAR archive " . $part->base_name); |
---|
| 12096 | my($decompressor_name) = basename((split(' ',$unrar))[0]); |
---|
| 12097 | snmp_count("OpsDecBy\u${decompressor_name}Attempt"); |
---|
| 12098 | my(@common_rar_switches) = qw(-c- -p- -av- -idp); |
---|
| 12099 | my($err, $retval, $rv1); |
---|
| 12100 | # unrar exit codes: SUCCESS=0, WARNING=1, FATAL_ERROR=2, CRC_ERROR=3, |
---|
| 12101 | # LOCK_ERROR=4, WRITE_ERROR=5, OPEN_ERROR=6, USER_ERROR=7, MEMORY_ERROR=8, |
---|
| 12102 | # CREATE_ERROR=9, USER_BREAK=255 |
---|
| 12103 | # Check whether we can really unrar it |
---|
| 12104 | $rv1 = |
---|
| 12105 | system($unrar, 't', '-inul', @common_rar_switches, '--', $part->full_name); |
---|
| 12106 | $err = $!; $retval = retcode($rv1); |
---|
| 12107 | if ($retval == 7) { # USER_ERROR |
---|
| 12108 | do_log(-1,"do_unrar: $unrar does not recognize all switches, " |
---|
| 12109 | . "it is probably too old. Retrying without '-av- -idp'. " |
---|
| 12110 | . "Upgrade: http://www.rarlab.com/"); |
---|
| 12111 | @common_rar_switches = qw(-c- -p-); # retry without new switches |
---|
| 12112 | $rv1 = system($unrar, 't', '-inul', @common_rar_switches, '--', |
---|
| 12113 | $part->full_name); |
---|
| 12114 | $err = $!; $retval = retcode($rv1); |
---|
| 12115 | } |
---|
| 12116 | if (!grep { $_ == $retval } (0,1,3)) { |
---|
| 12117 | # not one of: SUCCESS, WARNING, CRC_ERROR |
---|
| 12118 | # NOTE: password protected files in the archive cause CRC_ERROR |
---|
| 12119 | do_log(4,sprintf("unrar 't' %s, command: %s", |
---|
| 12120 | exit_status_str($rv1,$err), $unrar)); |
---|
| 12121 | return 0; |
---|
| 12122 | } |
---|
| 12123 | |
---|
| 12124 | # We have to jump hoops because there is no simple way to |
---|
| 12125 | # just list all the files |
---|
| 12126 | ll(4) && do_log(4, "Expanding RAR archive " . $part->base_name); |
---|
| 12127 | |
---|
| 12128 | my(@list); my($hypcount) = 0; my($encryptedcount) = 0; |
---|
| 12129 | my($lcnt) = 0; my($member_name); my($bytes) = 0; my($last_line); |
---|
| 12130 | my($item_num) = 0; my($parent_placement) = $part->mime_placement; |
---|
| 12131 | my($proc_fh,$pid) = |
---|
| 12132 | run_command(undef, "&1", $unrar, 'v', @common_rar_switches, '--', |
---|
| 12133 | $part->full_name); |
---|
| 12134 | while (defined($_ = $proc_fh->getline)) { |
---|
| 12135 | $last_line = $_ if !/^\s*$/; # keep last nonempty line |
---|
| 12136 | chomp; |
---|
| 12137 | if (/^unexpected end of archive/) { |
---|
| 12138 | last; |
---|
| 12139 | } elsif (/^------/) { |
---|
| 12140 | $hypcount++; |
---|
| 12141 | last if $hypcount >= 2; |
---|
| 12142 | } elsif ($hypcount < 1 && /^Encrypted file:/) { |
---|
| 12143 | do_log(4,"do_unrar: ".$_); |
---|
| 12144 | $part->attributes_add('U','C'); |
---|
| 12145 | } elsif ($hypcount == 1) { |
---|
| 12146 | $lcnt++; |
---|
| 12147 | if ($lcnt % 2 == 0) { # information line (every other line) |
---|
| 12148 | if (!/^\s+(\d+)\s+(\d+)\s+(\d+%|-->|<--)/) { |
---|
| 12149 | do_log(-1,"do_unrar: can't parse info line for \"$member_name\" $_"); |
---|
| 12150 | } elsif (defined $member_name) { |
---|
| 12151 | do_log(5,"do_unrar: member: \"$member_name\", size: $1"); |
---|
| 12152 | if ($1 > 0) { $bytes += $1; push(@list, $member_name) } |
---|
| 12153 | } |
---|
| 12154 | $member_name = undef; |
---|
| 12155 | } elsif (/^(.)(.*)\z/s) { |
---|
| 12156 | $member_name = $2; # all but the first character (space or an asterisk) |
---|
| 12157 | if ($1 eq '*') { # member is encrypted |
---|
| 12158 | $encryptedcount++; $item_num++; |
---|
| 12159 | # make a phantom entry - carrying only name and attributes |
---|
| 12160 | my($newpart_obj)=Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
| 12161 | $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
| 12162 | $newpart_obj->name_declared($member_name); |
---|
| 12163 | $newpart_obj->attributes_add('U','C'); |
---|
| 12164 | $member_name = undef; # makes no sense extracting encrypted files |
---|
| 12165 | } |
---|
| 12166 | } |
---|
| 12167 | } |
---|
| 12168 | } |
---|
| 12169 | # consume all remaining output to avoid broken pipe |
---|
| 12170 | while (defined($_ = $proc_fh->getline)) { $last_line = $_ if !/^\s*$/ } |
---|
| 12171 | $err = undef; $proc_fh->close or $err = $!; $retval = retcode($?); |
---|
| 12172 | if ($retval == 3) { # CRC_ERROR |
---|
| 12173 | do_log(4,"do_unrar: CRC_ERROR - undecipherable"); |
---|
| 12174 | $part->attributes_add('U'); |
---|
| 12175 | } |
---|
| 12176 | my($fn) = $part->full_name; |
---|
| 12177 | if (!$bytes && $retval==0 && $last_line =~ /^\Q$fn\E is not RAR archive$/) { |
---|
| 12178 | do_log(4,"do_unrar: ".$last_line); |
---|
| 12179 | return 0; |
---|
| 12180 | } elsif ($last_line !~ /^\s*(\d+)\s+(\d+)/s) { |
---|
| 12181 | do_log(4,"do_unrar: unable to obtain orig total size: $last_line"); |
---|
| 12182 | } else { |
---|
| 12183 | do_log(4,"do_unrar: summary size: $2, sum of sizes: $bytes") |
---|
| 12184 | if abs($bytes - $2) > 100; |
---|
| 12185 | $bytes = $2 if $2 > $bytes; |
---|
| 12186 | } |
---|
| 12187 | consumed_bytes($bytes, 'do_unrar-pre', 1); # pre-check on estimated size |
---|
| 12188 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
| 12189 | if ($retval==0) {} # SUCCESS |
---|
| 12190 | elsif ($retval==1 && @list && $bytes > 0) {} # WARNING, probably still ok |
---|
| 12191 | else { # WARNING and suspicious, or really bad |
---|
| 12192 | die ("unrar: can't get a list of archive members: " . |
---|
| 12193 | exit_status_str($?,$err) ."; ".$last_line); |
---|
| 12194 | } |
---|
| 12195 | if (!@list) { |
---|
| 12196 | do_log(4,"do_unrar: no archive members, or not an archive at all"); |
---|
| 12197 | #***return 0 if $exec; |
---|
| 12198 | } else { |
---|
| 12199 | # my $rv = store_mgr($tempdir, $part, \@list, $unrar, |
---|
| 12200 | # qw(p -inul -kb), @common_rar_switches, '--', |
---|
| 12201 | # $part->full_name); |
---|
| 12202 | my($proc_fh,$pid) = |
---|
| 12203 | run_command(undef, "&1", $unrar, qw(x -inul -ver -o- -kb), |
---|
| 12204 | @common_rar_switches, '--', |
---|
| 12205 | $part->full_name, "$tempdir/parts/rar/"); |
---|
| 12206 | my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ } |
---|
| 12207 | my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); |
---|
| 12208 | if (!grep { $_ == $retval } (0,1,3)) { # not one of: SUCCESS, WARNING, CRC |
---|
| 12209 | do_log(-1, 'unrar '.exit_status_str($?,$err)); |
---|
| 12210 | } |
---|
| 12211 | my($errn) = lstat("$tempdir/parts/rar") ? 0 : 0+$!; |
---|
| 12212 | if ($errn != ENOENT) { |
---|
| 12213 | my($b) = flatten_and_tidy_dir("$tempdir/parts/rar","$tempdir/parts",$part); |
---|
| 12214 | consumed_bytes($b, 'do_unrar'); |
---|
| 12215 | } |
---|
| 12216 | } |
---|
| 12217 | if ($encryptedcount) { |
---|
| 12218 | do_log(1, sprintf( |
---|
| 12219 | "do_unrar: %s, %d members are encrypted, %s extracted, archive retained", |
---|
| 12220 | $part->base_name, $encryptedcount, !@list ? 'none' : 0+@list )); |
---|
| 12221 | return 2; |
---|
| 12222 | } |
---|
| 12223 | 1; |
---|
| 12224 | } |
---|
| 12225 | |
---|
| 12226 | # use external program to expand LHA archives |
---|
| 12227 | sub do_lha($$) { |
---|
| 12228 | my($part, $tempdir) = @_; |
---|
| 12229 | ll(4) && do_log(4, "Attempting to expand LHA archive " . $part->base_name); |
---|
| 12230 | my($decompressor_name) = basename((split(' ',$lha))[0]); |
---|
| 12231 | snmp_count("OpsDecBy\u${decompressor_name}Attempt"); |
---|
| 12232 | # lha needs extension .exe to understand SFX! |
---|
| 12233 | symlink($part->full_name, $part->full_name.".exe") |
---|
| 12234 | or die sprintf("Can't symlink %s %s.exe: %s", |
---|
| 12235 | $part->full_name, $part->full_name, $!); |
---|
| 12236 | # Check whether we can really lha it |
---|
| 12237 | my($checkerr); my($retval) = 1; |
---|
| 12238 | my($proc_fh,$pid) = run_command(undef, "&1", $lha, 'lq', |
---|
| 12239 | $part->full_name.".exe"); |
---|
| 12240 | while (defined($_ = $proc_fh->getline)) { |
---|
| 12241 | $checkerr = 1 if /Checksum error/i; |
---|
| 12242 | } |
---|
| 12243 | my($err); $proc_fh->close or $err = $!; |
---|
| 12244 | if ($? || $checkerr) { |
---|
| 12245 | $retval = 0; # consider atomic |
---|
| 12246 | do_log(4, "do_lha: not a LHA archive($checkerr) ? ". |
---|
| 12247 | exit_status_str($?,$err)); |
---|
| 12248 | } else { |
---|
| 12249 | do_log(4, "Expanding LHA archive " . $part->base_name . ".exe"); |
---|
| 12250 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
| 12251 | ($proc_fh,$pid) = |
---|
| 12252 | run_command(undef, undef, $lha, 'lq', $part->full_name.".exe"); |
---|
| 12253 | my(@list); |
---|
| 12254 | while (defined($_ = $proc_fh->getline)) { |
---|
| 12255 | chomp; |
---|
| 12256 | next if /\/\z/; # ignore directories |
---|
| 12257 | push(@list, (split(/\s+/))[-1]); #***??? split on whitespace ??? |
---|
| 12258 | } |
---|
| 12259 | $err=undef; $proc_fh->close or $err = $!; |
---|
| 12260 | $?==0 or do_log(-1, 'do_lha: '.exit_status_str($?,$err)); |
---|
| 12261 | if (!@list) { |
---|
| 12262 | do_log(4, "do_lha: no archive members, or not an archive at all"); |
---|
| 12263 | #*** $retval = 0 if $exec; |
---|
| 12264 | } else { |
---|
| 12265 | my $rv = store_mgr($tempdir, $part, \@list, $lha, 'pq', |
---|
| 12266 | $part->full_name.".exe"); |
---|
| 12267 | do_log(-1, 'do_lha '.exit_status_str($rv)) if $rv; |
---|
| 12268 | $retval = 1; # consider decoded |
---|
| 12269 | } |
---|
| 12270 | } |
---|
| 12271 | unlink($part->full_name.".exe") |
---|
| 12272 | or die "Can't unlink " . $part->full_name . ".exe: $!"; |
---|
| 12273 | $retval; |
---|
| 12274 | } |
---|
| 12275 | |
---|
| 12276 | # use external program to expand ARC archives; |
---|
| 12277 | # works with original arc, or a GPL licensed 'nomarch' |
---|
| 12278 | # (http://rus.members.beeb.net/nomarch.html) |
---|
| 12279 | sub do_arc($$) { |
---|
| 12280 | my($part, $tempdir) = @_; |
---|
| 12281 | my($decompressor_name) = basename((split(' ',$arc))[0]); |
---|
| 12282 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
| 12283 | my($is_nomarch) = $arc =~ /nomarch/i; |
---|
| 12284 | ll(4) && do_log(4,sprintf("Unarcing %s, using %s", |
---|
| 12285 | $part->base_name, ($is_nomarch ? "nomarch" : "arc") )); |
---|
| 12286 | my($cmdargs) = ($is_nomarch ? "-l -U" : "ln") . " " . $part->full_name; |
---|
| 12287 | my($proc_fh,$pid) = |
---|
| 12288 | run_command(undef, '/dev/null', $arc, split(' ',$cmdargs)); |
---|
| 12289 | my(@list) = $proc_fh->getlines; |
---|
| 12290 | my($err); $proc_fh->close or $err = $!; |
---|
| 12291 | $?==0 or do_log(-1, 'do_arc: '.exit_status_str($?,$err)); |
---|
| 12292 | |
---|
| 12293 | #*** no spaces in filenames allowed??? |
---|
| 12294 | map { s/^([^ \t\r\n]*).*\z/$1/s } @list; # keep only filenames |
---|
| 12295 | if (@list) { |
---|
| 12296 | my $rv = store_mgr($tempdir, $part, \@list, $arc, |
---|
| 12297 | ($is_nomarch ? ('-p', '-U') : 'p'), $part->full_name); |
---|
| 12298 | do_log(-1, 'arc '.exit_status_str($rv)) if $rv; |
---|
| 12299 | } |
---|
| 12300 | 1; |
---|
| 12301 | } |
---|
| 12302 | |
---|
| 12303 | # use external program to expand ZOO archives |
---|
| 12304 | sub do_zoo($$) { |
---|
| 12305 | my($part, $tempdir) = @_; |
---|
| 12306 | do_log(4, "Expanding ZOO archive " . $part->full_name); |
---|
| 12307 | my($decompressor_name) = basename((split(' ',$zoo))[0]); |
---|
| 12308 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
| 12309 | # Zoo needs extension of .zoo! |
---|
| 12310 | symlink($part->full_name, $part->full_name.".zoo") |
---|
| 12311 | or die sprintf("Can't symlink %s %s.zoo: %s", |
---|
| 12312 | $part->full_name, $part->full_name, $!); |
---|
| 12313 | my($proc_fh,$pid) = |
---|
| 12314 | run_command(undef, undef, $zoo, 'lf1q', $part->full_name.".zoo"); |
---|
| 12315 | my(@list) = $proc_fh->getlines; |
---|
| 12316 | my($err); $proc_fh->close or $err = $!; |
---|
| 12317 | $?==0 or do_log(-1, 'do_zoo: '.exit_status_str($?,$err)); |
---|
| 12318 | if (@list) { |
---|
| 12319 | chomp(@list); |
---|
| 12320 | my $rv = store_mgr($tempdir, $part, \@list, $zoo, 'xpqqq:', |
---|
| 12321 | $part->full_name . ".zoo"); |
---|
| 12322 | do_log(-1, 'zoo '.exit_status_str($rv)) if $rv; |
---|
| 12323 | } |
---|
| 12324 | unlink($part->full_name.".zoo") |
---|
| 12325 | or die "Can't unlink " . $part->full_name . ".zoo: $!"; |
---|
| 12326 | 1; |
---|
| 12327 | } |
---|
| 12328 | |
---|
| 12329 | # use external program to expand ARJ archives |
---|
| 12330 | sub do_unarj($$) { |
---|
| 12331 | my($part, $tempdir) = @_; |
---|
| 12332 | do_log(4, "Expanding ARJ archive " . $part->base_name); |
---|
| 12333 | my($decompressor_name) = basename((split(' ',$unrar))[0]); |
---|
| 12334 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
| 12335 | # options to arj, ignored by unarj |
---|
| 12336 | # provide some password in -g to turn fatal error into 'bad password' error |
---|
| 12337 | $ENV{ARJ_SW} = "-i -jo -b5 -2h -jyc -ja1 -gsecret -w$TEMPBASE"; |
---|
| 12338 | # unarj needs extension of .arj! |
---|
| 12339 | symlink($part->full_name, $part->full_name.".arj") |
---|
| 12340 | or die sprintf("Can't symlink %s %s.arj: %s", |
---|
| 12341 | $part->full_name, $part->full_name, $!); |
---|
| 12342 | # obtain total original size of archive members from the index/listing |
---|
| 12343 | my($proc_fh,$pid) = |
---|
| 12344 | run_command(undef,'/dev/null', $unarj, 'l', $part->full_name.".arj"); |
---|
| 12345 | my($last_line); |
---|
| 12346 | while (defined($_ = $proc_fh->getline)) { $last_line = $_ if !/^\s*$/ } |
---|
| 12347 | my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); |
---|
| 12348 | if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err |
---|
| 12349 | die ("unarj: can't get a list of archive members: ". |
---|
| 12350 | exit_status_str($?,$err)); |
---|
| 12351 | } |
---|
| 12352 | if ($last_line !~ /^\s*(\d+)\s*files\s*(\d+)/s) { |
---|
| 12353 | do_log(-1,"do_unarj: WARN: unable to obtain orig size of files: $last_line"); |
---|
| 12354 | } else { |
---|
| 12355 | consumed_bytes($2, 'do_unarj-pre', 1); # pre-check on estimated size |
---|
| 12356 | } |
---|
| 12357 | # unarj has very limited extraction options, arj is much better! |
---|
| 12358 | mkdir("$tempdir/parts/arj", 0750) or die "Can't mkdir $tempdir/parts/arj: $!"; |
---|
| 12359 | chdir("$tempdir/parts/arj") or die "Can't chdir to $tempdir/parts/arj: $!"; |
---|
| 12360 | ($proc_fh,$pid) = |
---|
| 12361 | run_command(undef, "&1", $unarj, 'e', $part->full_name.".arj"); |
---|
| 12362 | my($encryptedcount,$skippedcount) = (0,0); |
---|
| 12363 | while (defined($_ = $proc_fh->getline)) { |
---|
| 12364 | $encryptedcount++ |
---|
| 12365 | if /^(Extracting.*\bBad file data or bad password|File is password encrypted, Skipped)\b/s; |
---|
| 12366 | $skippedcount++ |
---|
| 12367 | if /(\bexists|^File is password encrypted|^Unsupported .*), Skipped\b/s; |
---|
| 12368 | } |
---|
| 12369 | $err = undef; $proc_fh->close or $err = $!; $retval = retcode($?); |
---|
| 12370 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
| 12371 | if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err |
---|
| 12372 | do_log(0, "unarj: error extracting: ".exit_status_str($?,$err)); |
---|
| 12373 | } |
---|
| 12374 | # add attributes to the parent object, because we didn't remember names |
---|
| 12375 | # of its scrambled members |
---|
| 12376 | $part->attributes_add('U') if $skippedcount; |
---|
| 12377 | $part->attributes_add('C') if $encryptedcount; |
---|
| 12378 | my($errn) = lstat("$tempdir/parts/arj") ? 0 : 0+$!; |
---|
| 12379 | if ($errn != ENOENT) { |
---|
| 12380 | my($b) = flatten_and_tidy_dir("$tempdir/parts/arj","$tempdir/parts",$part); |
---|
| 12381 | consumed_bytes($b, 'do_unarj'); |
---|
| 12382 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
| 12383 | } |
---|
| 12384 | unlink($part->full_name.".arj") |
---|
| 12385 | or die "Can't unlink " . $part->full_name . ".arj: $!"; |
---|
| 12386 | if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err |
---|
| 12387 | die ("unarj: can't extract archive members: ".exit_status_str($?,$err)); |
---|
| 12388 | } |
---|
| 12389 | if ($encryptedcount || $skippedcount) { |
---|
| 12390 | do_log(1, sprintf( |
---|
| 12391 | "do_unarj: %s, %d members are encrypted, %d skipped, archive retained", |
---|
| 12392 | $part->base_name, $encryptedcount, $skippedcount)); |
---|
| 12393 | return 2; |
---|
| 12394 | } |
---|
| 12395 | 1; |
---|
| 12396 | } |
---|
| 12397 | |
---|
| 12398 | # use Convert-TNEF |
---|
| 12399 | sub do_tnef($$) { |
---|
| 12400 | my($part, $tempdir) = @_; |
---|
| 12401 | do_log(4, "Extracting TNEF attachment " . $part->base_name); |
---|
| 12402 | snmp_count('OpsDecByTnef'); |
---|
| 12403 | chdir("$tempdir/parts") or die "Can't chdir to $tempdir/parts: $!"; |
---|
| 12404 | my $tnef = |
---|
| 12405 | Convert::TNEF->read_in($part->full_name, {ignore_checksum => "true"}); |
---|
| 12406 | if (!$tnef) { |
---|
| 12407 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
| 12408 | return 0; # Not TNEF - treat as atomic |
---|
| 12409 | } |
---|
| 12410 | my($item_num) = 0; my($parent_placement) = $part->mime_placement; |
---|
| 12411 | for my $a ($tnef->message, $tnef->attachments) { |
---|
| 12412 | if (my $dh = $a->datahandle) { |
---|
| 12413 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
| 12414 | $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
| 12415 | $newpart_obj->name_declared([$a->name, $a->longname]); |
---|
| 12416 | $newpart_obj->size($a->size); |
---|
| 12417 | consumed_bytes($a->size, 'do_tnef'); |
---|
| 12418 | my($newpart) = $newpart_obj->full_name; |
---|
| 12419 | my($outpart) = IO::File->new; |
---|
| 12420 | $outpart->open($newpart,'>') or die "Can't create file $newpart: $!"; |
---|
| 12421 | binmode($outpart) or die "Can't set file $newpart to binmode: $!"; |
---|
| 12422 | if (defined(my $file = $dh->path)) { |
---|
| 12423 | copy($file, $outpart); |
---|
| 12424 | } else { |
---|
| 12425 | my($s) = $dh->as_string; |
---|
| 12426 | $outpart->print($s) or die "Can't write to $newpart: $!"; |
---|
| 12427 | # consumed_bytes(length($s), 'do_tnef'); |
---|
| 12428 | } |
---|
| 12429 | $outpart->close or die "Can't close $newpart: $!"; |
---|
| 12430 | } |
---|
| 12431 | } |
---|
| 12432 | $tnef->purge; |
---|
| 12433 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
| 12434 | 1; |
---|
| 12435 | } |
---|
| 12436 | |
---|
| 12437 | # The pax and cpio utilities usually support the following archive formats: |
---|
| 12438 | # cpio, bcpio, sv4cpio, sv4crc, tar (old tar), ustar (POSIX.2 tar). |
---|
| 12439 | # The utilities from http://heirloom.sourceforge.net/ support |
---|
| 12440 | # several other tar/cpio variants such as SCO, Sun, DEC, Cray, SGI |
---|
| 12441 | sub do_pax_cpio($$$) { |
---|
| 12442 | my($part, $tempdir, $archiver) = @_; |
---|
| 12443 | my($archiver_name) = basename((split(' ',$archiver))[0]); |
---|
| 12444 | snmp_count("OpsDecBy\u${archiver_name}"); |
---|
| 12445 | ll(4) && do_log(4,sprintf("Expanding archive %s, using %s", |
---|
| 12446 | $part->base_name, $archiver_name)); |
---|
| 12447 | my($is_pax) = $archiver_name =~ /^cpio/i ? 0 : 1; |
---|
| 12448 | do_log(-1,"WARN: Using $archiver_name instead of pax can be a security ". |
---|
| 12449 | "risk; please add: \$pax='pax'; to amavisd.conf and check that ". |
---|
| 12450 | "the pax(1) utility is available on the system!") if !$is_pax; |
---|
| 12451 | my(@cmdargs) = $is_pax ? qw(-v) : qw(-i -t -v); |
---|
| 12452 | my($proc_fh,$pid) = run_command($part->full_name, undef, $archiver,@cmdargs); |
---|
| 12453 | my($bytes) = 0; local($1,$2,$3); |
---|
| 12454 | while (defined($_ = $proc_fh->getline)) { |
---|
| 12455 | chomp; |
---|
| 12456 | next if /^\d+ blocks\z/; |
---|
| 12457 | last if /^(cpio|pax): (.*bytes read|End of archive volume)/; |
---|
| 12458 | if (!/^ (?: \S+\s+ ){4} |
---|
| 12459 | (\d+) \s+ |
---|
| 12460 | ( (?: \s* \S+ ){3} (?: \s+ \d{4}, )? ) \s+ |
---|
| 12461 | (.+) \z/xs) { |
---|
| 12462 | do_log(-1,"do_pax_cpio: can't parse toc line: $_"); |
---|
| 12463 | } else { |
---|
| 12464 | my($mem,$size) = ($3,$1); |
---|
| 12465 | $mem = $1 if $is_pax && $mem =~ /^(.*) =[=>] (.*)\z/; # hard or soft link |
---|
| 12466 | do_log(5,"do_pax_cpio: member: \"$mem\", size: $size"); |
---|
| 12467 | $bytes += $size if $size > 0; |
---|
| 12468 | } |
---|
| 12469 | } |
---|
| 12470 | # consume remaining output to avoid broken pipe |
---|
| 12471 | while (defined($proc_fh->getline)) { } |
---|
| 12472 | my($err); $proc_fh->close or $err = $!; |
---|
| 12473 | $?==0 or do_log(-1, 'do_pax_cpio/1: '.exit_status_str($?,$err)); |
---|
| 12474 | consumed_bytes($bytes, 'do_pax_cpio/pre', 1); # pre-check on estimated size |
---|
| 12475 | mkdir("$tempdir/parts/arch", 0750) |
---|
| 12476 | or die "Can't mkdir $tempdir/parts/arch: $!"; |
---|
| 12477 | my($name_clash) = 0; |
---|
| 12478 | my(%orig_names); # maps filenames to archive member names when possible |
---|
| 12479 | eval { |
---|
| 12480 | chdir("$tempdir/parts/arch") |
---|
| 12481 | or die "Can't chdir to $tempdir/parts/arch: $!"; |
---|
| 12482 | my(@cmdargs) = $is_pax ? qw(-r -k -p am -s /[^A-Za-z0-9_]/-/gp) |
---|
| 12483 | : qw(-i -d --no-absolute-filenames --no-preserve-owner); |
---|
| 12484 | my($proc_fh,$pid) = run_command($part->full_name,"&1",$archiver,@cmdargs); |
---|
| 12485 | my($output) = ''; |
---|
| 12486 | while (defined($_ = $proc_fh->getline)) { |
---|
| 12487 | chomp; |
---|
| 12488 | if (!$is_pax || !/^(.*) >> (\S*)\z/) { $output .= $_."\n" } |
---|
| 12489 | else { # parse output from pax -s///p |
---|
| 12490 | my($member_name,$file_name) = ($1,$2); |
---|
| 12491 | if (!exists $orig_names{$file_name}) { |
---|
| 12492 | $orig_names{$file_name} = $member_name; |
---|
| 12493 | } else { |
---|
| 12494 | do_log(0,sprintf("do_pax_cpio: member \"%s\" is hidden by a ". |
---|
| 12495 | "previous archive member \"%s\", file: %s", |
---|
| 12496 | $member_name, $orig_names{$file_name}, $file_name)); |
---|
| 12497 | $orig_names{$file_name} = undef; # cause it to exist but undefined |
---|
| 12498 | $name_clash++; |
---|
| 12499 | } |
---|
| 12500 | } |
---|
| 12501 | } |
---|
| 12502 | chomp($output); my($err); $proc_fh->close or $err = $!; |
---|
| 12503 | $?==0 or die (exit_status_str($?,$err).' '.$output); |
---|
| 12504 | }; |
---|
| 12505 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
| 12506 | my($b) = flatten_and_tidy_dir("$tempdir/parts/arch", "$tempdir/parts", |
---|
| 12507 | $part, 0, \%orig_names); |
---|
| 12508 | consumed_bytes($b, 'do_pax_cpio'); |
---|
| 12509 | if ($@ ne '') { chomp($@); do_log(-1,"do_pax_cpio: $@"); 2 } |
---|
| 12510 | elsif ($name_clash) { 2 } else { 1 } |
---|
| 12511 | } |
---|
| 12512 | |
---|
| 12513 | # ar is a standard Unix binary archiver, also used by Debian packages |
---|
| 12514 | sub do_ar($$) { |
---|
| 12515 | my($part, $tempdir) = @_; |
---|
| 12516 | ll(4) && do_log(4,"Expanding Unix ar archive ".$part->full_name); |
---|
| 12517 | my($archiver_name) = basename((split(' ',$ar))[0]); |
---|
| 12518 | snmp_count("OpsDecBy\u${archiver_name}"); |
---|
| 12519 | my($proc_fh,$pid) = run_command(undef, undef, $ar, 'tv', $part->full_name); |
---|
| 12520 | my($bytes) = 0; local($1,$2,$3); |
---|
| 12521 | while (defined($_ = $proc_fh->getline)) { |
---|
| 12522 | chomp; |
---|
| 12523 | if (!/^(?:\S+\s+){2}(\d+)\s+((?:\S+\s+){3}\S+)\s+(.*)\z/) { |
---|
| 12524 | do_log(-1,"do_ar: can't parse contents listing line: $_"); |
---|
| 12525 | } else { |
---|
| 12526 | do_log(5,"do_ar: member: \"$3\", size: $1"); |
---|
| 12527 | $bytes += $1 if $1 > 0; |
---|
| 12528 | } |
---|
| 12529 | } |
---|
| 12530 | # consume remaining output to avoid broken pipe |
---|
| 12531 | while (defined($proc_fh->getline)) { } |
---|
| 12532 | my($err); $proc_fh->close or $err = $!; |
---|
| 12533 | $?==0 or do_log(-1, 'ar-1 '.exit_status_str($?,$err)); |
---|
| 12534 | |
---|
| 12535 | consumed_bytes($bytes, 'do_ar-pre', 1); # pre-check on estimated size |
---|
| 12536 | mkdir("$tempdir/parts/ar", 0750) |
---|
| 12537 | or die "Can't mkddir $tempdir/parts/ar: $!"; |
---|
| 12538 | chdir("$tempdir/parts/ar") or die "Can't chdir to $tempdir/parts/ar: $!"; |
---|
| 12539 | ($proc_fh,$pid) = run_command(undef, "&1", $ar, 'x', $part->full_name); |
---|
| 12540 | my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ } |
---|
| 12541 | $err = undef; $proc_fh->close or $err = $!; |
---|
| 12542 | $?==0 or do_log(-1, 'ar-2 '.exit_status_str($?,$err).' '.$output); |
---|
| 12543 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
| 12544 | my($b) = flatten_and_tidy_dir("$tempdir/parts/ar","$tempdir/parts",$part); |
---|
| 12545 | consumed_bytes($b, 'do_ar'); |
---|
| 12546 | 1; |
---|
| 12547 | } |
---|
| 12548 | |
---|
| 12549 | sub do_cabextract($$) { |
---|
| 12550 | my($part, $tempdir) = @_; |
---|
| 12551 | do_log(4, "Expanding cab archive " . $part->base_name); |
---|
| 12552 | my($archiver_name) = basename((split(' ',$cabextract))[0]); |
---|
| 12553 | snmp_count("OpsDecBy\u${archiver_name}"); |
---|
| 12554 | my($bytes) = 0; |
---|
| 12555 | my($proc_fh,$pid) = |
---|
| 12556 | run_command(undef,undef,$cabextract,'-l',$part->full_name); |
---|
| 12557 | while (defined($_ = $proc_fh->getline)) { |
---|
| 12558 | chomp; |
---|
| 12559 | next if /^(File size|----|Viewing cabinet:|\z)/; |
---|
| 12560 | if (!/^\s* (\d+) \s* \| [^|]* \| \s (.*) \z/x) { |
---|
| 12561 | do_log(-1, "do_cabextract: can't parse toc line: $_"); |
---|
| 12562 | } else { |
---|
| 12563 | do_log(5, "do_cabextract: member: \"$2\", size: $1"); |
---|
| 12564 | $bytes += $1 if $1 > 0; |
---|
| 12565 | } |
---|
| 12566 | } |
---|
| 12567 | # consume remaining output to avoid broken pipe (just in case) |
---|
| 12568 | while (defined($proc_fh->getline)) { } |
---|
| 12569 | my($err); $proc_fh->close or $err = $!; |
---|
| 12570 | $?==0 or do_log(-1, 'cabextract-1 '.exit_status_str($?,$err)); |
---|
| 12571 | |
---|
| 12572 | consumed_bytes($bytes, 'do_cabextract-pre', 1); # pre-check on estimated size |
---|
| 12573 | mkdir("$tempdir/parts/cab", 0750) or die "Can't mkdir $tempdir/parts/cab: $!"; |
---|
| 12574 | ($proc_fh,$pid) = run_command(undef, '/dev/null', $cabextract, '-q', '-d', |
---|
| 12575 | "$tempdir/parts/cab", $part->full_name); |
---|
| 12576 | my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ } |
---|
| 12577 | $err = undef; $proc_fh->close or $err = $!; |
---|
| 12578 | $?==0 or do_log(-1, 'cabextract-2 '.exit_status_str($?,$err).' '.$output); |
---|
| 12579 | my($b) = flatten_and_tidy_dir("$tempdir/parts/cab", "$tempdir/parts", $part); |
---|
| 12580 | consumed_bytes($b, 'do_cabextract'); |
---|
| 12581 | 1; |
---|
| 12582 | } |
---|
| 12583 | |
---|
| 12584 | sub do_ole($$) { |
---|
| 12585 | my($part, $tempdir) = @_; |
---|
| 12586 | do_log(4,"Expanding MS OLE document " . $part->base_name); |
---|
| 12587 | my($archiver_name) = basename((split(' ',$ripole))[0]); |
---|
| 12588 | snmp_count("OpsDecBy\u${archiver_name}"); |
---|
| 12589 | mkdir("$tempdir/parts/ole",0750) or die "Can't mkdir $tempdir/parts/ole: $!"; |
---|
| 12590 | my($proc_fh,$pid) = run_command(undef, "&1", $ripole, '-v', |
---|
| 12591 | '-i', $part->full_name, '-d',"$tempdir/parts/ole"); |
---|
| 12592 | my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ } |
---|
| 12593 | my($err); $proc_fh->close or $err = $!; |
---|
| 12594 | $?==0 or do_log(0, 'ripOLE '.exit_status_str($?,$err).' '.$output); |
---|
| 12595 | my($b) = flatten_and_tidy_dir("$tempdir/parts/ole", "$tempdir/parts", $part); |
---|
| 12596 | if ($b > 0) { |
---|
| 12597 | do_log(4, "ripOLE extracted $b bytes from an OLE document"); |
---|
| 12598 | consumed_bytes($b, 'do_ole'); |
---|
| 12599 | } |
---|
| 12600 | 2; # always keep the original OLE document |
---|
| 12601 | } |
---|
| 12602 | |
---|
| 12603 | # Check for self-extracting archives. Note that we don't rely on |
---|
| 12604 | # file magic here since it's not reliable. Instead we will try each |
---|
| 12605 | # archiver. |
---|
| 12606 | sub do_executable($$) { |
---|
| 12607 | my($part, $tempdir) = @_; |
---|
| 12608 | |
---|
| 12609 | ll(4) && do_log(4,"Check whether ".$part->base_name. |
---|
| 12610 | " is a self-extracting archive"); |
---|
| 12611 | # ZIP? |
---|
| 12612 | return 2 if eval { do_unzip($part, $tempdir) }; |
---|
| 12613 | chomp($@); |
---|
| 12614 | do_log(-1,"do_executable/do_unzip failed, ignoring: $@") if $@ ne ''; |
---|
| 12615 | |
---|
| 12616 | # RAR? |
---|
| 12617 | return 2 if defined $unrar && eval { do_unrar($part, $tempdir) }; |
---|
| 12618 | chomp($@); |
---|
| 12619 | do_log(-1,"do_executable/do_unrar failed, ignoring: $@") if $@ ne ''; |
---|
| 12620 | |
---|
| 12621 | # LHA? |
---|
| 12622 | return 2 if defined $lha && eval { do_lha($part, $tempdir) }; |
---|
| 12623 | chomp($@); |
---|
| 12624 | do_log(-1,"do_executable/do_lha failed, ignoring: $@") if $@ ne ''; |
---|
| 12625 | |
---|
| 12626 | # # ARJ? |
---|
| 12627 | # return 2 if defined $unarj && eval { do_unarj($part, $tempdir) }; |
---|
| 12628 | # chomp($@); |
---|
| 12629 | # do_log(-1,"do_executable/do_unarj failed, ignoring: $@") if $@ ne ''; |
---|
| 12630 | |
---|
| 12631 | return 0; |
---|
| 12632 | } |
---|
| 12633 | |
---|
| 12634 | # my($k,$v,$fn); |
---|
| 12635 | # while (($k,$v) = each(%::)) { |
---|
| 12636 | # local(*e)=$v; $fn=fileno(\*e); |
---|
| 12637 | # printf STDERR ("%-10s %-10s %s$eol",$k,$v,$fn) if defined $fn; |
---|
| 12638 | # } |
---|
| 12639 | |
---|
| 12640 | # Given a file handle (typically opened pipe to a subprocess, as returned |
---|
| 12641 | # from run_command), copy from it to a specified output file in binary mode. |
---|
| 12642 | sub run_command_copy($$) { |
---|
| 12643 | my($outfile, $ifh) = @_; |
---|
| 12644 | my($ofh) = IO::File->new; |
---|
| 12645 | $ofh->open($outfile,'>') or die "Can't create file $outfile: $!"; |
---|
| 12646 | binmode($ofh) or die "Can't set file $outfile to binmode: $!"; |
---|
| 12647 | binmode($ifh) or die "Can't set binmode on pipe: $!"; |
---|
| 12648 | my($len, $buf, $offset, $written); |
---|
| 12649 | while ($len = $ifh->sysread($buf, 16384)) { |
---|
| 12650 | $offset = 0; |
---|
| 12651 | while ($len > 0) { # handle partial writes |
---|
| 12652 | $written = syswrite($ofh, $buf, $len, $offset); |
---|
| 12653 | defined($written) or die "syswrite to $outfile failed: $!"; |
---|
| 12654 | consumed_bytes($written, 'run_command_copy'); |
---|
| 12655 | $len -= $written; $offset += $written; |
---|
| 12656 | } |
---|
| 12657 | } |
---|
| 12658 | my($rerr); $ifh->close or $rerr=$!; my($rv) = $?; |
---|
| 12659 | $ofh->close or die "Can't close $outfile: $!"; |
---|
| 12660 | ($rv,$rerr); # return subprocess termination status and errno |
---|
| 12661 | } |
---|
| 12662 | |
---|
| 12663 | # extract listed files from archive and store in new file |
---|
| 12664 | sub store_mgr($$$@) { |
---|
| 12665 | my($tempdir, $parent_obj, $list, $cmd, @args) = @_; |
---|
| 12666 | |
---|
| 12667 | my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement; |
---|
| 12668 | my(@rv); |
---|
| 12669 | for my $f (@$list) { |
---|
| 12670 | next if $f =~ m{/\z}; # ignore directories |
---|
| 12671 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts", |
---|
| 12672 | $parent_obj); |
---|
| 12673 | $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
| 12674 | $newpart_obj->name_declared($f); # store tainted name |
---|
| 12675 | my($newpart) = $newpart_obj->full_name; |
---|
| 12676 | do_log(5,sprintf('store_mgr: extracting "%s" to file %s using %s', |
---|
| 12677 | $f, $newpart, $cmd)); |
---|
| 12678 | if ($f =~ m{^\.?[A-Za-z0-9_][A-Za-z0-9/._=~-]*\z}) { # apparently safe arg |
---|
| 12679 | } else { # this is not too bad, as run_command does not use shell |
---|
| 12680 | do_log(1, "store_mgr: NOTICE: untainting funny argument \"$f\""); |
---|
| 12681 | } |
---|
| 12682 | my($proc_fh,$pid) = run_command(undef,undef,$cmd,@args,untaint($f)); |
---|
| 12683 | my($rv,$rerr) = run_command_copy($newpart,$proc_fh); |
---|
| 12684 | do_log(5,"store_mgr: extracted by $cmd, ".exit_status_str($rv,$rerr)); |
---|
| 12685 | push(@rv, $rv); |
---|
| 12686 | } |
---|
| 12687 | @rv = grep { $_ != 0 } @rv; |
---|
| 12688 | @rv ? $rv[0] : 0; # just return the first nonzero status (if any), or 0 |
---|
| 12689 | } |
---|
| 12690 | |
---|
| 12691 | 1; |
---|
| 12692 | |
---|
| 12693 | __DATA__ |
---|
| 12694 | # |
---|
| 12695 | # ============================================================================= |
---|
| 12696 | # This text section governs how a main per-message amavisd-new log entry |
---|
| 12697 | # is formed. An empty text will prevent a log entry, multi-line text will |
---|
| 12698 | # produce several log entries, one for each nonempty line. |
---|
| 12699 | # Syntax is explained in the README.customize file. |
---|
| 12700 | [?%#D|#|Passed # |
---|
| 12701 | [? [?%#V|1] |INFECTED (%V)|# |
---|
| 12702 | [? [?%#F|1] |BANNED (%F)|# |
---|
| 12703 | [? [? %2|1] |SPAM|# |
---|
| 12704 | [? [?%#X|1] |BAD-HEADER|CLEAN]]]]# |
---|
| 12705 | , [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%D|,]# |
---|
| 12706 | [? %q ||, quarantine: %i]# |
---|
| 12707 | [? %Q ||, Queue-ID: %Q]# |
---|
| 12708 | [? %m ||, Message-ID: %m]# |
---|
| 12709 | [? %r ||, Resent-Message-ID: %r]# |
---|
| 12710 | , Hits: %c# |
---|
| 12711 | #, size: %z# |
---|
| 12712 | #[? %j ||, Subject: "%j"]# |
---|
| 12713 | #[? %#T ||, tests=[%T|,]]# |
---|
| 12714 | , %y ms# |
---|
| 12715 | ] |
---|
| 12716 | [?%#O|#|Blocked # |
---|
| 12717 | [? [?%#V|1] |INFECTED (%V)|# |
---|
| 12718 | [? [?%#F|1] |BANNED (%F)|# |
---|
| 12719 | [? [? %2|1] |SPAM|# |
---|
| 12720 | [? [?%#X|1] |BAD-HEADER|CLEAN]]]]# |
---|
| 12721 | , [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%O|,]# |
---|
| 12722 | [? %q ||, quarantine: %i]# |
---|
| 12723 | [? %Q ||, Queue-ID: %Q]# |
---|
| 12724 | [? %m ||, Message-ID: %m]# |
---|
| 12725 | [? %r ||, Resent-Message-ID: %r]# |
---|
| 12726 | , Hits: %c# |
---|
| 12727 | #, size: %z# |
---|
| 12728 | #[? %j ||, Subject: "%j"]# |
---|
| 12729 | #[? %#T ||, tests=[%T|,]]# |
---|
| 12730 | , %y ms# |
---|
| 12731 | ] |
---|
| 12732 | __DATA__ |
---|
| 12733 | # |
---|
| 12734 | # ============================================================================= |
---|
| 12735 | # This text section governs how a main per-recipient amavisd-new log entry |
---|
| 12736 | # is formed. An empty text will prevent a log entry, multi-line text will |
---|
| 12737 | # produce several log entries, one for each nonempty line. |
---|
| 12738 | # Macro %. might be useful, it counts recipients starting from 1. |
---|
| 12739 | # Syntax is explained in the README.customize file. |
---|
| 12740 | # |
---|
| 12741 | [?%#D||Passed # |
---|
| 12742 | [? [?%#V|1] |INFECTED (%V)|# |
---|
| 12743 | [? [?%#F|1] |BANNED (%F)|# |
---|
| 12744 | [? [? %2|1] |SPAM|# |
---|
| 12745 | [? [?%#X|1] |BAD-HEADER|CLEAN]]]]# |
---|
| 12746 | , <%o> -> [%D|,], Hits: %c# |
---|
| 12747 | , tag=%3, tag2=%4, kill=%5# NOTE: macros %3, %4, %5 are experimental |
---|
| 12748 | , %0/%1/%2/%k# |
---|
| 12749 | ] |
---|
| 12750 | [?%#O||Blocked # |
---|
| 12751 | [? [?%#V|1] |INFECTED (%V)|# |
---|
| 12752 | [? [?%#F|1] |BANNED (%F)|# |
---|
| 12753 | [? [? %2|1] |SPAM|# |
---|
| 12754 | [? [?%#X|1] |BAD-HEADER|CLEAN]]]]# |
---|
| 12755 | , <%o> -> [%O|,], Hits: %c# |
---|
| 12756 | , tag=%3, tag2=%4, kill=%5# NOTE: macros %3, %4, %5 are experimental |
---|
| 12757 | , %0/%1/%2/%k# |
---|
| 12758 | ] |
---|
| 12759 | __DATA__ |
---|
| 12760 | # |
---|
| 12761 | # ============================================================================= |
---|
| 12762 | # This is a template for (neutral: non-virus, non-spam, non-banned) DELIVERY |
---|
| 12763 | # STATUS NOTIFICATIONS to sender. For syntax and customization instructions |
---|
| 12764 | # see README.customize. Note that only valid header fields are allowed; |
---|
| 12765 | # non-standard header field heads must begin with "X-" . |
---|
| 12766 | # The From, To and Date header fields will be provided automatically. |
---|
| 12767 | # |
---|
| 12768 | Subject: Undeliverable mail[?%#X||, invalid characters in header] |
---|
| 12769 | Message-ID: <DSN%n@%h> |
---|
| 12770 | |
---|
| 12771 | [? %#X ||INVALID HEADER (INVALID CHARACTERS OR SPACE GAP) |
---|
| 12772 | |
---|
| 12773 | [%X\n] |
---|
| 12774 | ]\ |
---|
| 12775 | This nondelivery report was generated by the amavisd-new program |
---|
| 12776 | at host %h. Our internal reference code for your message |
---|
| 12777 | is %n. |
---|
| 12778 | |
---|
| 12779 | [? %#X || |
---|
| 12780 | WHAT IS AN INVALID CHARACTER IN MAIL HEADER? |
---|
| 12781 | |
---|
| 12782 | The RFC 2822 standard specifies rules for forming internet messages. |
---|
| 12783 | It does not allow the use of characters with codes above 127 to be used |
---|
| 12784 | directly (non-encoded) in mail header (it also prohibits NUL and bare CR). |
---|
| 12785 | |
---|
| 12786 | If characters (e.g. with diacritics) from ISO Latin or other alphabets |
---|
| 12787 | need to be included in the header, these characters need to be properly |
---|
| 12788 | encoded according to RFC 2047. This encoding is often done transparently |
---|
| 12789 | by mail reader (MUA), but if automatic encoding is not available (e.g. |
---|
| 12790 | by some older MUA) it is the user's responsibility to avoid the use |
---|
| 12791 | of such characters in mail header, or to encode them manually. Typically |
---|
| 12792 | the offending header fields in this category are 'Subject', 'Organization', |
---|
| 12793 | and comment fields in e-mail addresses of the 'From', 'To' and 'Cc'. |
---|
| 12794 | |
---|
| 12795 | Sometimes such invalid header fields are inserted automatically |
---|
| 12796 | by some MUA, MTA, content checker, or other mail handling service. |
---|
| 12797 | If this is the case, that service needs to be fixed or properly configured. |
---|
| 12798 | Typically the offending header fields in this category are 'Date', |
---|
| 12799 | 'Received', 'X-Mailer', 'X-Priority', 'X-Scanned', etc. |
---|
| 12800 | |
---|
| 12801 | If you don't know how to fix or avoid the problem, please report it |
---|
| 12802 | to _your_ postmaster or system manager. |
---|
| 12803 | ]\ |
---|
| 12804 | |
---|
| 12805 | Return-Path: %s |
---|
| 12806 | Your message[?%m|| %m][?%r|| (Resent-Message-ID: %r)] |
---|
| 12807 | could not be delivered to:[\n %N] |
---|
| 12808 | __DATA__ |
---|
| 12809 | # |
---|
| 12810 | # ============================================================================= |
---|
| 12811 | # This is a template for VIRUS/BANNED SENDER NOTIFICATIONS. |
---|
| 12812 | # For syntax and customization instructions see README.customize. |
---|
| 12813 | # Note that only valid header fields are allowed; |
---|
| 12814 | # non-standard header field heads must begin with "X-" . |
---|
| 12815 | # The From, To and Date header fields will be provided automatically. |
---|
| 12816 | # |
---|
| 12817 | Subject: [? %#V |[? %#F |Unknown problem|BANNED (%F)]|VIRUS (%V)] IN MAIL FROM YOU |
---|
| 12818 | [? %m |#|In-Reply-To: %m] |
---|
| 12819 | Message-ID: <VS%n@%h> |
---|
| 12820 | |
---|
| 12821 | [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED CONTENTS ALERT]|VIRUS ALERT] |
---|
| 12822 | |
---|
| 12823 | Our content checker found |
---|
| 12824 | [? %#V |#| [? %#V |viruses|virus|viruses]: %V] |
---|
| 12825 | [? %#F |#| banned [? %#F |names|name|names]: %F] |
---|
| 12826 | [? %#X |#|\n[%X\n]] |
---|
| 12827 | in email presumably from you (%s), |
---|
| 12828 | to the following [? %#R |recipients|recipient|recipients]:[ |
---|
| 12829 | -> %R] |
---|
| 12830 | |
---|
| 12831 | Our internal reference code for your message is %n. |
---|
| 12832 | |
---|
| 12833 | [? %#V ||Please check your system for viruses, |
---|
| 12834 | or ask your system administrator to do so. |
---|
| 12835 | |
---|
| 12836 | ]# |
---|
| 12837 | [? %#D |Delivery of the email was stopped! |
---|
| 12838 | |
---|
| 12839 | ]# |
---|
| 12840 | [? %#V |[? %#F ||# |
---|
| 12841 | The message has been blocked because it contains a component |
---|
| 12842 | (as a MIME part or nested within) with declared name |
---|
| 12843 | or MIME type or contents type violating our access policy. |
---|
| 12844 | |
---|
| 12845 | To transfer contents that may be considered risky or unwanted |
---|
| 12846 | by site policies, or simply too large for mailing, please consider |
---|
| 12847 | publishing your content on the web, and only sending an URL of the |
---|
| 12848 | document to the recipient. |
---|
| 12849 | |
---|
| 12850 | Depending on the recipient and sender site policies, with a little |
---|
| 12851 | effort it might still be possible to send any contents (including |
---|
| 12852 | viruses) using one of the following methods: |
---|
| 12853 | |
---|
| 12854 | - encrypted using pgp, gpg or other encryption methods; |
---|
| 12855 | |
---|
| 12856 | - wrapped in a password-protected or scrambled container or archive |
---|
| 12857 | (e.g.: zip -e, arj -g, arc g, rar -p, or other methods) |
---|
| 12858 | |
---|
| 12859 | Note that if the contents is not intended to be secret, the |
---|
| 12860 | encryption key or password may be included in the same message |
---|
| 12861 | for recipient's convenience. |
---|
| 12862 | |
---|
| 12863 | We are sorry for inconvenience if the contents was not malicious. |
---|
| 12864 | |
---|
| 12865 | The purpose of these restrictions is to cut the most common propagation |
---|
| 12866 | methods used by viruses and other malware. These often exploit automatic |
---|
| 12867 | mechanisms and security holes in certain mail readers (Microsoft mail |
---|
| 12868 | readers and browsers are a common and easy target). By requiring an |
---|
| 12869 | explicit and decisive action from the recipient to decode mail, |
---|
| 12870 | the dangers of automatic malware propagation is largely reduced. |
---|
| 12871 | # |
---|
| 12872 | # Details of our mail restrictions policy are available at ... |
---|
| 12873 | |
---|
| 12874 | ]]# |
---|
| 12875 | For your reference, here are headers from your email: |
---|
| 12876 | ------------------------- BEGIN HEADERS ----------------------------- |
---|
| 12877 | Return-Path: %s |
---|
| 12878 | [%H |
---|
| 12879 | ]\ |
---|
| 12880 | -------------------------- END HEADERS ------------------------------ |
---|
| 12881 | __DATA__ |
---|
| 12882 | # |
---|
| 12883 | # ============================================================================= |
---|
| 12884 | # This is a template for non-spam (VIRUS,...) ADMINISTRATOR NOTIFICATIONS. |
---|
| 12885 | # For syntax and customization instructions see README.customize. |
---|
| 12886 | # Note that only valid header fields are allowed; non-standard header |
---|
| 12887 | # field heads must begin with "X-" . |
---|
| 12888 | # |
---|
| 12889 | Date: %d |
---|
| 12890 | From: %f |
---|
| 12891 | Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED (%F)]|VIRUS (%V)]# |
---|
| 12892 | FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>] |
---|
| 12893 | To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]] |
---|
| 12894 | [? %#C |#|Cc: [<%C>|, ]] |
---|
| 12895 | Message-ID: <VA%n@%h> |
---|
| 12896 | |
---|
| 12897 | [? %#V |No viruses were found. |
---|
| 12898 | |A virus was found: %V |
---|
| 12899 | |Two viruses were found:\n %V |
---|
| 12900 | |%#V viruses were found:\n %V |
---|
| 12901 | ] |
---|
| 12902 | [? %#F |#\ |
---|
| 12903 | |A banned name was found:\n %F |
---|
| 12904 | |Two banned names were found:\n %F |
---|
| 12905 | |%#F banned names were found:\n %F |
---|
| 12906 | ] |
---|
| 12907 | [? %#X |#\ |
---|
| 12908 | |Bad header was found:[\n %X] |
---|
| 12909 | ] |
---|
| 12910 | [? %#W |#\ |
---|
| 12911 | |Scanner detecting a virus: %W |
---|
| 12912 | |Scanners detecting a virus: %W |
---|
| 12913 | ] |
---|
| 12914 | The mail originated from: <%o> |
---|
| 12915 | [? %a |#|First upstream SMTP client IP address: \[%a\] %g |
---|
| 12916 | ] |
---|
| 12917 | [? %t |#|According to the 'Received:' trace, the message originated at: |
---|
| 12918 | \[%e\] |
---|
| 12919 | %t |
---|
| 12920 | ] |
---|
| 12921 | [? %#S |Notification to sender will not be mailed. |
---|
| 12922 | |
---|
| 12923 | ]# |
---|
| 12924 | [? %#D |#|The message WILL BE delivered to:[\n%D] |
---|
| 12925 | ] |
---|
| 12926 | [? %#N |#|The message WAS NOT delivered to:[\n%N] |
---|
| 12927 | ] |
---|
| 12928 | [? %#V |#|[? %#v |#|Virus scanner output:[\n %v] |
---|
| 12929 | ]] |
---|
| 12930 | [? %q |Not quarantined.|The message has been quarantined as:\n %q |
---|
| 12931 | ] |
---|
| 12932 | ------------------------- BEGIN HEADERS ----------------------------- |
---|
| 12933 | Return-Path: %s |
---|
| 12934 | [%H |
---|
| 12935 | ]\ |
---|
| 12936 | -------------------------- END HEADERS ------------------------------ |
---|
| 12937 | __DATA__ |
---|
| 12938 | # |
---|
| 12939 | # ============================================================================= |
---|
| 12940 | # This is a template for VIRUS/BANNED/BAD-HEADER RECIPIENTS NOTIFICATIONS. |
---|
| 12941 | # For syntax and customization instructions see README.customize. |
---|
| 12942 | # Note that only valid header fields are allowed; non-standard header |
---|
| 12943 | # field heads must begin with "X-" . |
---|
| 12944 | # |
---|
| 12945 | Date: %d |
---|
| 12946 | From: %f |
---|
| 12947 | Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED]|VIRUS (%V)]# |
---|
| 12948 | IN MAIL TO YOU (from [?%o|(?)|<%o>]) |
---|
| 12949 | To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]] |
---|
| 12950 | [? %#C |#|Cc: [<%C>|, ]] |
---|
| 12951 | Message-ID: <VR%n@%h> |
---|
| 12952 | |
---|
| 12953 | [? %#V |[? %#F ||BANNED CONTENTS ALERT]|VIRUS ALERT] |
---|
| 12954 | |
---|
| 12955 | Our content checker found |
---|
| 12956 | [? %#V |#| [? %#V |viruses|virus|viruses]: %V] |
---|
| 12957 | [? %#F |#| banned [? %#F |names|name|names]: %F] |
---|
| 12958 | [? %#X |#|\n[%X\n]] |
---|
| 12959 | |
---|
| 12960 | in an email to you [? %S |from unknown sender:|from:] |
---|
| 12961 | %o |
---|
| 12962 | [? %S |claiming to be: %s|#] |
---|
| 12963 | |
---|
| 12964 | [? %a |#|First upstream SMTP client IP address: \[%a\] %g |
---|
| 12965 | ] |
---|
| 12966 | [? %t |#|According to the 'Received:' trace, the message originated at: |
---|
| 12967 | \[%e\] |
---|
| 12968 | %t |
---|
| 12969 | ] |
---|
| 12970 | Our internal reference code for this message is %n. |
---|
| 12971 | [? %q |Not quarantined.|The message has been quarantined as: |
---|
| 12972 | %q] |
---|
| 12973 | |
---|
| 12974 | Please contact your system administrator for details. |
---|
| 12975 | __DATA__ |
---|
| 12976 | # |
---|
| 12977 | # ============================================================================= |
---|
| 12978 | # This is a template for SPAM SENDER NOTIFICATIONS. |
---|
| 12979 | # For syntax and customization instructions see README.customize. |
---|
| 12980 | # Note that only valid header fields are allowed; |
---|
| 12981 | # non-standard header field heads must begin with "X-" . |
---|
| 12982 | # The From, To and Date header fields will be provided automatically. |
---|
| 12983 | # |
---|
| 12984 | Subject: Considered UNSOLICITED BULK EMAIL from you |
---|
| 12985 | [? %m |#|In-Reply-To: %m] |
---|
| 12986 | Message-ID: <SS%n@%h> |
---|
| 12987 | |
---|
| 12988 | Your message to:[ |
---|
| 12989 | -> %R] |
---|
| 12990 | |
---|
| 12991 | was considered unsolicited bulk e-mail (UBE). |
---|
| 12992 | [? %#X |#|\n[%X\n]] |
---|
| 12993 | Subject: %j |
---|
| 12994 | Return-Path: %s |
---|
| 12995 | Our internal reference code for your message is %n. |
---|
| 12996 | |
---|
| 12997 | [? %#D |Delivery of the email was stopped! |
---|
| 12998 | ]# |
---|
| 12999 | # |
---|
| 13000 | # SpamAssassin report: |
---|
| 13001 | # [%A |
---|
| 13002 | # ]\ |
---|
| 13003 | __DATA__ |
---|
| 13004 | # |
---|
| 13005 | # ============================================================================= |
---|
| 13006 | # This is a template for SPAM ADMINISTRATOR NOTIFICATIONS. |
---|
| 13007 | # For syntax and customization instructions see README.customize. |
---|
| 13008 | # Note that only valid header fields are allowed; non-standard header |
---|
| 13009 | # field heads must begin with "X-" . |
---|
| 13010 | # |
---|
| 13011 | Date: %d |
---|
| 13012 | From: %f |
---|
| 13013 | Subject: SPAM FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>] |
---|
| 13014 | To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]] |
---|
| 13015 | [? %#C |#|Cc: [<%C>|, ]] |
---|
| 13016 | [? %#B |#|Bcc: [<%B>|, ]] |
---|
| 13017 | Message-ID: <SA%n@%h> |
---|
| 13018 | |
---|
| 13019 | Unsolicited bulk email [? %S |from unknown or forged sender:|from:] |
---|
| 13020 | %o |
---|
| 13021 | Subject: %j |
---|
| 13022 | |
---|
| 13023 | [? %a |#|First upstream SMTP client IP address: \[%a\] %g |
---|
| 13024 | ] |
---|
| 13025 | [? %t |#|According to the 'Received:' trace, the message originated at: |
---|
| 13026 | \[%e\] |
---|
| 13027 | %t |
---|
| 13028 | ] |
---|
| 13029 | [? %#D |#|The message WILL BE delivered to:[\n%D] |
---|
| 13030 | ] |
---|
| 13031 | [? %#N |#|The message WAS NOT delivered to:[\n%N] |
---|
| 13032 | ] |
---|
| 13033 | [? %q |Not quarantined.|The message has been quarantined as:\n %q |
---|
| 13034 | ] |
---|
| 13035 | SpamAssassin report: |
---|
| 13036 | [%A |
---|
| 13037 | ]\ |
---|
| 13038 | |
---|
| 13039 | ------------------------- BEGIN HEADERS ----------------------------- |
---|
| 13040 | Return-Path: %s |
---|
| 13041 | [%H |
---|
| 13042 | ]\ |
---|
| 13043 | -------------------------- END HEADERS ------------------------------ |
---|