Parent Directory
|
Revision Log
Fix handling of stdin/stderr/stdout. cpio and tar handling was broken before.....
| 1 | zobel | 21 | #!/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,2005 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::IP | ||
| 67 | # Amavis::Lookup::Label | ||
| 68 | # Amavis::Lookup | ||
| 69 | # Amavis::Expand | ||
| 70 | # Amavis::IO::Zlib | ||
| 71 | # Amavis::In::Connection | ||
| 72 | # Amavis::In::Message::PerRecip | ||
| 73 | # Amavis::In::Message | ||
| 74 | # Amavis::Out::EditHeader | ||
| 75 | # Amavis::Out::Local | ||
| 76 | # Amavis::Out | ||
| 77 | # Amavis::UnmangleSender | ||
| 78 | # Amavis::Unpackers::NewFilename | ||
| 79 | # Amavis::Unpackers::Part | ||
| 80 | # Amavis::Unpackers::OurFiler | ||
| 81 | # Amavis::Unpackers::Validity | ||
| 82 | # Amavis::Unpackers::MIME | ||
| 83 | # Amavis::Notify | ||
| 84 | # Amavis::Cache | ||
| 85 | # Amavis | ||
| 86 | #optionally compiled-in packages: --------------------------------------------- | ||
| 87 | # Amavis::DB::SNMP | ||
| 88 | # Amavis::DB | ||
| 89 | # Amavis::Cache | ||
| 90 | # Amavis::Out::SQL::Connection | ||
| 91 | # Amavis::Out::SQL::Log | ||
| 92 | # Amavis::IO::SQL | ||
| 93 | # Amavis::Out::SQL::Quarantine | ||
| 94 | # Amavis::Lookup::SQLfield | ||
| 95 | # Amavis::Lookup::SQL | ||
| 96 | # Amavis::LDAP::Connection | ||
| 97 | # Amavis::Lookup::LDAP | ||
| 98 | # Amavis::Lookup::LDAPattr | ||
| 99 | # Amavis::In::AMCL | ||
| 100 | # Amavis::In::SMTP | ||
| 101 | # Amavis::AV | ||
| 102 | # Amavis::SpamControl | ||
| 103 | # Amavis::Unpackers | ||
| 104 | #------------------------------------------------------------------------------ | ||
| 105 | |||
| 106 | # | ||
| 107 | package Amavis::Boot; | ||
| 108 | use strict; | ||
| 109 | use re 'taint'; | ||
| 110 | |||
| 111 | # Fetch all required modules (or nicely report missing ones), and compile them | ||
| 112 | # once-and-for-all at the parent process, so that forked children can inherit | ||
| 113 | # and share already compiled code in memory. Children will still need to 'use' | ||
| 114 | # modules if they want to inherit from their name space. | ||
| 115 | # | ||
| 116 | sub fetch_modules($$@) { | ||
| 117 | my($reason, $required, @modules) = @_; | ||
| 118 | my(@missing); | ||
| 119 | for my $m (@modules) { | ||
| 120 | local($_) = $m; | ||
| 121 | $_ .= /^auto::/ ? '.al' : '.pm' if !/\.(pm|pl|al)\z/; | ||
| 122 | s[::][/]g; | ||
| 123 | eval { require $_ } or push(@missing, $m); | ||
| 124 | } | ||
| 125 | die "ERROR: MISSING $reason:\n" . join('', map { " $_\n" } @missing) | ||
| 126 | if $required && @missing; | ||
| 127 | \@missing; | ||
| 128 | } | ||
| 129 | |||
| 130 | BEGIN { | ||
| 131 | fetch_modules('REQUIRED BASIC MODULES', 1, qw( | ||
| 132 | Exporter POSIX Fcntl Socket Errno Carp Time::HiRes | ||
| 133 | IO::Handle IO::File IO::Socket IO::Socket::UNIX IO::Socket::INET | ||
| 134 | IO::Wrap IO::Stringy Digest::MD5 Unix::Syslog File::Basename | ||
| 135 | Mail::Field Mail::Address Mail::Header Mail::Internet Compress::Zlib | ||
| 136 | MIME::Base64 MIME::QuotedPrint MIME::Words | ||
| 137 | MIME::Head MIME::Body MIME::Entity MIME::Parser MIME::Decoder | ||
| 138 | MIME::Decoder::Base64 MIME::Decoder::Binary MIME::Decoder::QuotedPrint | ||
| 139 | MIME::Decoder::NBit MIME::Decoder::UU MIME::Decoder::Gzip64 | ||
| 140 | Net::Cmd Net::SMTP Net::Server Net::Server::PreForkSimple | ||
| 141 | )); | ||
| 142 | # with earlier versions of Perl one may need to add additional modules | ||
| 143 | # to the list, such as: auto::POSIX::setgid auto::POSIX::setuid ... | ||
| 144 | fetch_modules('OPTIONAL BASIC MODULES', 0, qw( | ||
| 145 | Carp::Heavy auto::POSIX::setgid auto::POSIX::setuid | ||
| 146 | MIME::Decoder::BinHex | ||
| 147 | )); | ||
| 148 | } | ||
| 149 | |||
| 150 | 1; | ||
| 151 | |||
| 152 | # | ||
| 153 | package Amavis::Conf; | ||
| 154 | use strict; | ||
| 155 | use re 'taint'; | ||
| 156 | |||
| 157 | # prototypes | ||
| 158 | sub D_REJECT(); | ||
| 159 | sub D_BOUNCE(); | ||
| 160 | sub D_DISCARD(); | ||
| 161 | sub D_PASS(); | ||
| 162 | |||
| 163 | BEGIN { | ||
| 164 | use Exporter (); | ||
| 165 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 166 | $VERSION = '2.043'; | ||
| 167 | @ISA = qw(Exporter); | ||
| 168 | %EXPORT_TAGS = ( | ||
| 169 | 'dynamic_confvars' => [qw( | ||
| 170 | $policy_bank_name $protocol @inet_acl | ||
| 171 | $log_level $log_templ $log_recip_templ $forward_method $notify_method | ||
| 172 | |||
| 173 | $amavis_auth_user $amavis_auth_pass $auth_reauthenticate_forwarded | ||
| 174 | $auth_required_out $auth_required_inp $auth_required_release | ||
| 175 | @auth_mech_avail | ||
| 176 | $local_client_bind_address | ||
| 177 | $localhost_name $smtpd_greeting_banner $smtpd_quit_banner | ||
| 178 | $smtpd_message_size_limit | ||
| 179 | |||
| 180 | $final_virus_destiny $final_spam_destiny | ||
| 181 | $final_banned_destiny $final_bad_header_destiny | ||
| 182 | $warnvirussender $warnspamsender $warnbannedsender $warnbadhsender | ||
| 183 | $warn_offsite | ||
| 184 | |||
| 185 | @av_scanners @av_scanners_backup $first_infected_stops_scan | ||
| 186 | $bypass_decode_parts @decoders | ||
| 187 | |||
| 188 | $defang_virus $defang_banned $defang_spam | ||
| 189 | $defang_bad_header $defang_undecipherable $defang_all | ||
| 190 | $undecipherable_subject_tag | ||
| 191 | $sa_spam_report_header $sa_spam_level_char | ||
| 192 | $sa_mail_body_size_limit | ||
| 193 | |||
| 194 | $localpart_is_case_sensitive | ||
| 195 | $recipient_delimiter $replace_existing_extension | ||
| 196 | $hdr_encoding $bdy_encoding $hdr_encoding_qb | ||
| 197 | $notify_xmailer_header $X_HEADER_TAG $X_HEADER_LINE | ||
| 198 | $remove_existing_x_scanned_headers $remove_existing_spam_headers | ||
| 199 | |||
| 200 | $hdrfrom_notify_sender $hdrfrom_notify_recip | ||
| 201 | $hdrfrom_notify_admin $hdrfrom_notify_spamadmin | ||
| 202 | $mailfrom_notify_sender $mailfrom_notify_recip | ||
| 203 | $mailfrom_notify_admin $mailfrom_notify_spamadmin | ||
| 204 | $mailfrom_to_quarantine | ||
| 205 | $virus_quarantine_method $spam_quarantine_method | ||
| 206 | $banned_files_quarantine_method $bad_header_quarantine_method | ||
| 207 | %local_delivery_aliases | ||
| 208 | |||
| 209 | $notify_sender_templ | ||
| 210 | $notify_virus_sender_templ $notify_spam_sender_templ | ||
| 211 | $notify_virus_admin_templ $notify_spam_admin_templ | ||
| 212 | $notify_virus_recips_templ $notify_spam_recips_templ | ||
| 213 | |||
| 214 | $banned_namepath_re | ||
| 215 | $per_recip_whitelist_sender_lookup_tables | ||
| 216 | $per_recip_blacklist_sender_lookup_tables | ||
| 217 | |||
| 218 | %sql_clause | ||
| 219 | |||
| 220 | @local_domains_maps @mynetworks_maps | ||
| 221 | @bypass_virus_checks_maps @bypass_spam_checks_maps | ||
| 222 | @bypass_banned_checks_maps @bypass_header_checks_maps | ||
| 223 | @virus_lovers_maps @spam_lovers_maps | ||
| 224 | @banned_files_lovers_maps @bad_header_lovers_maps | ||
| 225 | @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps | ||
| 226 | @newvirus_admin_maps @virus_admin_maps | ||
| 227 | @banned_admin_maps @bad_header_admin_maps @spam_admin_maps | ||
| 228 | @virus_quarantine_to_maps | ||
| 229 | @banned_quarantine_to_maps @bad_header_quarantine_to_maps | ||
| 230 | @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps | ||
| 231 | @banned_filename_maps | ||
| 232 | @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps | ||
| 233 | @spam_dsn_cutoff_level_maps @spam_quarantine_cutoff_level_maps | ||
| 234 | @spam_modifies_subj_maps @spam_subject_tag_maps @spam_subject_tag2_maps | ||
| 235 | @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps | ||
| 236 | @message_size_limit_maps | ||
| 237 | @addr_extension_virus_maps @addr_extension_spam_maps | ||
| 238 | @addr_extension_banned_maps @addr_extension_bad_header_maps | ||
| 239 | zobel | 143 | @debug_sender_maps %recipient_policy_bank_map %recipient_policy_bank_re_map $sa_site_rules_filename |
| 240 | zobel | 21 | )], |
| 241 | 'confvars' => [qw( | ||
| 242 | $myproduct_name $myversion_id $myversion_id_numeric $myversion_date | ||
| 243 | $myversion $myhostname | ||
| 244 | $MYHOME $TEMPBASE $QUARANTINEDIR $quarantine_subdir_levels | ||
| 245 | $daemonize $pid_file $lock_file $db_home | ||
| 246 | $enable_db $enable_global_cache | ||
| 247 | $daemon_user $daemon_group $daemon_chroot_dir $path | ||
| 248 | $DEBUG $DO_SYSLOG $SYSLOG_LEVEL $LOGFILE | ||
| 249 | $max_servers $max_requests $child_timeout | ||
| 250 | %current_policy_bank %policy_bank %interface_policy | ||
| 251 | $unix_socketname $inet_socket_port $inet_socket_bind | ||
| 252 | $insert_received_line $relayhost_is_client $smtpd_recipient_limit | ||
| 253 | $MAXLEVELS $MAXFILES | ||
| 254 | $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR | ||
| 255 | $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR | ||
| 256 | @lookup_sql_dsn @storage_sql_dsn | ||
| 257 | $virus_check_negative_ttl $virus_check_positive_ttl | ||
| 258 | $spam_check_negative_ttl $spam_check_positive_ttl | ||
| 259 | $enable_ldap $default_ldap | ||
| 260 | @keep_decoded_original_maps @map_full_type_to_short_type_maps | ||
| 261 | @viruses_that_fake_sender_maps %banned_rules | ||
| 262 | zobel | 143 | $file %recipient_policy_bank_map %recipient_policy_bank_re_map $sa_site_rules_filename |
| 263 | zobel | 21 | )], |
| 264 | 'sa' => [qw( | ||
| 265 | $helpers_home $dspam | ||
| 266 | $sa_local_tests_only $sa_auto_whitelist $sa_timeout $sa_debug | ||
| 267 | $sa_site_rules_filename | ||
| 268 | )], | ||
| 269 | 'platform' => [qw( | ||
| 270 | $can_truncate $unicode_aware $eol | ||
| 271 | &D_REJECT &D_BOUNCE &D_DISCARD &D_PASS | ||
| 272 | )], | ||
| 273 | |||
| 274 | # other variables settable by user in amavisd.conf, | ||
| 275 | # but not directly accessible by the program | ||
| 276 | 'hidden_confvars' => [qw( | ||
| 277 | $mydomain | ||
| 278 | )], | ||
| 279 | |||
| 280 | # legacy variables, predeclared for compatibility of amavisd.conf | ||
| 281 | # The rest of the program does not use them directly and they should not be | ||
| 282 | # visible in other modules, but may be referenced through @*_maps variables | ||
| 283 | 'legacy_confvars' => [qw( | ||
| 284 | %local_domains @local_domains_acl $local_domains_re @mynetworks | ||
| 285 | %bypass_virus_checks @bypass_virus_checks_acl $bypass_virus_checks_re | ||
| 286 | %bypass_spam_checks @bypass_spam_checks_acl $bypass_spam_checks_re | ||
| 287 | %bypass_banned_checks @bypass_banned_checks_acl $bypass_banned_checks_re | ||
| 288 | %bypass_header_checks @bypass_header_checks_acl $bypass_header_checks_re | ||
| 289 | %virus_lovers @virus_lovers_acl $virus_lovers_re | ||
| 290 | %spam_lovers @spam_lovers_acl $spam_lovers_re | ||
| 291 | %banned_files_lovers @banned_files_lovers_acl $banned_files_lovers_re | ||
| 292 | %bad_header_lovers @bad_header_lovers_acl $bad_header_lovers_re | ||
| 293 | %virus_admin %spam_admin | ||
| 294 | $newvirus_admin $virus_admin $banned_admin $bad_header_admin $spam_admin | ||
| 295 | $warnvirusrecip $warnbannedrecip $warnbadhrecip | ||
| 296 | $virus_quarantine_to $banned_quarantine_to $bad_header_quarantine_to | ||
| 297 | $spam_quarantine_to $spam_quarantine_bysender_to | ||
| 298 | $keep_decoded_original_re $map_full_type_to_short_type_re | ||
| 299 | $banned_filename_re $viruses_that_fake_sender_re | ||
| 300 | $sa_tag_level_deflt $sa_tag2_level_deflt $sa_kill_level_deflt | ||
| 301 | $sa_dsn_cutoff_level $sa_quarantine_cutoff_level | ||
| 302 | $sa_spam_modifies_subj $sa_spam_subject_tag1 $sa_spam_subject_tag | ||
| 303 | %whitelist_sender @whitelist_sender_acl $whitelist_sender_re | ||
| 304 | %blacklist_sender @blacklist_sender_acl $blacklist_sender_re | ||
| 305 | $addr_extension_virus $addr_extension_spam | ||
| 306 | $addr_extension_banned $addr_extension_bad_header | ||
| 307 | $sql_select_policy $sql_select_white_black_list | ||
| 308 | $gets_addr_in_quoted_form @debug_sender_acl | ||
| 309 | $arc $bzip2 $lzop $lha $unarj $gzip $uncompress $unfreeze | ||
| 310 | $unrar $zoo $pax $cpio $ar $rpm2cpio $cabextract $ripole $tnef | ||
| 311 | $gunzip $bunzip2 $unlzop | ||
| 312 | )], | ||
| 313 | ); | ||
| 314 | Exporter::export_tags qw(dynamic_confvars confvars sa platform | ||
| 315 | hidden_confvars legacy_confvars); | ||
| 316 | } # BEGIN | ||
| 317 | |||
| 318 | use POSIX (); | ||
| 319 | use Carp (); | ||
| 320 | use Errno qw(ENOENT EACCES); | ||
| 321 | |||
| 322 | use vars @EXPORT; | ||
| 323 | |||
| 324 | sub c($); sub cr($); sub ca($); # prototypes | ||
| 325 | use subs qw(c cr ca); # access subroutine to new-style config variables | ||
| 326 | BEGIN { push(@EXPORT,qw(c cr ca)) } | ||
| 327 | |||
| 328 | { # initialize policy bank hash containing dynamic config settings | ||
| 329 | for my $tag (@EXPORT_TAGS{'dynamic_confvars'}) { | ||
| 330 | for my $v (@$tag) { | ||
| 331 | if ($v !~ /^([%\$\@])(.*)\z/) { die "Unsupported variable type: $v" } | ||
| 332 | else { | ||
| 333 | no strict 'refs'; my($type,$name) = ($1,$2); | ||
| 334 | $current_policy_bank{$name} = $type eq '$' ? \${"Amavis::Conf::$name"} | ||
| 335 | : $type eq '@' ? \@{"Amavis::Conf::$name"} | ||
| 336 | : $type eq '%' ? \%{"Amavis::Conf::$name"} | ||
| 337 | : undef; | ||
| 338 | } | ||
| 339 | } | ||
| 340 | } | ||
| 341 | $current_policy_bank{'policy_bank_name'} = ''; # builtin policy | ||
| 342 | $current_policy_bank{'policy_bank_path'} = ''; | ||
| 343 | $policy_bank{''} = { %current_policy_bank }; # copy | ||
| 344 | } | ||
| 345 | |||
| 346 | # new-style access to dynamic config variables | ||
| 347 | # return a config variable value - usually a scalar; | ||
| 348 | # one level of indirection for scalars is allowed | ||
| 349 | sub c($) { | ||
| 350 | my($name) = @_; | ||
| 351 | if (!exists $current_policy_bank{$name}) { | ||
| 352 | Carp::croak(sprintf('No entry "%s" in policy bank "%s"', | ||
| 353 | $name, $current_policy_bank{'policy_bank_name'})); | ||
| 354 | } | ||
| 355 | my($var) = $current_policy_bank{$name}; my($r) = ref($var); | ||
| 356 | !$r ? $var : $r eq 'SCALAR' ? $$var | ||
| 357 | : $r eq 'ARRAY' ? @$var : $r eq 'HASH' ? %$var : $var; | ||
| 358 | } | ||
| 359 | |||
| 360 | # return a ref to a config variable value, or undef if var is undefined | ||
| 361 | sub cr($) { | ||
| 362 | my($name) = @_; | ||
| 363 | if (!exists $current_policy_bank{$name}) { | ||
| 364 | Carp::croak(sprintf('No entry "%s" in policy bank "%s"', | ||
| 365 | $name, $current_policy_bank{'policy_bank_name'})); | ||
| 366 | } | ||
| 367 | my($var) = $current_policy_bank{$name}; | ||
| 368 | !defined($var) ? undef : !ref($var) ? \$var : $var; | ||
| 369 | } | ||
| 370 | |||
| 371 | # return a ref to a config variable value (which is supposed to be an array), | ||
| 372 | # converting undef to an empty array, and a scalar to a one-element array | ||
| 373 | # if necessary | ||
| 374 | sub ca($) { | ||
| 375 | my($name) = @_; | ||
| 376 | if (!exists $current_policy_bank{$name}) { | ||
| 377 | Carp::croak(sprintf('No entry "%s" in policy bank "%s"', | ||
| 378 | $name, $current_policy_bank{'policy_bank_name'})); | ||
| 379 | } | ||
| 380 | my($var) = $current_policy_bank{$name}; | ||
| 381 | !defined($var) ? [] : !ref($var) ? [$var] : $var; | ||
| 382 | } | ||
| 383 | |||
| 384 | $myproduct_name = 'amavisd-new'; | ||
| 385 | $myversion_id = '2.3.3'; $myversion_date = '20050822'; | ||
| 386 | |||
| 387 | $myversion = "$myproduct_name-$myversion_id ($myversion_date)"; | ||
| 388 | $myversion_id_numeric = # x.yyyzzz, allows numerical comparision, like Perl $] | ||
| 389 | sprintf("%8.6f", $1 + ($2 + $3/1000)/1000) | ||
| 390 | if $myversion_id =~ /^(\d+)(?:\.(\d*)(?:\.(\d*))?)?(.*)$/; | ||
| 391 | |||
| 392 | $eol = "\n"; # native record separator in files: LF or CRLF or even CR | ||
| 393 | $unicode_aware = $]>=5.008 && length("\x{263a}")==1 && eval { require Encode }; | ||
| 394 | |||
| 395 | # serves only as a quick default for other configuration settings | ||
| 396 | $MYHOME = '/var/amavis'; | ||
| 397 | $mydomain = '!change-mydomain-variable!.example.com';#intentionally bad default | ||
| 398 | |||
| 399 | # Create debugging output - true: log to stderr; false: log to syslog/file | ||
| 400 | $DEBUG = 0; | ||
| 401 | |||
| 402 | # Cause Net::Server parameters 'background' and 'setsid' to be set, | ||
| 403 | # resulting in the program to detach itself from the terminal | ||
| 404 | $daemonize = 1; | ||
| 405 | |||
| 406 | # Net::Server pre-forking settings - defaults, overruled by amavisd.conf | ||
| 407 | $max_servers = 2; # number of pre-forked children | ||
| 408 | $max_requests = 10; # retire a child after that many accepts | ||
| 409 | |||
| 410 | $child_timeout = 8*60; # abort child if it does not complete each task in n sec | ||
| 411 | |||
| 412 | # Can file be truncated? | ||
| 413 | # Set to 1 if 'truncate' works (it is XPG4-UNIX standard feature, | ||
| 414 | # not required by Posix). | ||
| 415 | # Things will go faster with SMTP-in, otherwise (e.g. with milter) | ||
| 416 | # it makes no difference as file truncation will not be used. | ||
| 417 | $can_truncate = 1; | ||
| 418 | |||
| 419 | # expiration time of cached results: time to live in seconds | ||
| 420 | # (how long the result of a virus/spam test remains valid) | ||
| 421 | $virus_check_negative_ttl= 3*60; # time to remember that mail was not infected | ||
| 422 | $virus_check_positive_ttl= 30*60; # time to remember that mail was infected | ||
| 423 | $spam_check_negative_ttl = 30*60; # time to remember that mail was not spam | ||
| 424 | $spam_check_positive_ttl = 30*60; # time to remember that mail was spam | ||
| 425 | # | ||
| 426 | # NOTE: | ||
| 427 | # Cache size will be determined by the largest of the $*_ttl values. | ||
| 428 | # Depending on the mail rate, the cache database may grow quite large. | ||
| 429 | # Reasonable compromise for the max value is 15 minutes to 2 hours. | ||
| 430 | |||
| 431 | # Customizable notification messages, logging | ||
| 432 | |||
| 433 | $SYSLOG_LEVEL = 'mail.debug'; | ||
| 434 | |||
| 435 | $enable_db = 0; # load optional modules Amavis::DB & Amavis::DB::SNMP | ||
| 436 | $enable_global_cache = 0; # enable use of bdb-based Amavis::Cache | ||
| 437 | |||
| 438 | # Where to find SQL server(s) and database to support SQL lookups? | ||
| 439 | # A list of triples: (dsn,user,passw). Specify more than one | ||
| 440 | # for multiple (backup) SQL servers. | ||
| 441 | # | ||
| 442 | #@storage_sql_dsn = | ||
| 443 | #@lookup_sql_dsn = | ||
| 444 | # ( ['DBI:mysql:mail:host1', 'some-username1', 'some-password1'], | ||
| 445 | # ['DBI:mysql:mail:host2', 'some-username2', 'some-password2'] ); | ||
| 446 | |||
| 447 | # The SQL select clause to fetch per-recipient policy settings | ||
| 448 | # The %k will be replaced by a comma-separated list of query addresses | ||
| 449 | # (e.g. full address, domain only, catchall). Use ORDER, if there | ||
| 450 | # is a chance that multiple records will match - the first match wins | ||
| 451 | # If field names are not unique (e.g. 'id'), the later field overwrites the | ||
| 452 | # earlier in a hash returned by lookup, which is why we use '*,users.id'. | ||
| 453 | $sql_select_policy = | ||
| 454 | 'SELECT *,users.id FROM users LEFT JOIN policy ON users.policy_id=policy.id'. | ||
| 455 | ' WHERE users.email IN (%k) ORDER BY users.priority DESC'; | ||
| 456 | |||
| 457 | # The SQL select clause to check sender in per-recipient whitelist/blacklist | ||
| 458 | # The first SELECT argument '?' will be users.id from recipient SQL lookup, | ||
| 459 | # the %k will be sender addresses (e.g. full address, domain only, catchall). | ||
| 460 | # Only the first occurrence of '?' will be replaced by users.id, subsequent | ||
| 461 | # occurrences of '?' will see empty string as an argument. There can be zero | ||
| 462 | # or more occurrences of %k, lookup keys will be multiplied accordingly. | ||
| 463 | # Up until version 2.2.0 the '?' had to be placed before the '%k'; | ||
| 464 | # starting with 2.2.1 this restriction is lifted. | ||
| 465 | $sql_select_white_black_list = | ||
| 466 | 'SELECT wb FROM wblist LEFT JOIN mailaddr ON wblist.sid=mailaddr.id'. | ||
| 467 | ' WHERE (wblist.rid=?) AND (mailaddr.email IN (%k))'. | ||
| 468 | ' ORDER BY mailaddr.priority DESC'; | ||
| 469 | |||
| 470 | %sql_clause = ( | ||
| 471 | 'sel_policy' => \$sql_select_policy, | ||
| 472 | 'sel_wblist' => \$sql_select_white_black_list, | ||
| 473 | 'sel_adr' => | ||
| 474 | 'SELECT id FROM maddr WHERE email=?', | ||
| 475 | 'ins_adr' => | ||
| 476 | 'INSERT INTO maddr (email, domain) VALUES (?,?)', | ||
| 477 | 'ins_msg' => | ||
| 478 | 'INSERT INTO msgs (mail_id, secret_id, am_id, time_num, time_iso, sid,'. | ||
| 479 | ' policy, client_addr, size, host) VALUES (?,?,?,?,?,?,?,?,?,?)', | ||
| 480 | 'upd_msg' => | ||
| 481 | 'UPDATE msgs SET content=?, quar_type=?, dsn_sent=?, spam_level=?,'. | ||
| 482 | ' message_id=?, from_addr=?, subject=? WHERE mail_id=?', | ||
| 483 | 'ins_rcp' => | ||
| 484 | 'INSERT INTO msgrcpt (mail_id, rid, ds, rs, bl, wl, bspam_level,'. | ||
| 485 | ' smtp_resp) VALUES (?,?,?,?,?,?,?,?)', | ||
| 486 | 'ins_quar' => | ||
| 487 | 'INSERT INTO quarantine (mail_id, chunk_ind, mail_text) VALUES (?,?,?)', | ||
| 488 | 'sel_quar' => | ||
| 489 | 'SELECT mail_text FROM quarantine WHERE mail_id=? ORDER BY chunk_ind', | ||
| 490 | ); | ||
| 491 | |||
| 492 | # | ||
| 493 | # Receiving mail related | ||
| 494 | |||
| 495 | # $unix_socketname = '/var/amavis/amavisd.sock'; # traditional amavis client protocol | ||
| 496 | # $inet_socket_port = 10024; # accept SMTP on this TCP port | ||
| 497 | # $inet_socket_port = [10024,10026,10027]; # ...possibly on more than one | ||
| 498 | $inet_socket_bind = '127.0.0.1'; # limit socket bind to loopback interface | ||
| 499 | |||
| 500 | @inet_acl = qw( 127.0.0.1 [::1] ); # allow SMTP access only from localhost | ||
| 501 | @mynetworks = qw( 127.0.0.0/8 [::1] [FE80::]/10 [FEC0::]/10 | ||
| 502 | 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 ); | ||
| 503 | |||
| 504 | $notify_method = 'smtp:[127.0.0.1]:10025'; | ||
| 505 | $forward_method = 'smtp:[127.0.0.1]:10025'; | ||
| 506 | |||
| 507 | #old defaults: | ||
| 508 | # $virus_quarantine_method = 'local:virus-%i-%n'; | ||
| 509 | # $spam_quarantine_method = 'local:spam-%b-%i-%n.gz'; | ||
| 510 | # $banned_files_quarantine_method = 'local:banned-%i-%n'; | ||
| 511 | # $bad_header_quarantine_method = 'local:badh-%i-%n'; | ||
| 512 | |||
| 513 | #new defaults: | ||
| 514 | $virus_quarantine_method = 'local:virus-%m'; | ||
| 515 | $spam_quarantine_method = 'local:spam-%m.gz'; | ||
| 516 | $banned_files_quarantine_method = 'local:banned-%m'; | ||
| 517 | $bad_header_quarantine_method = 'local:badh-%m'; | ||
| 518 | |||
| 519 | $insert_received_line = 1; # insert 'Received:' header field? (not with milter) | ||
| 520 | $remove_existing_x_scanned_headers = 0; | ||
| 521 | $remove_existing_spam_headers = 1; | ||
| 522 | |||
| 523 | # encoding (charset in MIME terminology) | ||
| 524 | # to be used in RFC 2047-encoded ... | ||
| 525 | $hdr_encoding = 'iso-8859-1'; # ... header field bodies | ||
| 526 | $bdy_encoding = 'iso-8859-1'; # ... notification body text | ||
| 527 | |||
| 528 | # encoding (encoding in MIME terminology) | ||
| 529 | $hdr_encoding_qb = 'Q'; # quoted-printable (default) | ||
| 530 | #$hdr_encoding_qb = 'B'; # base64 (usual for far east charsets) | ||
| 531 | |||
| 532 | $smtpd_recipient_limit = 1100; # max recipients (RCPT TO) - sanity limit | ||
| 533 | |||
| 534 | # $myhostname is used by SMTP server module in the initial SMTP welcome line, | ||
| 535 | # in inserted 'Received:' lines, Message-ID in notifications, log entries, ... | ||
| 536 | $myhostname = (POSIX::uname)[1]; # should be a FQDN ! | ||
| 537 | |||
| 538 | $smtpd_greeting_banner = '${helo-name} ${protocol} ${product} service ready'; | ||
| 539 | $smtpd_quit_banner = '${helo-name} ${product} closing transmission channel'; | ||
| 540 | |||
| 541 | # $localhost_name is the name of THIS host running amavisd | ||
| 542 | # (typically 'localhost'). It is used in HELO SMTP command | ||
| 543 | # when reinjecting mail back to MTA via SMTP for final delivery. | ||
| 544 | $localhost_name = 'localhost'; | ||
| 545 | |||
| 546 | # @auth_mech_avail = ('PLAIN','LOGIN'); # empty list disables incoming AUTH | ||
| 547 | #$auth_required_inp = 1; # incoming SMTP authentication required by amavisd? | ||
| 548 | #$auth_required_out = 1; # SMTP authentication required by MTA | ||
| 549 | $auth_required_release = 1; # secret_id is required for a quarantine release | ||
| 550 | |||
| 551 | # SMTP AUTH username and password for notification submissions | ||
| 552 | # (and reauthentication of forwarded mail if requested) | ||
| 553 | #$amavis_auth_user = undef; # perhaps: 'amavisd' | ||
| 554 | #$amavis_auth_pass = undef; | ||
| 555 | #$auth_reauthenticate_forwarded = undef; # supply our own credentials also | ||
| 556 | # for forwarded (passed) mail | ||
| 557 | |||
| 558 | # whom quarantined messages appear to be sent from (envelope sender) | ||
| 559 | # $mailfrom_to_quarantine = undef; # original sender if undef, or set explicitly | ||
| 560 | |||
| 561 | # where to send quarantined malware | ||
| 562 | # Specify undef to disable, or e-mail address containing '@', | ||
| 563 | # or just a local part, which will be mapped by %local_delivery_aliases | ||
| 564 | # into local mailbox name or directory. The lookup key is a recipient address | ||
| 565 | $virus_quarantine_to = 'virus-quarantine'; # %local_delivery_aliases mapped | ||
| 566 | $banned_quarantine_to = 'banned-quarantine'; # %local_delivery_aliases mapped | ||
| 567 | $bad_header_quarantine_to = 'bad-header-quarantine'; # %local_delivery_aliases | ||
| 568 | $spam_quarantine_to = 'spam-quarantine'; # %local_delivery_aliases mapped | ||
| 569 | |||
| 570 | $banned_admin = \@virus_admin_maps; # compatibility | ||
| 571 | $bad_header_admin = \@virus_admin_maps; # compatibility | ||
| 572 | |||
| 573 | # similar to $spam_quarantine_to, but the lookup key is the sender address | ||
| 574 | $spam_quarantine_bysender_to = undef; # dflt: no by-sender spam quarantine | ||
| 575 | |||
| 576 | # quarantine directory or mailbox file or empty | ||
| 577 | # (only used if $virus_quarantine_to specifies direct local delivery) | ||
| 578 | $QUARANTINEDIR = undef; # no quarantine unless overridden by config | ||
| 579 | |||
| 580 | $undecipherable_subject_tag = '***UNCHECKED*** '; | ||
| 581 | |||
| 582 | # string to prepend to Subject header field when message qualifies as spam | ||
| 583 | # $sa_spam_subject_tag1 = undef; # example: '***possible SPAM*** ' | ||
| 584 | # $sa_spam_subject_tag = undef; # example: '***SPAM*** ' | ||
| 585 | $sa_spam_modifies_subj = 1; # true for compatibility; can be a | ||
| 586 | # lookup table indicating per-recip settings | ||
| 587 | $sa_spam_level_char = '*'; # character to be used in X-Spam-Level bar; | ||
| 588 | # empty or undef disables adding this header field | ||
| 589 | # $sa_spam_report_header = undef; # insert X-Spam-Report header field? | ||
| 590 | $sa_local_tests_only = 0; | ||
| 591 | $sa_debug = undef; | ||
| 592 | $sa_timeout = 30; # timeout in seconds for a call to SpamAssassin | ||
| 593 | |||
| 594 | # MIME defanging is only done when enabled and malware is allowed to pass | ||
| 595 | # $defang_virus = undef; | ||
| 596 | # $defang_banned = undef; | ||
| 597 | # $defang_spam = undef; | ||
| 598 | # $defang_bad_header = undef; | ||
| 599 | # $defang_undecipherable = undef; | ||
| 600 | # $defang_all = undef; | ||
| 601 | |||
| 602 | $file = 'file'; # path to the file(1) utility for classifying contents | ||
| 603 | |||
| 604 | $MIN_EXPANSION_FACTOR = 5; # times original mail size | ||
| 605 | $MAX_EXPANSION_FACTOR = 500; # times original mail size | ||
| 606 | |||
| 607 | # See amavisd.conf and README.lookups for details. | ||
| 608 | |||
| 609 | # What to do with the message (this is independent of quarantining): | ||
| 610 | # Reject: tell MTA to generate a non-delivery notification, MTA gets 5xx | ||
| 611 | # Bounce: generate a non-delivery notification by ourselves, MTA gets 250 | ||
| 612 | # Discard: drop the message and pretend it was delivered, MTA gets 250 | ||
| 613 | # Pass: deliver/accept the message | ||
| 614 | # | ||
| 615 | # Bounce and Reject are similar: in both cases sender gets a non-delivery | ||
| 616 | # notification, either generated by amavisd-new, or by MTA. The notification | ||
| 617 | # issued by amavisd-new may be more informative, while on the other hand | ||
| 618 | # MTA may be able to do a true reject on the original SMTP session | ||
| 619 | # (e.g. with sendmail milter), or else it just generates normal non-delivery | ||
| 620 | # notification / bounce (e.g. with Postfix, Exim). As a consequence, | ||
| 621 | # with Postfix and Exim and dual-sendmail setup the Bounce is more informative | ||
| 622 | # than Reject, but sendmail-milter users may prefer Reject. | ||
| 623 | # | ||
| 624 | # Bounce and Discard are similar: in both cases amavisd-new confirms | ||
| 625 | # to MTA the message reception with success code 250. The difference is | ||
| 626 | # in sender notification: Bounce sends a non-delivery notification to sender, | ||
| 627 | # Discard does not, the message is silently dropped. Quarantine and | ||
| 628 | # admin notifications are not affected by any of these settings. | ||
| 629 | # | ||
| 630 | # COMPATIBITITY NOTE: the separation of *_destiny values into | ||
| 631 | # D_BOUNCE, D_REJECT, D_DISCARD and D_PASS made settings $warnvirussender | ||
| 632 | # and $warnspamsender only still useful with D_PASS. The combination of | ||
| 633 | # D_DISCARD + $warn*sender=1 is mapped into D_BOUNCE for compatibility. | ||
| 634 | |||
| 635 | # intentionally leave value -1 unassigned for compatibility | ||
| 636 | sub D_REJECT () { -3 } | ||
| 637 | sub D_BOUNCE () { -2 } | ||
| 638 | sub D_DISCARD() { 0 } | ||
| 639 | sub D_PASS () { 1 } | ||
| 640 | |||
| 641 | # The following symbolic constants can be used in *destiny settings: | ||
| 642 | # | ||
| 643 | # D_PASS mail will pass to recipients, regardless of contents; | ||
| 644 | # | ||
| 645 | # D_DISCARD mail will not be delivered to its recipients, sender will NOT be | ||
| 646 | # notified. Effectively we lose mail (but it will be quarantined | ||
| 647 | # unless disabled). | ||
| 648 | # | ||
| 649 | # D_BOUNCE mail will not be delivered to its recipients, a non-delivery | ||
| 650 | # notification (bounce) will be sent to the sender by amavisd-new; | ||
| 651 | # Exception: bounce (DSN) will not be sent if a virus name matches | ||
| 652 | # $viruses_that_fake_sender_maps, or to messages from mailing lists | ||
| 653 | # (Precedence: bulk|list|junk), or for spam exceeding | ||
| 654 | # spam_dsn_cutoff_level | ||
| 655 | # | ||
| 656 | # D_REJECT mail will not be delivered to its recipients, sender should | ||
| 657 | # preferably get a reject, e.g. SMTP permanent reject response | ||
| 658 | # (e.g. with milter), or non-delivery notification from MTA | ||
| 659 | # (e.g. Postfix). If this is not possible (e.g. different recipients | ||
| 660 | # have different tolerances to bad mail contents and not using LMTP) | ||
| 661 | # amavisd-new sends a bounce by itself (same as D_BOUNCE). | ||
| 662 | # | ||
| 663 | # Notes: | ||
| 664 | # D_REJECT and D_BOUNCE are similar, the difference is in who is responsible | ||
| 665 | # for informing the sender about non-delivery, and how informative | ||
| 666 | # the notification can be (amavisd-new knows more than MTA); | ||
| 667 | # With D_REJECT, MTA may reject original SMTP, or send DSN (delivery status | ||
| 668 | # notification, colloquially called 'bounce') - depending on MTA; | ||
| 669 | # Best suited for sendmail milter, especially for spam. | ||
| 670 | # With D_BOUNCE, amavisd-new (not MTA) sends DSN (can better explain the | ||
| 671 | # reason for mail non-delivery but unable to reject the original | ||
| 672 | # SMTP session, and is in position to suppress DSN if considered | ||
| 673 | # unsuitable). Best suited for Postfix and other dual-MTA setups. | ||
| 674 | |||
| 675 | $final_virus_destiny = D_DISCARD; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS | ||
| 676 | $final_banned_destiny = D_BOUNCE; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS | ||
| 677 | $final_spam_destiny = D_BOUNCE; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS | ||
| 678 | $final_bad_header_destiny = D_PASS; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS | ||
| 679 | |||
| 680 | # If you decide to pass viruses (or spam) to certain users using | ||
| 681 | # @virus_lovers_maps, (or @spam_lovers_maps), or $final_virus_destiny=D_PASS | ||
| 682 | # ($final_spam_destiny=D_PASS), you can set the variable $addr_extension_virus | ||
| 683 | # ($addr_extension_spam) to some string, and the recipient address will have | ||
| 684 | # this string appended as an address extension to the local-part of the | ||
| 685 | # address. This extension can be used by final local delivery agent to place | ||
| 686 | # such mail in different folders. Leave these variables undefined or empty | ||
| 687 | # strings to prevent appending address extensions. Setting has no effect | ||
| 688 | # on users which will not be receiving viruses (spam). Recipients which | ||
| 689 | # do not match access lists in @local_domains_maps are not affected (i.e. | ||
| 690 | # non-local recipients do not get address extension appended). | ||
| 691 | # | ||
| 692 | # LDAs usually default to stripping away address extension if no special | ||
| 693 | # handling for it is specified, so having this option enabled normally | ||
| 694 | # does no harm, provided the $recipients_delimiter character matches | ||
| 695 | # the setting at the final MTA's local delivery agent (LDA). | ||
| 696 | # | ||
| 697 | # $addr_extension_virus = 'virus'; # for example | ||
| 698 | # $addr_extension_spam = 'spam'; | ||
| 699 | # $addr_extension_banned = 'banned'; | ||
| 700 | # $addr_extension_bad_header = 'badh'; | ||
| 701 | |||
| 702 | # Delimiter between local part of the recipient address and address extension | ||
| 703 | # (which can optionally be added, see variables $addr_extension_virus and | ||
| 704 | # $addr_extension_spam). E.g. recipient address <user@domain.example> gets | ||
| 705 | # changed to <user+virus@domain.example>. | ||
| 706 | # | ||
| 707 | # Delimiter should match equivalent (final) MTA delimiter setting. | ||
| 708 | # (e.g. for Postfix add 'recipient_delimiter = +' to main.cf). | ||
| 709 | # Setting it to an empty string or to undef disables this feature | ||
| 710 | # regardless of $addr_extension_virus and $addr_extension_spam settings. | ||
| 711 | |||
| 712 | # $recipient_delimiter = '+'; | ||
| 713 | $replace_existing_extension = 1; # true: replace ext; false: append ext | ||
| 714 | |||
| 715 | # Affects matching of localpart of e-mail addresses (left of '@') | ||
| 716 | # in lookups: true = case sensitive, false = case insensitive | ||
| 717 | $localpart_is_case_sensitive = 0; | ||
| 718 | |||
| 719 | # first match wins, more specific entries should precede general ones! | ||
| 720 | # the result may be a string or a ref to a list of strings; | ||
| 721 | # see also sub decompose_part() | ||
| 722 | $map_full_type_to_short_type_re = Amavis::Lookup::RE->new( | ||
| 723 | [qr/^empty\z/ => 'empty'], | ||
| 724 | [qr/^directory\z/ => 'dir'], | ||
| 725 | [qr/^can't (stat|read)\b/ => 'dat'], # file(1) diagnostics | ||
| 726 | [qr/^cannot open\b/ => 'dat'], # file(1) diagnostics | ||
| 727 | [qr/^ERROR: Corrupted\b/ => 'dat'], # file(1) diagnostics | ||
| 728 | [qr/can't read magic file|couldn't find any magic files/ => 'dat'], | ||
| 729 | [qr/^data\z/ => 'dat'], | ||
| 730 | |||
| 731 | [qr/^ISO-8859.*\btext\b/ => 'txt'], | ||
| 732 | [qr/^Non-ISO.*ASCII\b.*\btext\b/ => 'txt'], | ||
| 733 | [qr/^Unicode\b.*\btext\b/i => 'txt'], | ||
| 734 | [qr/^'diff' output text\b/ => 'txt'], | ||
| 735 | [qr/^GNU message catalog\b/ => 'mo'], | ||
| 736 | [qr/^PGP encrypted data\b/ => 'pgp'], | ||
| 737 | [qr/^PGP armored data( signed)? message\b/ => ['pgp','pgp.asc'] ], | ||
| 738 | [qr/^PGP armored\b/ => ['pgp','pgp.asc'] ], | ||
| 739 | |||
| 740 | ### 'file' is a bit too trigger happy to claim something is 'mail text' | ||
| 741 | # [qr/^RFC 822 mail text\b/ => 'mail'], | ||
| 742 | [qr/^(ASCII|smtp|RFC 822) mail text\b/ => 'txt'], | ||
| 743 | |||
| 744 | [qr/^JPEG image data\b/ =>['image','jpg'] ], | ||
| 745 | [qr/^GIF image data\b/ =>['image','gif'] ], | ||
| 746 | [qr/^PNG image data\b/ =>['image','png'] ], | ||
| 747 | [qr/^TIFF image data\b/ =>['image','tif'] ], | ||
| 748 | [qr/^PCX\b.*\bimage data\b/ =>['image','pcx'] ], | ||
| 749 | [qr/^PC bitmap data\b/ =>['image','bmp'] ], | ||
| 750 | |||
| 751 | [qr/^MP2\b/ =>['audio','mpa','mp2'] ], | ||
| 752 | [qr/^MP3\b/ =>['audio','mpa','mp3'] ], | ||
| 753 | [qr/^MPEG video stream data\b/ =>['movie','mpv'] ], | ||
| 754 | [qr/^MPEG system stream data\b/ =>['movie','mpg'] ], | ||
| 755 | [qr/^MPEG\b/ =>['movie','mpg'] ], | ||
| 756 | [qr/^Microsoft ASF\b/ =>['movie','wmv'] ], | ||
| 757 | [qr/^RIFF\b.*\bAVI\b/ =>['movie','avi'] ], | ||
| 758 | [qr/^RIFF\b.*\bWAVE audio\b/ =>['audio','wav'] ], | ||
| 759 | |||
| 760 | [qr/^Macromedia Flash data\b/ => 'swf'], | ||
| 761 | [qr/^HTML document text\b/ => 'html'], | ||
| 762 | [qr/^XML document text\b/ => 'xml'], | ||
| 763 | [qr/^exported SGML document text\b/ => 'sgml'], | ||
| 764 | [qr/^PostScript document text\b/ => 'ps'], | ||
| 765 | [qr/^PDF document\b/ => 'pdf'], | ||
| 766 | [qr/^Rich Text Format data\b/ => 'rtf'], | ||
| 767 | [qr/^Microsoft Office Document\b/i => 'doc'], # OLE2: doc, ppt, xls, ... | ||
| 768 | [qr/^LaTeX\b.*\bdocument text\b/ => 'lat'], | ||
| 769 | [qr/^TeX DVI file\b/ => 'dvi'], | ||
| 770 | [qr/\bdocument text\b/ => 'txt'], | ||
| 771 | [qr/^compiled Java class data\b/ => 'java'], | ||
| 772 | [qr/^MS Windows 95 Internet shortcut text\b/ => 'url'], | ||
| 773 | |||
| 774 | [qr/^frozen\b/ => 'F'], | ||
| 775 | [qr/^gzip compressed\b/ => 'gz'], | ||
| 776 | [qr/^bzip compressed\b/ => 'bz'], | ||
| 777 | [qr/^bzip2 compressed\b/ => 'bz2'], | ||
| 778 | [qr/^lzop compressed\b/ => 'lzo'], | ||
| 779 | [qr/^compress'd/ => 'Z'], | ||
| 780 | [qr/^Zip archive\b/i => 'zip'], | ||
| 781 | [qr/^RAR archive\b/i => 'rar'], | ||
| 782 | [qr/^LHa.*\barchive\b/i => 'lha'], # (also known as .lzh) | ||
| 783 | [qr/^ARC archive\b/i => 'arc'], | ||
| 784 | [qr/^ARJ archive\b/i => 'arj'], | ||
| 785 | [qr/^Zoo archive\b/i => 'zoo'], | ||
| 786 | [qr/^(\S+\s+)?tar archive\b/i => 'tar'], | ||
| 787 | [qr/^(\S+\s+)?cpio archive\b/i => 'cpio'], | ||
| 788 | [qr/^Debian binary package\b/i => 'deb'], # standard Unix archive (ar) | ||
| 789 | [qr/^current ar archive\b/i => 'a'], # standard Unix archive (ar) | ||
| 790 | [qr/^RPM\b/ => 'rpm'], | ||
| 791 | [qr/^(Transport Neutral Encapsulation Format|TNEF)\b/i => 'tnef'], | ||
| 792 | [qr/^Microsoft cabinet file\b/ => 'cab'], | ||
| 793 | |||
| 794 | [qr/^(uuencoded|xxencoded)\b/i => 'uue'], | ||
| 795 | [qr/^binhex\b/i => 'hqx'], | ||
| 796 | [qr/^(ASCII|text)\b/i => 'asc'], | ||
| 797 | [qr/^Emacs.*byte-compiled Lisp data/i => 'asc'], # BinHex with an empty line | ||
| 798 | [qr/\bscript text executable\b/ => 'txt'], | ||
| 799 | |||
| 800 | [qr/^MS-DOS\b.*\bexecutable\b/ => ['exe','exe-ms'] ], | ||
| 801 | [qr/^MS Windows\b.*\bexecutable\b/ => ['exe','exe-ms'] ], | ||
| 802 | [qr/^PA-RISC.*\bexecutable\b/ => ['exe','exe-unix'] ], | ||
| 803 | [qr/^ELF .*\bexecutable\b/ => ['exe','exe-unix'] ], | ||
| 804 | [qr/^COFF format .*\bexecutable\b/ => ['exe','exe-unix'] ], | ||
| 805 | [qr/^executable \(RISC System\b/ => ['exe','exe-unix'] ], | ||
| 806 | [qr/^VMS\b.*\bexecutable\b/ => ['exe','exe-vms'] ], | ||
| 807 | |||
| 808 | [qr/\bexecutable\b/i => 'exe'], | ||
| 809 | [qr/^MS Windows\b.*\bDLL\b/ => 'dll'], | ||
| 810 | [qr/\bshared object, /i => 'so'], | ||
| 811 | [qr/\brelocatable, /i => 'o'], | ||
| 812 | [qr/\btext\b/i => 'asc'], | ||
| 813 | [qr/^/ => 'dat'], # catchall | ||
| 814 | |||
| 815 | ); | ||
| 816 | |||
| 817 | # MS Windows PE 32-bit Intel 80386 GUI executable not relocatable | ||
| 818 | # MS-DOS executable (EXE), OS/2 or MS Windows | ||
| 819 | # PA-RISC1.1 executable dynamically linked | ||
| 820 | # PA-RISC1.1 shared executable dynamically linked | ||
| 821 | # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (FreeBSD), for FreeBSD 5.0.1, dynamically linked (uses shared libs), stripped | ||
| 822 | # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), stripped | ||
| 823 | # ELF 64-bit MSB executable, SPARC V9, version 1 (FreeBSD), for FreeBSD 5.0, dynamically linked (uses shared libs), stripped | ||
| 824 | # ELF 64-bit MSB shared object, SPARC V9, version 1 (FreeBSD), stripped | ||
| 825 | # ELF 32-bit LSB executable, Intel 80386, version 1, dynamically` | ||
| 826 | # ELF 32-bit MSB executable, SPARC, version 1, dynamically linke` | ||
| 827 | # COFF format alpha executable paged stripped - version 3.11-10 | ||
| 828 | # COFF format alpha executable paged dynamically linked stripped` | ||
| 829 | # COFF format alpha demand paged executable or object module stripped - version 3.11-10 | ||
| 830 | # COFF format alpha paged dynamically linked not stripped shared` | ||
| 831 | # executable (RISC System/6000 V3.1) or obj module | ||
| 832 | # VMS VAX executable | ||
| 833 | |||
| 834 | # prototypes | ||
| 835 | sub Amavis::Unpackers::do_mime_decode($$); | ||
| 836 | sub Amavis::Unpackers::do_ascii($$); | ||
| 837 | sub Amavis::Unpackers::do_uncompress($$$); | ||
| 838 | sub Amavis::Unpackers::do_gunzip($$); | ||
| 839 | sub Amavis::Unpackers::do_pax_cpio($$$); | ||
| 840 | sub Amavis::Unpackers::do_tar($$); | ||
| 841 | sub Amavis::Unpackers::do_ar($$$); | ||
| 842 | sub Amavis::Unpackers::do_unzip($$); | ||
| 843 | sub Amavis::Unpackers::do_unrar($$$); | ||
| 844 | sub Amavis::Unpackers::do_unarj($$$); | ||
| 845 | sub Amavis::Unpackers::do_arc($$$); | ||
| 846 | sub Amavis::Unpackers::do_zoo($$$); | ||
| 847 | sub Amavis::Unpackers::do_lha($$$); | ||
| 848 | sub Amavis::Unpackers::do_ole($$$); | ||
| 849 | sub Amavis::Unpackers::do_cabextract($$$); | ||
| 850 | sub Amavis::Unpackers::do_tnef($$); | ||
| 851 | sub Amavis::Unpackers::do_tnef_ext($$$); | ||
| 852 | sub Amavis::Unpackers::do_executable($$@); | ||
| 853 | |||
| 854 | # Define alias names or shortcuts in this module to make it simpler | ||
| 855 | # to call these routines from amavisd.conf | ||
| 856 | *read_text = \&Amavis::Util::read_text; | ||
| 857 | *read_l10n_templates = \&Amavis::Util::read_l10n_templates; | ||
| 858 | *read_hash = \&Amavis::Util::read_hash; | ||
| 859 | *read_array = \&Amavis::Util::read_array; | ||
| 860 | *dump_hash = \&Amavis::Util::dump_hash; | ||
| 861 | *dump_array = \&Amavis::Util::dump_array; | ||
| 862 | *ask_daemon = \&Amavis::AV::ask_daemon; | ||
| 863 | *sophos_savi = \&Amavis::AV::ask_sophos_savi; | ||
| 864 | *ask_clamav = \&Amavis::AV::ask_clamav; | ||
| 865 | *do_mime_decode = \&Amavis::Unpackers::do_mime_decode; | ||
| 866 | *do_ascii = \&Amavis::Unpackers::do_ascii; | ||
| 867 | *do_uncompress = \&Amavis::Unpackers::do_uncompress; | ||
| 868 | *do_gunzip = \&Amavis::Unpackers::do_gunzip; | ||
| 869 | *do_pax_cpio = \&Amavis::Unpackers::do_pax_cpio; | ||
| 870 | *do_tar = \&Amavis::Unpackers::do_tar; | ||
| 871 | *do_ar = \&Amavis::Unpackers::do_ar; | ||
| 872 | *do_unzip = \&Amavis::Unpackers::do_unzip; | ||
| 873 | *do_unrar = \&Amavis::Unpackers::do_unrar; | ||
| 874 | *do_unarj = \&Amavis::Unpackers::do_unarj; | ||
| 875 | *do_arc = \&Amavis::Unpackers::do_arc; | ||
| 876 | *do_zoo = \&Amavis::Unpackers::do_zoo; | ||
| 877 | *do_lha = \&Amavis::Unpackers::do_lha; | ||
| 878 | *do_ole = \&Amavis::Unpackers::do_ole; | ||
| 879 | *do_cabextract = \&Amavis::Unpackers::do_cabextract; | ||
| 880 | *do_tnef_ext = \&Amavis::Unpackers::do_tnef_ext; | ||
| 881 | *do_tnef = \&Amavis::Unpackers::do_tnef; | ||
| 882 | *do_executable = \&Amavis::Unpackers::do_executable; | ||
| 883 | sub new_RE { Amavis::Lookup::RE->new(@_) } | ||
| 884 | |||
| 885 | # initialize the @decoders list | ||
| 886 | sub init_decoders() { | ||
| 887 | # A list of pairs or n-tuples: [short-type, code_ref, optional-args...]. | ||
| 888 | # Maps short types to a decoding routine, the first match wins. | ||
| 889 | # Arguments beyond the first two can be program path string (or a listref of | ||
| 890 | # paths to be searched) or a reference to a variable containing such a path, | ||
| 891 | # which allows for lazy evaluation, making possible to assign values to | ||
| 892 | # legacy configuration variables even after the assignment to @decoders. | ||
| 893 | @decoders = ( | ||
| 894 | ['mail', \&Amavis::Unpackers::do_mime_decode], | ||
| 895 | ['asc', \&Amavis::Unpackers::do_ascii], | ||
| 896 | ['uue', \&Amavis::Unpackers::do_ascii], | ||
| 897 | ['hqx', \&Amavis::Unpackers::do_ascii], | ||
| 898 | ['ync', \&Amavis::Unpackers::do_ascii], | ||
| 899 | ['F', \&Amavis::Unpackers::do_uncompress, \$unfreeze], | ||
| 900 | ['Z', \&Amavis::Unpackers::do_uncompress, \$uncompress], | ||
| 901 | ['gz', \&Amavis::Unpackers::do_gunzip], | ||
| 902 | ['gz', \&Amavis::Unpackers::do_uncompress, \$gunzip], | ||
| 903 | ['bz2', \&Amavis::Unpackers::do_uncompress, \$bunzip2], | ||
| 904 | ['lzo', \&Amavis::Unpackers::do_uncompress, \$unlzop], | ||
| 905 | ['rpm', \&Amavis::Unpackers::do_uncompress, \$rpm2cpio], | ||
| 906 | ['cpio', \&Amavis::Unpackers::do_pax_cpio, \$pax], | ||
| 907 | ['cpio', \&Amavis::Unpackers::do_pax_cpio, \$cpio], | ||
| 908 | ['tar', \&Amavis::Unpackers::do_pax_cpio, \$pax], | ||
| 909 | ['tar', \&Amavis::Unpackers::do_pax_cpio, \$cpio], | ||
| 910 | ['tar', \&Amavis::Unpackers::do_tar], | ||
| 911 | ['deb', \&Amavis::Unpackers::do_ar, \$ar], | ||
| 912 | # ['a', \&Amavis::Unpackers::do_ar, \$ar], #unpacking .a seems an overkill | ||
| 913 | ['zip', \&Amavis::Unpackers::do_unzip], | ||
| 914 | ['rar', \&Amavis::Unpackers::do_unrar, \$unrar], | ||
| 915 | ['arj', \&Amavis::Unpackers::do_unarj, \$unarj], | ||
| 916 | ['arc', \&Amavis::Unpackers::do_arc, \$arc], | ||
| 917 | ['zoo', \&Amavis::Unpackers::do_zoo, \$zoo], | ||
| 918 | ['lha', \&Amavis::Unpackers::do_lha, \$lha], | ||
| 919 | ['doc', \&Amavis::Unpackers::do_ole, \$ripole], | ||
| 920 | ['cab', \&Amavis::Unpackers::do_cabextract, \$cabextract], | ||
| 921 | ['tnef', \&Amavis::Unpackers::do_tnef_ext, \$tnef], | ||
| 922 | ['tnef', \&Amavis::Unpackers::do_tnef], | ||
| 923 | ['exe', \&Amavis::Unpackers::do_executable, \$unrar,\$lha,\$unarj], | ||
| 924 | ); | ||
| 925 | } | ||
| 926 | |||
| 927 | sub build_default_maps() { | ||
| 928 | @local_domains_maps = ( | ||
| 929 | \%local_domains, \@local_domains_acl, \$local_domains_re); | ||
| 930 | @mynetworks_maps = (\@mynetworks); | ||
| 931 | @bypass_virus_checks_maps = ( | ||
| 932 | \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re); | ||
| 933 | @bypass_spam_checks_maps = ( | ||
| 934 | \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re); | ||
| 935 | @bypass_banned_checks_maps = ( | ||
| 936 | \%bypass_banned_checks, \@bypass_banned_checks_acl, \$bypass_banned_checks_re); | ||
| 937 | @bypass_header_checks_maps = ( | ||
| 938 | \%bypass_header_checks, \@bypass_header_checks_acl, \$bypass_header_checks_re); | ||
| 939 | @virus_lovers_maps = ( | ||
| 940 | \%virus_lovers, \@virus_lovers_acl, \$virus_lovers_re); | ||
| 941 | @spam_lovers_maps = ( | ||
| 942 | \%spam_lovers, \@spam_lovers_acl, \$spam_lovers_re); | ||
| 943 | @banned_files_lovers_maps = ( | ||
| 944 | \%banned_files_lovers, \@banned_files_lovers_acl, \$banned_files_lovers_re); | ||
| 945 | @bad_header_lovers_maps = ( | ||
| 946 | \%bad_header_lovers, \@bad_header_lovers_acl, \$bad_header_lovers_re); | ||
| 947 | @warnvirusrecip_maps = (\$warnvirusrecip); | ||
| 948 | @warnbannedrecip_maps = (\$warnbannedrecip); | ||
| 949 | @warnbadhrecip_maps = (\$warnbadhrecip); | ||
| 950 | @newvirus_admin_maps = (\$newvirus_admin); | ||
| 951 | @virus_admin_maps = (\%virus_admin, \$virus_admin); | ||
| 952 | @banned_admin_maps = (\$banned_admin); | ||
| 953 | @bad_header_admin_maps= (\$bad_header_admin); | ||
| 954 | @spam_admin_maps = (\%spam_admin, \$spam_admin); | ||
| 955 | @virus_quarantine_to_maps = (\$virus_quarantine_to); | ||
| 956 | @banned_quarantine_to_maps = (\$banned_quarantine_to); | ||
| 957 | @bad_header_quarantine_to_maps = (\$bad_header_quarantine_to); | ||
| 958 | @spam_quarantine_to_maps = (\$spam_quarantine_to); | ||
| 959 | @spam_quarantine_bysender_to_maps = (\$spam_quarantine_bysender_to); | ||
| 960 | @keep_decoded_original_maps = (\$keep_decoded_original_re); | ||
| 961 | @map_full_type_to_short_type_maps = (\$map_full_type_to_short_type_re); | ||
| 962 | # @banned_filename_maps = ( {'.' => [$banned_filename_re]} ); | ||
| 963 | # @banned_filename_maps = ( {'.' => 'DEFAULT'} );#names mapped by %banned_rules | ||
| 964 | @banned_filename_maps = ( 'DEFAULT' ); # same as previous, but shorter | ||
| 965 | @viruses_that_fake_sender_maps = (\$viruses_that_fake_sender_re, 1); | ||
| 966 | @spam_tag_level_maps = (\$sa_tag_level_deflt); | ||
| 967 | @spam_tag2_level_maps = (\$sa_tag2_level_deflt); | ||
| 968 | @spam_kill_level_maps = (\$sa_kill_level_deflt); | ||
| 969 | @spam_dsn_cutoff_level_maps = (\$sa_dsn_cutoff_level); | ||
| 970 | @spam_quarantine_cutoff_level_maps = (\$sa_quarantine_cutoff_level); | ||
| 971 | @spam_modifies_subj_maps = (\$sa_spam_modifies_subj); | ||
| 972 | @spam_subject_tag_maps = (\$sa_spam_subject_tag1); # note: inconsistent | ||
| 973 | @spam_subject_tag2_maps = (\$sa_spam_subject_tag); # note: inconsistent | ||
| 974 | @whitelist_sender_maps = ( | ||
| 975 | \%whitelist_sender, \@whitelist_sender_acl, \$whitelist_sender_re); | ||
| 976 | @blacklist_sender_maps = ( | ||
| 977 | \%blacklist_sender, \@blacklist_sender_acl, \$blacklist_sender_re); | ||
| 978 | @score_sender_maps = (); # new variable, no backwards compatibility needed | ||
| 979 | @message_size_limit_maps = (); # new variable | ||
| 980 | @addr_extension_virus_maps = (\$addr_extension_virus); | ||
| 981 | @addr_extension_spam_maps = (\$addr_extension_spam); | ||
| 982 | @addr_extension_banned_maps = (\$addr_extension_banned); | ||
| 983 | @addr_extension_bad_header_maps = (\$addr_extension_bad_header); | ||
| 984 | @debug_sender_maps = (\@debug_sender_acl); | ||
| 985 | } | ||
| 986 | |||
| 987 | # prepend a lookup table label object for logging purposes | ||
| 988 | sub label_default_maps() { | ||
| 989 | for my $varname (qw( | ||
| 990 | @local_domains_maps @mynetworks_maps | ||
| 991 | @bypass_virus_checks_maps @bypass_spam_checks_maps | ||
| 992 | @bypass_banned_checks_maps @bypass_header_checks_maps | ||
| 993 | @virus_lovers_maps @spam_lovers_maps | ||
| 994 | @banned_files_lovers_maps @bad_header_lovers_maps | ||
| 995 | @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps | ||
| 996 | @newvirus_admin_maps @virus_admin_maps | ||
| 997 | @banned_admin_maps @bad_header_admin_maps @spam_admin_maps | ||
| 998 | @virus_quarantine_to_maps | ||
| 999 | @banned_quarantine_to_maps @bad_header_quarantine_to_maps | ||
| 1000 | @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps | ||
| 1001 | @keep_decoded_original_maps @map_full_type_to_short_type_maps | ||
| 1002 | @banned_filename_maps | ||
| 1003 | @viruses_that_fake_sender_maps | ||
| 1004 | @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps | ||
| 1005 | @spam_dsn_cutoff_level_maps @spam_quarantine_cutoff_level_maps | ||
| 1006 | @spam_modifies_subj_maps @spam_subject_tag_maps @spam_subject_tag2_maps | ||
| 1007 | @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps | ||
| 1008 | @message_size_limit_maps | ||
| 1009 | @addr_extension_virus_maps @addr_extension_spam_maps | ||
| 1010 | @addr_extension_banned_maps @addr_extension_bad_header_maps | ||
| 1011 | @debug_sender_maps )) | ||
| 1012 | { | ||
| 1013 | my($g) = $varname; $g =~ s{\@}{Amavis::Conf::}; # qualified variable name | ||
| 1014 | my($label) = $varname; $label=~s/^\@//; $label=~s/_maps$//; | ||
| 1015 | { no strict 'refs'; | ||
| 1016 | unshift(@$g, # NOTE: a symbolic reference | ||
| 1017 | Amavis::Lookup::Label->new($label)) if @$g; # no label if empty | ||
| 1018 | } | ||
| 1019 | } | ||
| 1020 | } | ||
| 1021 | |||
| 1022 | # read and evaluate configuration files (one or more) | ||
| 1023 | sub read_config(@) { | ||
| 1024 | my(@config_files) = @_; | ||
| 1025 | for my $config_file (@config_files) { | ||
| 1026 | my($msg); | ||
| 1027 | my($errn) = stat($config_file) ? 0 : 0+$!; | ||
| 1028 | if ($errn == ENOENT) { $msg = "does not exist" } | ||
| 1029 | elsif ($errn) { $msg = "is inaccessible: $!" } | ||
| 1030 | elsif (-d _) { $msg = "is a directory" } | ||
| 1031 | elsif (!-f _) { $msg = "is not a regular file" } | ||
| 1032 | elsif ($> && -o _) { $msg = "is owned by EUID $>, should be owned by root"} | ||
| 1033 | elsif ($> && -w _) { $msg = "is writable by EUID $>, EGID $)" } | ||
| 1034 | if (defined $msg) { die "Config file \"$config_file\" $msg," } | ||
| 1035 | $! = 0; | ||
| 1036 | if (defined(do $config_file)) {} | ||
| 1037 | elsif ($@ ne '') { die "Error in config file \"$config_file\": $@" } | ||
| 1038 | elsif ($! != 0) { die "Error reading config file \"$config_file\": $!" } | ||
| 1039 | } | ||
| 1040 | $daemon_chroot_dir = '' | ||
| 1041 | if !defined $daemon_chroot_dir || $daemon_chroot_dir eq '/'; | ||
| 1042 | # provide some sensible defaults for essential settings (post-defaults) | ||
| 1043 | $TEMPBASE = $MYHOME if !defined $TEMPBASE; | ||
| 1044 | $helpers_home = $MYHOME if !defined $helpers_home; | ||
| 1045 | $db_home = "$MYHOME/db" if !defined $db_home; | ||
| 1046 | $lock_file = "$MYHOME/amavisd.lock" if !defined $lock_file; | ||
| 1047 | $pid_file = "$MYHOME/amavisd.pid" if !defined $pid_file; | ||
| 1048 | |||
| 1049 | $X_HEADER_TAG = 'X-Virus-Scanned' if !defined $X_HEADER_TAG; | ||
| 1050 | $X_HEADER_LINE= "$myproduct_name at $mydomain" if !defined $X_HEADER_LINE; | ||
| 1051 | |||
| 1052 | $gunzip = "$gzip -d" if !defined $gunzip && $gzip ne ''; | ||
| 1053 | $bunzip2 = "$bzip2 -d" if !defined $bunzip2 && $bzip2 ne ''; | ||
| 1054 | $unlzop = "$lzop -d" if !defined $unlzop && $lzop ne ''; | ||
| 1055 | |||
| 1056 | my($pname) = "\"Content-filter at $myhostname\""; | ||
| 1057 | $hdrfrom_notify_sender = "$pname <postmaster\@$myhostname>" | ||
| 1058 | if !defined $hdrfrom_notify_sender; | ||
| 1059 | $hdrfrom_notify_recip = $mailfrom_notify_recip ne '' | ||
| 1060 | ? "$pname <$mailfrom_notify_recip>" | ||
| 1061 | : $hdrfrom_notify_sender if !defined $hdrfrom_notify_recip; | ||
| 1062 | $hdrfrom_notify_admin = $mailfrom_notify_admin ne '' | ||
| 1063 | ? "$pname <$mailfrom_notify_admin>" | ||
| 1064 | : $hdrfrom_notify_sender if !defined $hdrfrom_notify_admin; | ||
| 1065 | $hdrfrom_notify_spamadmin = $mailfrom_notify_spamadmin ne '' | ||
| 1066 | ? "$pname <$mailfrom_notify_spamadmin>" | ||
| 1067 | : $hdrfrom_notify_sender if !defined $hdrfrom_notify_spamadmin; | ||
| 1068 | |||
| 1069 | # compatibility with deprecated $warn*sender and old *_destiny values | ||
| 1070 | # map old values <0, =0, >0 into D_REJECT/D_BOUNCE, D_DISCARD, D_PASS | ||
| 1071 | for ($final_virus_destiny, $final_banned_destiny, $final_spam_destiny) { | ||
| 1072 | if ($_ > 0) { $_ = D_PASS } | ||
| 1073 | elsif ($_ < 0 && $_ != D_BOUNCE && $_ != D_REJECT) { # compatibility | ||
| 1074 | # favour Reject with sendmail milter, Bounce with others | ||
| 1075 | $_ = c('forward_method') eq '' ? D_REJECT : D_BOUNCE; | ||
| 1076 | } | ||
| 1077 | } | ||
| 1078 | if ($final_virus_destiny == D_DISCARD && c('warnvirussender') ) | ||
| 1079 | { $final_virus_destiny = D_BOUNCE } | ||
| 1080 | if ($final_spam_destiny == D_DISCARD && c('warnspamsender') ) | ||
| 1081 | { $final_spam_destiny = D_BOUNCE } | ||
| 1082 | if ($final_banned_destiny == D_DISCARD && c('warnbannedsender') ) | ||
| 1083 | { $final_banned_destiny = D_BOUNCE } | ||
| 1084 | if ($final_bad_header_destiny == D_DISCARD && c('warnbadhsender') ) | ||
| 1085 | { $final_bad_header_destiny = D_BOUNCE } | ||
| 1086 | if (!%banned_rules) { | ||
| 1087 | # an associative array mapping a rule name | ||
| 1088 | # to a single 'banned names/types' lookup table | ||
| 1089 | %banned_rules = ('DEFAULT'=>$banned_filename_re); # backwards compatibile | ||
| 1090 | } | ||
| 1091 | } | ||
| 1092 | |||
| 1093 | 1; | ||
| 1094 | |||
| 1095 | # | ||
| 1096 | package Amavis::Lock; | ||
| 1097 | use strict; | ||
| 1098 | use re 'taint'; | ||
| 1099 | |||
| 1100 | BEGIN { | ||
| 1101 | use Exporter (); | ||
| 1102 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 1103 | $VERSION = '2.043'; | ||
| 1104 | @ISA = qw(Exporter); | ||
| 1105 | @EXPORT = qw(&lock &unlock); | ||
| 1106 | } | ||
| 1107 | use Fcntl qw(LOCK_SH LOCK_EX LOCK_UN); | ||
| 1108 | |||
| 1109 | use subs @EXPORT; | ||
| 1110 | |||
| 1111 | sub lock($) { | ||
| 1112 | my($file_handle) = @_; | ||
| 1113 | flock($file_handle, LOCK_EX) or die "Can't lock $file_handle: $!"; | ||
| 1114 | # NOTE: a lock is on a file, not on a file handle | ||
| 1115 | } | ||
| 1116 | |||
| 1117 | sub unlock($) { | ||
| 1118 | my($file_handle) = @_; | ||
| 1119 | flock($file_handle, LOCK_UN) or die "Can't unlock $file_handle: $!"; | ||
| 1120 | } | ||
| 1121 | |||
| 1122 | 1; | ||
| 1123 | |||
| 1124 | # | ||
| 1125 | package Amavis::Log; | ||
| 1126 | use strict; | ||
| 1127 | use re 'taint'; | ||
| 1128 | |||
| 1129 | BEGIN { | ||
| 1130 | use Exporter (); | ||
| 1131 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 1132 | $VERSION = '2.043'; | ||
| 1133 | @ISA = qw(Exporter); | ||
| 1134 | formorer | 189 | @EXPORT_OK = qw(&init &write_log &open_log &close_log &log_fd); |
| 1135 | zobel | 21 | } |
| 1136 | use subs @EXPORT_OK; | ||
| 1137 | |||
| 1138 | use POSIX qw(locale_h strftime); | ||
| 1139 | use Unix::Syslog qw(:macros :subs); | ||
| 1140 | use IO::File (); | ||
| 1141 | use File::Basename; | ||
| 1142 | |||
| 1143 | BEGIN { | ||
| 1144 | import Amavis::Conf qw(:platform $myversion $myhostname $daemon_user); | ||
| 1145 | import Amavis::Lock; | ||
| 1146 | } | ||
| 1147 | |||
| 1148 | use vars qw($loghandle); # log file handle | ||
| 1149 | use vars qw($myname); | ||
| 1150 | use vars qw($syslog_facility $syslog_priority %syslog_priority); | ||
| 1151 | use vars qw($log_to_stderr $do_syslog $logfile); | ||
| 1152 | |||
| 1153 | sub init($$$$) { | ||
| 1154 | my($syslog_level); | ||
| 1155 | ($log_to_stderr, $do_syslog, $syslog_level, $logfile) = @_; | ||
| 1156 | |||
| 1157 | $myname = $0; | ||
| 1158 | if ($syslog_level =~ /^\s*([a-z0-9]+)\.([a-z0-9]+)\s*\z/i) { | ||
| 1159 | $syslog_facility = eval("LOG_\U$1"); | ||
| 1160 | $syslog_priority = eval("LOG_\U$2"); | ||
| 1161 | } | ||
| 1162 | $syslog_facility = LOG_DAEMON if $syslog_facility !~ /^\d+\z/; | ||
| 1163 | $syslog_priority = LOG_WARNING if $syslog_priority !~ /^\d+\z/; | ||
| 1164 | open_log(); | ||
| 1165 | if (!$do_syslog && $logfile eq '') | ||
| 1166 | { print STDERR "Logging to STDERR (no \$LOGFILE and no \$DO_SYSLOG)\n" } | ||
| 1167 | my($msg) = "starting. $myname at $myhostname $myversion"; | ||
| 1168 | $msg .= ", eol=\"$eol\"" if $eol ne "\n"; | ||
| 1169 | $msg .= ", Unicode aware" if $unicode_aware; | ||
| 1170 | $msg .= ", LC_ALL=$ENV{LC_ALL}" if $ENV{LC_ALL} ne ''; | ||
| 1171 | $msg .= ", LC_TYPE=$ENV{LC_TYPE}" if $ENV{LC_TYPE} ne ''; | ||
| 1172 | $msg .= ", LC_CTYPE=$ENV{LC_CTYPE}" if $ENV{LC_CTYPE} ne ''; | ||
| 1173 | $msg .= ", LANG=$ENV{LANG}" if $ENV{LANG} ne ''; | ||
| 1174 | write_log(0, $msg, undef); | ||
| 1175 | } | ||
| 1176 | |||
| 1177 | sub open_log() { | ||
| 1178 | # don't bother to skip opening the log even if $log_to_stderr (debug) is true | ||
| 1179 | if ($do_syslog) { | ||
| 1180 | openlog('amavis', LOG_PID | LOG_NDELAY, $syslog_facility); | ||
| 1181 | } elsif ($logfile ne '') { | ||
| 1182 | $loghandle = IO::File->new($logfile,'>>') | ||
| 1183 | or die "Failed to open log file $logfile: $!"; | ||
| 1184 | $loghandle->autoflush(1); | ||
| 1185 | if ($> == 0) { | ||
| 1186 | my($uid) = $daemon_user=~/^(\d+)$/ ? $1 : (getpwnam($daemon_user))[2]; | ||
| 1187 | if ($uid) { | ||
| 1188 | chown($uid,-1,$logfile) | ||
| 1189 | or die "Can't chown logfile $logfile to $uid: $!"; | ||
| 1190 | } | ||
| 1191 | } | ||
| 1192 | } | ||
| 1193 | } | ||
| 1194 | |||
| 1195 | sub close_log() { | ||
| 1196 | if ($do_syslog) { | ||
| 1197 | closelog(); | ||
| 1198 | } elsif (defined($loghandle) && $logfile ne '') { | ||
| 1199 | $loghandle->close or die "Error closing log file $logfile: $!"; | ||
| 1200 | $loghandle = undef; | ||
| 1201 | } | ||
| 1202 | } | ||
| 1203 | |||
| 1204 | # Log either to syslog or to a file | ||
| 1205 | sub write_log($$$) { | ||
| 1206 | my($level,$errmsg,$am_id) = @_; | ||
| 1207 | |||
| 1208 | $am_id = !defined $am_id ? '' : "($am_id) "; | ||
| 1209 | $errmsg = Amavis::Util::sanitize_str($errmsg); | ||
| 1210 | # my($old_locale) = POSIX::setlocale(LC_TIME,"C"); # English dates required! | ||
| 1211 | # if (length($errmsg) > 2000) { # crop at some arbitrary limit (< LINE_MAX) | ||
| 1212 | # $errmsg = substr($errmsg,0,2000) . "..."; | ||
| 1213 | # } | ||
| 1214 | if ($do_syslog && !$log_to_stderr) { | ||
| 1215 | my($prio) = $syslog_priority; # never go below this priority level | ||
| 1216 | # syslog priorities: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT, EMERG | ||
| 1217 | if ($level <= -3) { $prio = LOG_CRIT if $prio > LOG_CRIT } | ||
| 1218 | elsif ($level <= -2) { $prio = LOG_ERR if $prio > LOG_ERR } | ||
| 1219 | elsif ($level <= -1) { $prio = LOG_WARNING if $prio > LOG_WARNING } | ||
| 1220 | elsif ($level <= 0) { $prio = LOG_NOTICE if $prio > LOG_NOTICE } | ||
| 1221 | elsif ($level <= 2) { $prio = LOG_INFO if $prio > LOG_INFO } | ||
| 1222 | else { $prio = LOG_DEBUG if $prio > LOG_DEBUG } | ||
| 1223 | my($pre) = ''; | ||
| 1224 | my($logline_size) = 980; # less than (1023 - prefix) | ||
| 1225 | while (length($am_id)+length($pre)+length($errmsg) > $logline_size) { | ||
| 1226 | my($avail) = $logline_size - length($am_id . $pre . "..."); | ||
| 1227 | syslog($prio, "%s", $am_id . $pre . substr($errmsg,0,$avail) . "..."); | ||
| 1228 | $pre = "..."; | ||
| 1229 | $errmsg = substr($errmsg, $avail); | ||
| 1230 | } | ||
| 1231 | syslog($prio, "%s", $am_id . $pre . $errmsg); | ||
| 1232 | } else { | ||
| 1233 | my($prefix) = sprintf("%s %s %s[%s]: ", # prepare syslog-alike prefix | ||
| 1234 | strftime("%b %e %H:%M:%S",localtime), $myhostname, $myname, $$); | ||
| 1235 | if (defined $loghandle && !$log_to_stderr) { | ||
| 1236 | lock($loghandle); | ||
| 1237 | seek($loghandle,0,2) or die "Can't position log file to its tail: $!"; | ||
| 1238 | $loghandle->print($prefix, $am_id, $errmsg, $eol) | ||
| 1239 | or die "Error writing to log file: $!"; | ||
| 1240 | unlock($loghandle); | ||
| 1241 | } else { | ||
| 1242 | print STDERR $prefix, $am_id, $errmsg, $eol | ||
| 1243 | or die "Error writing to STDERR: $!"; | ||
| 1244 | } | ||
| 1245 | } | ||
| 1246 | # POSIX::setlocale(LC_TIME, $old_locale); | ||
| 1247 | } | ||
| 1248 | |||
| 1249 | formorer | 189 | sub log_fd() { |
| 1250 | $log_to_stderr ? fileno(STDERR) | ||
| 1251 | : $do_syslog ? undef # how to obtain fd on syslog? | ||
| 1252 | : defined $loghandle ? $loghandle->fileno : fileno(STDERR); | ||
| 1253 | } | ||
| 1254 | |||
| 1255 | zobel | 21 | 1; |
| 1256 | |||
| 1257 | # | ||
| 1258 | package Amavis::Timing; | ||
| 1259 | use strict; | ||
| 1260 | use re 'taint'; | ||
| 1261 | |||
| 1262 | BEGIN { | ||
| 1263 | use Exporter (); | ||
| 1264 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 1265 | $VERSION = '2.043'; | ||
| 1266 | @ISA = qw(Exporter); | ||
| 1267 | @EXPORT_OK = qw(&init §ion_time &report &get_time_so_far); | ||
| 1268 | } | ||
| 1269 | use subs @EXPORT_OK; | ||
| 1270 | |||
| 1271 | use Time::HiRes 1.49 (); | ||
| 1272 | |||
| 1273 | use vars qw(@timing); | ||
| 1274 | |||
| 1275 | # clear array @timing and enter start time | ||
| 1276 | sub init() { | ||
| 1277 | @timing = (); section_time('init'); | ||
| 1278 | } | ||
| 1279 | |||
| 1280 | # enter current time reading into array @timing | ||
| 1281 | sub section_time($) { | ||
| 1282 | push(@timing,shift,Time::HiRes::time); | ||
| 1283 | } | ||
| 1284 | |||
| 1285 | # returns a string - a report of elapsed time by section | ||
| 1286 | sub report() { | ||
| 1287 | section_time('rundown'); | ||
| 1288 | my($notneeded, $t0) = (shift(@timing), shift(@timing)); | ||
| 1289 | my($total) = $t0 <= 0 ? 0 : $timing[$#timing] - $t0; | ||
| 1290 | if ($total < 0.0000001) { $total = 0.0000001 } | ||
| 1291 | my(@sections); my($t00) = $t0; | ||
| 1292 | while (@timing) { | ||
| 1293 | my($section, $t) = (shift(@timing), shift(@timing)); | ||
| 1294 | my($dt) = $t <= $t0 ? 0 : $t-$t0; # handle possible clock jumps | ||
| 1295 | my($dt_c) = $t <= $t00 ? 0 : $t-$t00; # handle possible clock jumps | ||
| 1296 | my($dtp) = $dt >= $total ? 100 : $dt*100.0/$total; # this event | ||
| 1297 | my($dtp_c) = $dt_c >= $total ? 100 : $dt_c*100.0/$total; # cumulative | ||
| 1298 | push(@sections, sprintf("%s: %.0f (%.0f%%)%.0f", | ||
| 1299 | $section, $dt*1000, $dtp, $dtp_c)); | ||
| 1300 | $t0 = $t; | ||
| 1301 | } | ||
| 1302 | sprintf("TIMING [total %.0f ms] - %s", $total * 1000, join(", ",@sections)); | ||
| 1303 | } | ||
| 1304 | |||
| 1305 | # returns value in seconds of elapsed time for processing of this mail so far | ||
| 1306 | sub get_time_so_far() { | ||
| 1307 | my($notneeded, $t0) = @timing; | ||
| 1308 | my($total) = $t0 <= 0 ? 0 : Time::HiRes::time - $t0; | ||
| 1309 | $total < 0 ? 0 : $total; | ||
| 1310 | } | ||
| 1311 | |||
| 1312 | use vars qw($t_was_busy $t_busy_cum $t_idle_cum $t0); | ||
| 1313 | |||
| 1314 | sub idle_proc(@) { | ||
| 1315 | my($t1) = Time::HiRes::time; | ||
| 1316 | if (defined $t0) { | ||
| 1317 | ($t_was_busy ? $t_busy_cum : $t_idle_cum) += $t1 - $t0; | ||
| 1318 | Amavis::Util::ll(5) && Amavis::Util::do_log(5, | ||
| 1319 | sprintf("idle_proc, @_: was %s, %.1f ms, total idle %.3f s, busy %.3f s", | ||
| 1320 | $t_was_busy ? "busy" : "idle", 1000 * ($t1 - $t0), | ||
| 1321 | $t_idle_cum, $t_busy_cum)); | ||
| 1322 | } | ||
| 1323 | $t0 = $t1; | ||
| 1324 | } | ||
| 1325 | |||
| 1326 | sub go_idle(@) { | ||
| 1327 | if ($t_was_busy) { idle_proc(@_); $t_was_busy = 0 } | ||
| 1328 | } | ||
| 1329 | |||
| 1330 | sub go_busy(@) { | ||
| 1331 | if (!$t_was_busy) { idle_proc(@_); $t_was_busy = 1 } | ||
| 1332 | } | ||
| 1333 | |||
| 1334 | sub report_load() { | ||
| 1335 | return if $t_busy_cum + $t_idle_cum <= 0; | ||
| 1336 | Amavis::Util::do_log(3, sprintf( | ||
| 1337 | "load: %.0f %%, total idle %.3f s, busy %.3f s", | ||
| 1338 | 100*$t_busy_cum / ($t_busy_cum + $t_idle_cum), $t_idle_cum, $t_busy_cum)); | ||
| 1339 | } | ||
| 1340 | |||
| 1341 | 1; | ||
| 1342 | |||
| 1343 | # | ||
| 1344 | package Amavis::Util; | ||
| 1345 | use strict; | ||
| 1346 | use re 'taint'; | ||
| 1347 | |||
| 1348 | BEGIN { | ||
| 1349 | use Exporter (); | ||
| 1350 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 1351 | $VERSION = '2.043'; | ||
| 1352 | @ISA = qw(Exporter); | ||
| 1353 | @EXPORT_OK = qw(&untaint &min &max &safe_encode &safe_decode &q_encode | ||
| 1354 | &snmp_count &snmp_counters_init &snmp_counters_get | ||
| 1355 | &am_id &new_am_id &ll &do_log &debug_oneshot | ||
| 1356 | &add_entropy &fetch_entropy &generate_mail_id | ||
| 1357 | &retcode &exit_status_str &prolong_timer | ||
| 1358 | &sanitize_str &fmt_struct &strip_tempdir &rmdir_recursively | ||
| 1359 | &read_text &read_l10n_templates &read_hash &read_array | ||
| 1360 | &dump_hash &dump_array &run_command &run_command_consumer); | ||
| 1361 | } | ||
| 1362 | use subs @EXPORT_OK; | ||
| 1363 | use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED | ||
| 1364 | WEXITSTATUS WTERMSIG WSTOPSIG); | ||
| 1365 | use Errno qw(ENOENT EACCES); | ||
| 1366 | use Digest::MD5 2.22; # need 'clone' method | ||
| 1367 | # use Encode; # Perl 5.8 UTF-8 support | ||
| 1368 | |||
| 1369 | BEGIN { | ||
| 1370 | import Amavis::Conf qw(:platform $DEBUG c cr ca); | ||
| 1371 | formorer | 189 | import Amavis::Log qw(write_log open_log close_log log_fd); |
| 1372 | zobel | 21 | import Amavis::Timing qw(section_time); |
| 1373 | } | ||
| 1374 | |||
| 1375 | # Return untainted copy of a string (argument can be a string or a string ref) | ||
| 1376 | sub untaint($) { | ||
| 1377 | no re 'taint'; | ||
| 1378 | my($str); | ||
| 1379 | if (defined($_[0])) { | ||
| 1380 | local($1); # avoid Perl taint bug: tainted global $1 propagates taintedness | ||
| 1381 | $str = $1 if (ref($_[0]) ? ${$_[0]} : $_[0]) =~ /^(.*)\z/s; | ||
| 1382 | } | ||
| 1383 | $str; | ||
| 1384 | } | ||
| 1385 | |||
| 1386 | # Returns the smallest defined number from the list, or undef | ||
| 1387 | sub min(@) { | ||
| 1388 | my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref | ||
| 1389 | my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ < $m) } | ||
| 1390 | $m; | ||
| 1391 | } | ||
| 1392 | |||
| 1393 | # Returns the largest defined number from the list, or undef | ||
| 1394 | sub max(@) { | ||
| 1395 | my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref | ||
| 1396 | my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ > $m) } | ||
| 1397 | $m; | ||
| 1398 | } | ||
| 1399 | |||
| 1400 | # A wrapper for Encode::encode, avoiding a bug in Perl 5.8.0 which causes | ||
| 1401 | # Encode::encode to loop and fill memory when given a tainted string | ||
| 1402 | # | ||
| 1403 | # hmh@d.o : in Debian's 5.8.4-2, trying to restore the taintedness | ||
| 1404 | # actually causes perl to somehow lose track of the encoding and it | ||
| 1405 | # completely breaks this sub. OTOH, perl does loop eating up memory | ||
| 1406 | # on tainted strings, so we will have to lose taint state for now. | ||
| 1407 | sub safe_encode($$;$) { | ||
| 1408 | if (!$unicode_aware) { $_[1] } # just return the second argument | ||
| 1409 | else { | ||
| 1410 | my($encoding,$str,$check) = @_; | ||
| 1411 | $check = 0 if !defined($check); | ||
| 1412 | $str = untaint(\$str); | ||
| 1413 | return Encode::encode($encoding, $str, $check); # reattach taintedness | ||
| 1414 | # # taintedness of the string, with UTF-8 flag unconditionally off | ||
| 1415 | # my($taint) = Encode::encode('ascii',substr($str,0,0)); | ||
| 1416 | # $taint . Encode::encode($encoding,untaint($str),$check); # preserve taint | ||
| 1417 | } | ||
| 1418 | } | ||
| 1419 | |||
| 1420 | sub safe_decode($$;$) { | ||
| 1421 | if (!$unicode_aware) { $_[1] } # just return the second argument | ||
| 1422 | else { | ||
| 1423 | my($encoding,$str,$check) = @_; | ||
| 1424 | $check = 0 if !defined($check); | ||
| 1425 | my($taint) = substr($str,0,0); # taintedness of the string | ||
| 1426 | $taint . Encode::decode($encoding,untaint($str),$check); # preserve taint | ||
| 1427 | } | ||
| 1428 | } | ||
| 1429 | |||
| 1430 | # Do the Q-encoding manually, the MIME::Words::encode_mimeword does not | ||
| 1431 | # encode spaces and does not limit to 75 ch, which violates the RFC 2047 | ||
| 1432 | sub q_encode($$$) { | ||
| 1433 | my($octets,$encoding,$charset) = @_; | ||
| 1434 | my($prefix) = '=?' . $charset . '?' . $encoding . '?'; | ||
| 1435 | my($suffix) = '?='; local($1,$2,$3); | ||
| 1436 | # FWS | utext (= NO-WS-CTL|rest of US-ASCII) | ||
| 1437 | $octets =~ /^ ( [\001-\011\013\014\016-\177]* [ \t] )? (.*?) | ||
| 1438 | ( [ \t] [\001-\011\013\014\016-\177]* )? \z/sx; | ||
| 1439 | my($head,$rest,$tail) = ($1,$2,$3); | ||
| 1440 | # Q-encode $rest according to RFC 2047 | ||
| 1441 | # more restricted than =?_ so that it may be used in 'phrase' | ||
| 1442 | $rest =~ s{([^ 0-9a-zA-Z!*/+-])}{sprintf('=%02X',ord($1))}egs; | ||
| 1443 | $rest =~ tr/ /_/; # turn spaces into _ (rfc2047 allows it) | ||
| 1444 | my($s) = $head; my($len) = 75 - (length($prefix)+length($suffix)) - 2; | ||
| 1445 | while ($rest ne '') { | ||
| 1446 | $s .= ' ' if $s !~ /[ \t]\z/; # encoded words must be separated by FWS | ||
| 1447 | $rest =~ /^ ( .{0,$len} [^=] (?: [^=] | \z ) ) (.*) \z/sx; | ||
| 1448 | $s .= $prefix.$1.$suffix; $rest = $2; | ||
| 1449 | } | ||
| 1450 | $s.$tail; | ||
| 1451 | } | ||
| 1452 | |||
| 1453 | # Set or get Amavis internal message id. | ||
| 1454 | # This message id performs a similar function as queue-id in MTA responses. | ||
| 1455 | # It may only be used in generating text part of SMTP responses, | ||
| 1456 | # or in generating log entries. It is only unique within a limited timespan. | ||
| 1457 | use vars qw($amavis_task_id); # internal message id (accessible via &am_id) | ||
| 1458 | |||
| 1459 | sub am_id(;$) { | ||
| 1460 | if (@_) { # set, if argument present | ||
| 1461 | $amavis_task_id = shift; | ||
| 1462 | $0 = "amavisd ($amavis_task_id)"; | ||
| 1463 | } | ||
| 1464 | $amavis_task_id; # return current value | ||
| 1465 | } | ||
| 1466 | |||
| 1467 | sub new_am_id($;$$) { | ||
| 1468 | my($str, $cnt, $seq) = @_; | ||
| 1469 | my($id); | ||
| 1470 | $id = defined $str ? $str : sprintf("%05d", $$); | ||
| 1471 | $id .= sprintf("-%02d", $cnt) if defined $cnt; | ||
| 1472 | $id .= "-$seq" if defined $seq && $seq > 1; | ||
| 1473 | am_id($id); | ||
| 1474 | } | ||
| 1475 | |||
| 1476 | use vars qw($entropy); # MD5 ctx (128 bits, 32 hex digits or 22 base64 chars) | ||
| 1477 | sub add_entropy(@) { | ||
| 1478 | $entropy = Digest::MD5->new if !defined $entropy; | ||
| 1479 | my($s) = join(",", map {!defined($_) ? 'U' : ref eq 'ARRAY' ? @$_ : $_} @_); | ||
| 1480 | # do_log(5,"add_entropy: ".$s); | ||
| 1481 | $entropy->add($s); | ||
| 1482 | } | ||
| 1483 | |||
| 1484 | sub fetch_entropy() { | ||
| 1485 | $entropy->clone->b64digest; | ||
| 1486 | } | ||
| 1487 | |||
| 1488 | # generate a reasonably unique (long-term) id based on collected entropy. | ||
| 1489 | # The result is a pair of (mostly public) mail_id, and a secret id, | ||
| 1490 | # where mail_id == b64(md5(b64(secret))). The secret id could be used to | ||
| 1491 | # authorize releasing quarantined mail. Both the mail_id and secret are | ||
| 1492 | # 12-char strings of characters [A-Za-z0-9+-], with an additional restriction | ||
| 1493 | # for mail_id which must begin and end with an alphanumeric character. | ||
| 1494 | sub generate_mail_id() { | ||
| 1495 | my($secret_id,$id,$rest); | ||
| 1496 | for (my $j=0; $j<100; $j++) { # provide some sanity loop limit just in case | ||
| 1497 | # take 72 bits from entropy accum. to produce a secret id, leave 56 bits | ||
| 1498 | local($1,$2); $entropy->clone->b64digest =~ /^(.{12})(.*)\z/s; | ||
| 1499 | ($secret_id,$rest) = ($1,$2); $secret_id =~ tr{/}{-}; # [A-Za-z0-9+-] | ||
| 1500 | # mail_id computed as md5(secret_id), rely on unidirectionality of md5 | ||
| 1501 | $id = Digest::MD5->new->add($secret_id)->b64digest; # md5(b64(secret_id)) | ||
| 1502 | last if $id =~ /^[A-Za-z0-9].{10}[A-Za-z0-9]/s; # starts&ends with alfnum | ||
| 1503 | add_entropy($j); # retry on less than 7% of cases | ||
| 1504 | do_log(5,"generate_mail_id retry: $id"); | ||
| 1505 | } | ||
| 1506 | # start with a fresh entropy accumulator, wiping out traces of secret id | ||
| 1507 | $entropy = undef; | ||
| 1508 | add_entropy($rest); # carry over unused portion of old entropy accumulator | ||
| 1509 | add_entropy($id); # mix-in the full mail_id before chopping it to 12 chars | ||
| 1510 | $id = substr($id,0,12); $id =~ tr{/}{-}; | ||
| 1511 | ($id,$secret_id); | ||
| 1512 | } | ||
| 1513 | |||
| 1514 | use vars qw(@counter_names); | ||
| 1515 | # elements may be counter names (increment is 1), or pairs: [name,increment] | ||
| 1516 | sub snmp_counters_init() { @counter_names = () } | ||
| 1517 | sub snmp_count(@) { push(@counter_names, @_) } | ||
| 1518 | sub snmp_counters_get() { \@counter_names } | ||
| 1519 | |||
| 1520 | use vars qw($debug_oneshot); | ||
| 1521 | sub debug_oneshot(;$$) { | ||
| 1522 | if (@_) { | ||
| 1523 | my($new_debug_oneshot) = shift; | ||
| 1524 | if (($new_debug_oneshot ? 1 : 0) != ($debug_oneshot ? 1 : 0)) { | ||
| 1525 | do_log(0, "DEBUG_ONESHOT: TURNED ".($new_debug_oneshot ? "ON" : "OFF")); | ||
| 1526 | do_log(0, shift) if @_; # caller-provided extra log entry, usually | ||
| 1527 | # the one that caused debug_oneshot call | ||
| 1528 | } | ||
| 1529 | $debug_oneshot = $new_debug_oneshot; | ||
| 1530 | } | ||
| 1531 | $debug_oneshot; | ||
| 1532 | } | ||
| 1533 | |||
| 1534 | # is a message log level below the current log level? | ||
| 1535 | sub ll($) { | ||
| 1536 | my($level) = @_; | ||
| 1537 | $level = 0 if $level > 0 && ($DEBUG || $debug_oneshot); | ||
| 1538 | my($current_log_level) = c('log_level'); | ||
| 1539 | $current_log_level = 0 if !defined($current_log_level); | ||
| 1540 | $level <= $current_log_level; | ||
| 1541 | } | ||
| 1542 | |||
| 1543 | # write log entry | ||
| 1544 | sub do_log($$) { | ||
| 1545 | my($level, $errmsg) = @_; | ||
| 1546 | if (ll($level)) { | ||
| 1547 | $level = 0 if $level > 0 && ($DEBUG || $debug_oneshot); | ||
| 1548 | write_log($level, $errmsg, am_id()); | ||
| 1549 | } | ||
| 1550 | } | ||
| 1551 | |||
| 1552 | sub retcode($) { # (this subroutine is being phased out) | ||
| 1553 | my $code = shift; | ||
| 1554 | return WEXITSTATUS($code) if WIFEXITED($code); | ||
| 1555 | return 128 + WTERMSIG($code) if WIFSIGNALED($code); | ||
| 1556 | return 255; | ||
| 1557 | } | ||
| 1558 | |||
| 1559 | # map process termination status number to a string, and append optional | ||
| 1560 | # user error mesage, returning the resulting string | ||
| 1561 | sub exit_status_str($;$) { | ||
| 1562 | my($stat,$err) = @_; my($str); | ||
| 1563 | if (WIFEXITED($stat)) { | ||
| 1564 | $str = sprintf("exit %d", WEXITSTATUS($stat)); | ||
| 1565 | } elsif (WIFSTOPPED($stat)) { | ||
| 1566 | $str = sprintf("stopped, signal %d", WSTOPSIG($stat)); | ||
| 1567 | } else { | ||
| 1568 | $str = sprintf("DIED on signal %d (%04x)", WTERMSIG($stat),$stat); | ||
| 1569 | } | ||
| 1570 | $str .= ', '.$err if defined $err && $err ne ''; | ||
| 1571 | $str; | ||
| 1572 | } | ||
| 1573 | |||
| 1574 | sub prolong_timer($;$) { | ||
| 1575 | my($which_section, $child_remaining_time) = @_; | ||
| 1576 | if (!defined($child_remaining_time)) { | ||
| 1577 | $child_remaining_time = alarm(0); # check how much time is left | ||
| 1578 | } | ||
| 1579 | do_log(4, "prolong_timer after $which_section: " | ||
| 1580 | . "remaining time = $child_remaining_time s"); | ||
| 1581 | $child_remaining_time = 60 if $child_remaining_time < 60; | ||
| 1582 | alarm($child_remaining_time); # restart/prolong the timer | ||
| 1583 | } | ||
| 1584 | |||
| 1585 | # Mostly for debugging and reporting purposes: | ||
| 1586 | # Convert nonprintable characters in the argument | ||
| 1587 | # to \[rnftbe], or \octal code, and '\' to '\\', | ||
| 1588 | # and Unicode characters to \x{xxxx}, returning the sanitized string. | ||
| 1589 | sub sanitize_str { | ||
| 1590 | my($str, $keep_eol) = @_; | ||
| 1591 | my(%map) = ("\r" => '\\r', "\n" => '\\n', "\f" => '\\f', "\t" => '\\t', | ||
| 1592 | "\b" => '\\b', "\e" => '\\e', "\\" => '\\\\'); | ||
| 1593 | if ($keep_eol) { | ||
| 1594 | $str =~ s/([^\012\040-\133\135-\176])/ # and \240-\376 ? | ||
| 1595 | exists($map{$1}) ? $map{$1} : | ||
| 1596 | sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg; | ||
| 1597 | } else { | ||
| 1598 | $str =~ s/([^\040-\133\135-\176])/ # and \240-\376 ? | ||
| 1599 | exists($map{$1}) ? $map{$1} : | ||
| 1600 | sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg; | ||
| 1601 | } | ||
| 1602 | $str; | ||
| 1603 | } | ||
| 1604 | |||
| 1605 | # pretty-print a structure for logging purposes: returns a string | ||
| 1606 | sub fmt_struct($) { | ||
| 1607 | my($arg) = @_; | ||
| 1608 | !defined($arg) ? 'undef' : !ref($arg) ? '"'.$arg.'"' : | ||
| 1609 | ref($arg) eq 'ARRAY' ? '['.join(',',map {fmt_struct($_)} @$arg).']' : $arg; | ||
| 1610 | }; | ||
| 1611 | |||
| 1612 | # Checks tempdir after being cleaned. | ||
| 1613 | # It may only contain subdirectory 'parts' and file email.txt, nothing else. | ||
| 1614 | # | ||
| 1615 | sub check_tempdir($) { | ||
| 1616 | my($dir) = shift; | ||
| 1617 | local(*DIR); opendir(DIR,$dir) or die "Can't open directory $dir: $!"; | ||
| 1618 | eval { | ||
| 1619 | undef $!, my($f); | ||
| 1620 | while (defined($f = readdir(DIR))) { | ||
| 1621 | if (!-d ("$dir/$f")) { | ||
| 1622 | die "Unexpected file $dir/$f" if $f ne 'email.txt'; | ||
| 1623 | } elsif ($f eq '.' || $f eq '..' || $f eq 'parts') { | ||
| 1624 | } else { | ||
| 1625 | die "Unexpected subdirectory $dir/$f"; | ||
| 1626 | } | ||
| 1627 | } | ||
| 1628 | # $!==0 or die "Error reading directory $dir: $!"; | ||
| 1629 | }; | ||
| 1630 | closedir(DIR) or die "Error closing directory $dir: $!"; | ||
| 1631 | if ($@ ne '') { chomp($@); die "check_tempdir: $@\n" } | ||
| 1632 | 1; | ||
| 1633 | } | ||
| 1634 | |||
| 1635 | # Remove all files and subdirectories from the temporary directory, leaving | ||
| 1636 | # only the directory itself, file email.txt, and empty subdirectory ./parts . | ||
| 1637 | # Leaving directories for reuse represents an important saving in time, | ||
| 1638 | # as directory creation + deletion is quite an expensive operation, | ||
| 1639 | # requiring atomic file system operation, including flushing buffers to disk. | ||
| 1640 | # | ||
| 1641 | sub strip_tempdir($) { | ||
| 1642 | my($dir) = shift; | ||
| 1643 | do_log(4, "strip_tempdir: $dir"); | ||
| 1644 | my($errn) = lstat("$dir/parts") ? 0 : 0+$!; | ||
| 1645 | if ($errn == ENOENT) {} # fine, no such directory | ||
| 1646 | elsif ($errn != 0) { die "strip_tempdir: error accessing $dir/parts: $!" } | ||
| 1647 | elsif ( -l _) { die "strip_tempdir: $dir/parts is a symbolic link" } | ||
| 1648 | elsif (!-d _) { die "strip_tempdir: $dir/parts is not a directory" } | ||
| 1649 | else { rmdir_recursively("$dir/parts", 1) } | ||
| 1650 | # All done. Check for any remains in the top directory just in case | ||
| 1651 | check_tempdir($dir); | ||
| 1652 | 1; | ||
| 1653 | } | ||
| 1654 | |||
| 1655 | # | ||
| 1656 | # Removes a directory, along with its contents | ||
| 1657 | sub rmdir_recursively($;$); # prototype | ||
| 1658 | sub rmdir_recursively($;$) { | ||
| 1659 | my($dir, $exclude_itself) = @_; my($cnt) = 0; | ||
| 1660 | do_log(4,"rmdir_recursively: $dir, excl=$exclude_itself"); | ||
| 1661 | local(*DIR); my($errn) = opendir(DIR,$dir) ? 0 : 0+$!; | ||
| 1662 | if ($errn == ENOENT) { die "Directory $dir does not exist," } | ||
| 1663 | elsif ($errn == EACCES) { # relax protection on directory, then try again | ||
| 1664 | do_log(3,"rmdir_recursively: enabling read access to directory $dir"); | ||
| 1665 | chmod(0750,$dir) or die "Can't change protection-1 on dir $dir: $!"; | ||
| 1666 | $errn = opendir(DIR,$dir) ? 0 : 0+$!; # try again | ||
| 1667 | } | ||
| 1668 | if ($errn) { die "Can't open directory $dir: $!" } | ||
| 1669 | my(@dirfiles) = readdir(DIR); # must avoid modifying dir. while traversing it | ||
| 1670 | closedir(DIR) or die "Error closing directory $dir: $!"; | ||
| 1671 | for my $f (@dirfiles) { | ||
| 1672 | my($fname) = "$dir/$f"; | ||
| 1673 | $errn = lstat($fname) ? 0 : 0+$!; | ||
| 1674 | if ($errn == ENOENT) { die "File \"$fname\" does not exist" } | ||
| 1675 | elsif ($errn == EACCES) { # relax protection on the directory and retry | ||
| 1676 | do_log(3,"rmdir_recursively: enabling access to files in dir $dir"); | ||
| 1677 | chmod(0750,$dir) or die "Can't change protection-2 on dir $dir: $!"; | ||
| 1678 | $errn = lstat($fname) ? 0 : 0+$!; # try again | ||
| 1679 | } | ||
| 1680 | if ($errn) { die "File \"$fname\" inaccessible: $!" } | ||
| 1681 | next if ($f eq '.' || $f eq '..') && -d _; | ||
| 1682 | if (-d _) { rmdir_recursively(untaint($fname), 0) } | ||
| 1683 | else { | ||
| 1684 | $cnt++; | ||
| 1685 | if (unlink(untaint($fname))) { # ok | ||
| 1686 | } else { # relax protection on the directory, then try again | ||
| 1687 | do_log(3,"rmdir_recursively: enabling write access to dir $dir"); | ||
| 1688 | my($what) = -l _ ? 'symlink' :-d _ ? 'directory' :'non-regular file'; | ||
| 1689 | chmod(0750,$dir) or die "Can't change protection-3 on dir $dir: $!"; | ||
| 1690 | unlink(untaint($fname)) or die "Can't remove $what $fname: $!"; | ||
| 1691 | } | ||
| 1692 | } | ||
| 1693 | } | ||
| 1694 | section_time("unlink-$cnt-files"); | ||
| 1695 | if (!$exclude_itself) { | ||
| 1696 | rmdir($dir) or die "rmdir_recursively: Can't remove directory $dir: $!"; | ||
| 1697 | section_time('rmdir'); | ||
| 1698 | } | ||
| 1699 | 1; | ||
| 1700 | } | ||
| 1701 | |||
| 1702 | # read a multiline string from a file - may be called from amavisd.conf | ||
| 1703 | sub read_text($;$) { | ||
| 1704 | my($filename, $encoding) = @_; | ||
| 1705 | my($inp) = IO::File->new; | ||
| 1706 | $inp->open($filename,'<') or die "Can't open file $filename for reading: $!"; | ||
| 1707 | if ($unicode_aware && $encoding ne '') { | ||
| 1708 | binmode($inp, ":encoding($encoding)") | ||
| 1709 | or die "Can't set :encoding($encoding) on file $filename: $!"; | ||
| 1710 | } | ||
| 1711 | my($str) = ''; # must not be undef, work around a Perl UTF8 bug | ||
| 1712 | my($nbytes,$buff); | ||
| 1713 | while (($nbytes=$inp->read($buff,16384)) > 0) { $str .= $buff } | ||
| 1714 | defined $nbytes or die "Error reading from $filename: $!"; | ||
| 1715 | $inp->close or die "Error closing $filename: $!"; | ||
| 1716 | $str; | ||
| 1717 | } | ||
| 1718 | |||
| 1719 | # attempt to read all user-visible replies from a l10n dir | ||
| 1720 | # This function auto-fills $notify_sender_templ, $notify_virus_sender_templ, | ||
| 1721 | # $notify_virus_admin_templ, $notify_virus_recips_templ, | ||
| 1722 | # $notify_spam_sender_templ and $notify_spam_admin_templ from files named | ||
| 1723 | # template-dsn.txt, template-virus-sender.txt, template-virus-admin.txt, | ||
| 1724 | # template-virus-recipient.txt, template-spam-sender.txt, | ||
| 1725 | # template-spam-admin.txt. If this is available, it uses the charset | ||
| 1726 | # file to do automatic charset conversion. Used by the Debian distribution. | ||
| 1727 | sub read_l10n_templates($;$) { | ||
| 1728 | my($dir) = @_; | ||
| 1729 | if (@_ > 1) # compatibility with Debian | ||
| 1730 | { my($l10nlang, $l10nbase) = @_; $dir = "$l10nbase/$l10nlang" } | ||
| 1731 | my($file_chset) = Amavis::Util::read_text("$dir/charset"); | ||
| 1732 | if ($file_chset =~ m{^(?:#[^\n]*\n)*([^./\n\s]+)(\s*[#\n].*)?$}s) { | ||
| 1733 | $file_chset = untaint($1); | ||
| 1734 | } else { | ||
| 1735 | die "Invalid charset $file_chset\n"; | ||
| 1736 | } | ||
| 1737 | $Amavis::Conf::notify_sender_templ = | ||
| 1738 | Amavis::Util::read_text("$dir/template-dsn.txt", $file_chset); | ||
| 1739 | $Amavis::Conf::notify_virus_sender_templ = | ||
| 1740 | Amavis::Util::read_text("$dir/template-virus-sender.txt", $file_chset); | ||
| 1741 | $Amavis::Conf::notify_virus_admin_templ = | ||
| 1742 | Amavis::Util::read_text("$dir/template-virus-admin.txt", $file_chset); | ||
| 1743 | $Amavis::Conf::notify_virus_recips_templ = | ||
| 1744 | Amavis::Util::read_text("$dir/template-virus-recipient.txt", $file_chset); | ||
| 1745 | $Amavis::Conf::notify_spam_sender_templ = | ||
| 1746 | Amavis::Util::read_text("$dir/template-spam-sender.txt", $file_chset); | ||
| 1747 | $Amavis::Conf::notify_spam_admin_templ = | ||
| 1748 | Amavis::Util::read_text("$dir/template-spam-admin.txt", $file_chset); | ||
| 1749 | } | ||
| 1750 | |||
| 1751 | #use CDB_File; | ||
| 1752 | #sub tie_hash($$) { | ||
| 1753 | # my($hashref, $filename) = @_; | ||
| 1754 | # CDB_File::create(%$hashref, $filename, "$filename.tmp$$") | ||
| 1755 | # or die "Can't create cdb $filename: $!"; | ||
| 1756 | # my($cdb) = tie(%$hashref,'CDB_File',$filename) | ||
| 1757 | # or die "Tie to $filename failed: $!"; | ||
| 1758 | # $hashref; | ||
| 1759 | #} | ||
| 1760 | |||
| 1761 | # read a lookup associative array (Perl hash) from a file - may be called | ||
| 1762 | # from amavisd.conf | ||
| 1763 | # | ||
| 1764 | # Format: one key per line, anything from '#' to the end of line | ||
| 1765 | # is considered a comment, but '#' within correctly quoted rfc2821 | ||
| 1766 | # addresses is not treated as a comment (e.g. a hash sign within | ||
| 1767 | # "strange # \"foo\" address"@example.com is part of the string). | ||
| 1768 | # Lines may contain a pair: key value, separated by whitespace, or key only, | ||
| 1769 | # in which case a value 1 is implied. Trailing whitespace is discarded, | ||
| 1770 | # empty lines (containing only whitespace and comment) are ignored. | ||
| 1771 | # Addresses (lefthand-side) are converted from rfc2821-quoted form | ||
| 1772 | # into internal (raw) form and inserted as keys into a given hash. | ||
| 1773 | # NOTE: the format is partly compatible with Postfix maps (not aliases): | ||
| 1774 | # no continuation lines are honoured, Postfix maps do not allow | ||
| 1775 | # rfc2821-quoted addresses containing whitespace, Postfix only allows | ||
| 1776 | # comments starting at the beginning of a line. | ||
| 1777 | # | ||
| 1778 | # The $hashref argument is returned for convenience, so that one can do | ||
| 1779 | # for example: | ||
| 1780 | # $per_recip_whitelist_sender_lookup_tables = { | ||
| 1781 | # '.my1.example.com' => read_hash({},'/var/amavis/my1-example-com.wl'), | ||
| 1782 | # '.my2.example.com' => read_hash({},'/var/amavis/my2-example-com.wl') } | ||
| 1783 | # or even simpler: | ||
| 1784 | # $per_recip_whitelist_sender_lookup_tables = { | ||
| 1785 | # '.my1.example.com' => read_hash('/var/amavis/my1-example-com.wl'), | ||
| 1786 | # '.my2.example.com' => read_hash('/var/amavis/my2-example-com.wl') } | ||
| 1787 | # | ||
| 1788 | sub read_hash(@) { | ||
| 1789 | unshift(@_,{}) if !ref $_[0]; # first argument is optional, defaults to {} | ||
| 1790 | my($hashref, $filename, $keep_case) = @_; | ||
| 1791 | my($lpcs) = c('localpart_is_case_sensitive'); | ||
| 1792 | my($inp) = IO::File->new; | ||
| 1793 | $inp->open($filename,'<') or die "Can't open file $filename for reading: $!"; | ||
| 1794 | my($ln); | ||
| 1795 | for (undef $!; defined($ln=$inp->getline); undef $!) { | ||
| 1796 | chomp($ln); | ||
| 1797 | # carefully handle comments, '#' within "" does not count as a comment | ||
| 1798 | my($lhs) = ''; my($rhs) = ''; my($at_rhs) = 0; | ||
| 1799 | for my $t ( $ln =~ /\G ( " (?: \\. | [^"\\] )* " | | ||
| 1800 | [^#" \t]+ | [ \t]+ | . )/gcsx) { | ||
| 1801 | last if $t eq '#'; | ||
| 1802 | if (!$at_rhs && $t =~ /^[ \t]+\z/) { $at_rhs = 1 } | ||
| 1803 | else { ($at_rhs ? $rhs : $lhs) .= $t } | ||
| 1804 | } | ||
| 1805 | $rhs =~ s/[ \t]+\z//; # trim trailing whitespace | ||
| 1806 | next if $lhs eq '' && $rhs eq ''; | ||
| 1807 | my($addr) = Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($lhs); | ||
| 1808 | my($localpart,$domain) = Amavis::rfc2821_2822_Tools::split_address($addr); | ||
| 1809 | $localpart = lc($localpart) if !$lpcs; | ||
| 1810 | $addr = $localpart . lc($domain); | ||
| 1811 | $hashref->{$addr} = $rhs eq '' ? 1 : $rhs; | ||
| 1812 | # do_log(5, "read_hash: address: <$addr>: ".$hashref->{$addr}); | ||
| 1813 | } | ||
| 1814 | defined $ln || $!==0 or die "Error reading from $filename: $!"; | ||
| 1815 | $inp->close or die "Error closing $filename: $!"; | ||
| 1816 | $hashref; | ||
| 1817 | } | ||
| 1818 | |||
| 1819 | sub read_array(@) { | ||
| 1820 | unshift(@_,[]) if !ref $_[0]; # first argument is optional, defaults to [] | ||
| 1821 | my($arrref, $filename, $keep_case) = @_; | ||
| 1822 | my($inp) = IO::File->new; | ||
| 1823 | $inp->open($filename,'<') or die "Can't open file $filename for reading: $!"; | ||
| 1824 | my($ln); | ||
| 1825 | for (undef $!; defined($ln=$inp->getline); undef $!) { | ||
| 1826 | chomp($ln); my($lhs) = ''; | ||
| 1827 | # carefully handle comments, '#' within "" does not count as a comment | ||
| 1828 | for my $t ( $ln =~ /\G ( " (?: \\. | [^"\\] )* " | | ||
| 1829 | [^#" \t]+ | [ \t]+ | . )/gcsx) { | ||
| 1830 | last if $t eq '#'; | ||
| 1831 | $lhs .= $t; | ||
| 1832 | } | ||
| 1833 | $lhs =~ s/[ \t]+\z//; # trim trailing whitespace | ||
| 1834 | push(@$arrref, Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($lhs)) | ||
| 1835 | if $lhs ne ''; | ||
| 1836 | } | ||
| 1837 | defined $ln || $!==0 or die "Error reading from $filename: $!"; | ||
| 1838 | $inp->close or die "Error closing $filename: $!"; | ||
| 1839 | $arrref; | ||
| 1840 | } | ||
| 1841 | |||
| 1842 | sub dump_hash($) { | ||
| 1843 | my($hr) = @_; | ||
| 1844 | do_log(0, sprintf("dump_hash: %s => %s", $_,$hr->{$_})) for (sort keys %$hr); | ||
| 1845 | } | ||
| 1846 | |||
| 1847 | sub dump_array($) { | ||
| 1848 | my($ar) = @_; | ||
| 1849 | do_log(0, sprintf("dump_array: %s", $_)) for @$ar; | ||
| 1850 | } | ||
| 1851 | |||
| 1852 | formorer | 189 | |
| 1853 | |||
| 1854 | # Run specified command as a subprocess. Return a file handle open for | ||
| 1855 | zobel | 21 | sub run_command($$@) { |
| 1856 | my($stdin_from, $stderr_to, $cmd, @args) = @_; | ||
| 1857 | my($cmd_text) = join(' ', $cmd, @args); | ||
| 1858 | formorer | 189 | $stdin_from = '/dev/null' if $stdin_from eq ''; |
| 1859 | $stderr_to = '/dev/null' if defined($stderr_to) && $stderr_to eq ''; | ||
| 1860 | my($msg) = join(' ', $cmd, @args, "<$stdin_from", | ||
| 1861 | $stderr_to eq '' ? () : "2>$stderr_to"); | ||
| 1862 | # $^F == 2 or do_log(-1,"run_command: SYSTEM_FD_MAX not 2: %d", $^F); | ||
| 1863 | zobel | 21 | my($pid); my($proc_fh) = IO::File->new; |
| 1864 | formorer | 189 | eval { |
| 1865 | $pid = $proc_fh->open('-|'); 1; # fork, catching errors | ||
| 1866 | } or do { | ||
| 1867 | my($eval_stat) = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; | ||
| 1868 | die "run_command (open pipe): $eval_stat"; | ||
| 1869 | }; | ||
| 1870 | zobel | 21 | defined($pid) or die "run_command: can't fork: $!"; |
| 1871 | formorer | 189 | if (!$pid) { # child |
| 1872 | alarm(0); my($interrupt) = ''; | ||
| 1873 | my($h1) = sub { $interrupt = $_[0] }; | ||
| 1874 | my($h2) = sub { die "Received signal ".$_[0] }; | ||
| 1875 | @SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)} = ($h1) x 7; | ||
| 1876 | eval { # die must be caught, otherwise we end up with two running daemons | ||
| 1877 | local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7; | ||
| 1878 | if ($interrupt ne '') { my($i) = $interrupt; $interrupt = ''; die $i } | ||
| 1879 | zobel | 21 | # use Devel::Symdump (); |
| 1880 | # my($dumpobj) = Devel::Symdump->rnew; | ||
| 1881 | # for my $k ($dumpobj->ios) { | ||
| 1882 | formorer | 189 | # no strict 'refs'; my($fn) = fileno($k); |
| 1883 | # if (!defined($fn)) { do_log(2, "not open %s", $k) } | ||
| 1884 | # elsif ($fn == 1 || $fn == 2) { do_log(2, "KEEP %s, fileno=%s",$k,$fn) } | ||
| 1885 | # else { $! = 0; | ||
| 1886 | # close(*{$k}{IO}) and do_log(2, "DID CLOSE %s (fileno=%s)", $k,$fn); | ||
| 1887 | zobel | 21 | # } |
| 1888 | # } | ||
| 1889 | formorer | 189 | release_parent_resources(); |
| 1890 | open_on_specific_fd(0,$stdin_from,&POSIX::O_RDONLY,0); | ||
| 1891 | open_on_specific_fd(2,$stderr_to,&POSIX::O_WRONLY,0) if $stderr_to ne ''; | ||
| 1892 | # eval { close_log() }; # may have been closed by open_on_specific_fd | ||
| 1893 | zobel | 21 | # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC |
| 1894 | formorer | 189 | exec {$cmd} ($cmd,@args); |
| 1895 | die "run_command: failed to exec $cmd_text: $!"; | ||
| 1896 | zobel | 21 | }; |
| 1897 | formorer | 189 | my($err) = $@ ne '' ? $@ : "errno=$!"; chomp $err; |
| 1898 | zobel | 21 | eval { |
| 1899 | formorer | 189 | local(@SIG{qw(INT HUP TERM TSTP QUIT USR1 USR2)}) = ($h2) x 7; |
| 1900 | if ($interrupt ne '') { my($i) = $interrupt; $interrupt = ''; die $i } | ||
| 1901 | zobel | 21 | open_log(); # oops, exec failed, we will need logging after all... |
| 1902 | formorer | 189 | # we're in trouble if stderr was attached to a terminal, but no longer is |
| 1903 | do_log(-1,sprintf("run_command: child process [%s]: %s", $$,$err)); | ||
| 1904 | zobel | 21 | }; |
| 1905 | { no warnings; | ||
| 1906 | formorer | 189 | POSIX::_exit(8); # avoid END and destructor processing |
| 1907 | kill('KILL',$$); exit 1; # still kicking? die! | ||
| 1908 | zobel | 21 | } |
| 1909 | } | ||
| 1910 | # parent | ||
| 1911 | formorer | 189 | ll(5) && do_log(5,sprintf("run_command: [%s] %s", $pid,$msg)); |
| 1912 | zobel | 21 | binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1 |
| 1913 | ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID | ||
| 1914 | } | ||
| 1915 | |||
| 1916 | formorer | 189 | # POSIX::open a file or dup an existing fd (Perl open syntax), with a |
| 1917 | # requirement that it gets opened on a prescribed file descriptor $fd_target; | ||
| 1918 | # this subroutine is usually called from a forked process prior to exec | ||
| 1919 | sub open_on_specific_fd($$$$) { | ||
| 1920 | my($fd_target,$fname,$flags,$mode) = @_; | ||
| 1921 | my($fd_got); # fd directy given as argument, or obtained from POSIX::open | ||
| 1922 | my($logging_safe) = 0; | ||
| 1923 | if (ll(5)) { | ||
| 1924 | # crude attempt to prevent a forked process from writing log records | ||
| 1925 | # to its parent process on STDOUT or STDERR | ||
| 1926 | my($log_fd) = log_fd(); | ||
| 1927 | $logging_safe = 1 if !defined($log_fd) || $log_fd > 2; | ||
| 1928 | } | ||
| 1929 | local($1); | ||
| 1930 | if ($fname =~ /^&=?(\d+)\z/) { $fd_got = $1 } # fd directly specified | ||
| 1931 | my($flags_displayed) = $flags == &POSIX::O_RDONLY ? '<' | ||
| 1932 | : $flags == &POSIX::O_WRONLY ? '>' : $flags; | ||
| 1933 | if (!defined($fd_got) || $fd_got != $fd_target) { | ||
| 1934 | # close whatever is on a target descriptor but don't shoot self in the foot | ||
| 1935 | # with Net::Server <= 0.90 fd0 was main::stdin, but no longer is in 0.91 | ||
| 1936 | do_log(5, sprintf("open_on_specific_fd: target fd%s closing, to become %s %s", | ||
| 1937 | $fd_target,$flags_displayed,$fname)) if $logging_safe; | ||
| 1938 | # it pays off to close explicitly, with some luck open will get a target fd | ||
| 1939 | POSIX::close($fd_target); # ignore error, we may have just closed a log | ||
| 1940 | } | ||
| 1941 | if (!defined($fd_got)) { # file name was given, not a descriptor | ||
| 1942 | $fd_got = POSIX::open($fname,$flags,$mode); | ||
| 1943 | defined $fd_got or die "Can't open $fname: $!"; | ||
| 1944 | $fd_got = 0 + $fd_got; # turn into numeric, avoid: "0 but true" | ||
| 1945 | } | ||
| 1946 | if ($fd_got != $fd_target) { # dup, ensuring we get a specified descriptor | ||
| 1947 | eval { # we may have been left without a log file descriptor, must not die | ||
| 1948 | do_log(5, sprintf("open_on_specific_fd: target fd%s dup2 from fd%s %s %s", | ||
| 1949 | $fd_target,$fd_got,$flags_displayed,$fname)) if $logging_safe; | ||
| 1950 | }; | ||
| 1951 | # POSIX mandates we got the lowest fd available (but some kernels have | ||
| 1952 | # bugs), let's be explicit that we require a specified file descriptor | ||
| 1953 | defined POSIX::dup2($fd_got,$fd_target) | ||
| 1954 | or die "Can't dup2 from $fd_got to $fd_target: $!"; | ||
| 1955 | if ($fd_got > 2) { # let's get rid of the original fd, unless 0,1,2 | ||
| 1956 | my($err); defined POSIX::close($fd_got) or $err = $!; | ||
| 1957 | $err = defined $err ? ": $err" : ''; | ||
| 1958 | eval { # we may have been left without a log file descriptor, don't die | ||
| 1959 | do_log(5, sprintf("open_on_specific_fd: source fd%s closed%s", | ||
| 1960 | $fd_got,$err)) if $logging_safe; | ||
| 1961 | }; | ||
| 1962 | } | ||
| 1963 | } | ||
| 1964 | $fd_got; | ||
| 1965 | } | ||
| 1966 | |||
| 1967 | sub release_parent_resources() { | ||
| 1968 | $Amavis::sql_dataset_conn_lookups->dbh_inactive(1) | ||
| 1969 | if $Amavis::sql_dataset_conn_lookups; | ||
| 1970 | $Amavis::sql_dataset_conn_storage->dbh_inactive(1) | ||
| 1971 | if $Amavis::sql_dataset_conn_storage; | ||
| 1972 | # undef $Amavis::sql_dataset_conn_lookups; | ||
| 1973 | # undef $Amavis::sql_dataset_conn_storage; | ||
| 1974 | # undef $Amavis::body_digest_cache; undef $Amavis::snmp_db; | ||
| 1975 | # undef $Amavis::db_env; | ||
| 1976 | } | ||
| 1977 | |||
| 1978 | zobel | 21 | # WRITING to the subprocess. Use IO::Handle to ensure the subprocess |
| 1979 | # will be automatically reclaimed in case of failure. | ||
| 1980 | # | ||
| 1981 | sub run_command_consumer($$@) { | ||
| 1982 | my($stdout_to, $stderr_to, $cmd, @args) = @_; | ||
| 1983 | my($cmd_text) = join(' ', $cmd, @args); | ||
| 1984 | $stdout_to = '/dev/null' if $stdout_to eq ''; | ||
| 1985 | my($msg) = join(' ', $cmd, @args, ">$stdout_to"); | ||
| 1986 | $msg .= " 2>$stderr_to" if $stderr_to ne ''; | ||
| 1987 | my($pid); my($proc_fh) = IO::File->new; | ||
| 1988 | eval { $pid = $proc_fh->open('|-') }; # fork, catching errors | ||
| 1989 | if ($@ ne '') { chomp($@); die "run_command_consumer (open pipe): $@" } | ||
| 1990 | defined($pid) or die "run_command_consumer: can't fork: $!"; | ||
| 1991 | if (!$pid) { # child | ||
| 1992 | eval { # must not use die in forked process, or we end up with | ||
| 1993 | # two running daemons! Close unneeded files. | ||
| 1994 | # $sql_dataset_conn_lookups->dbh_inactive(1) if $sql_dataset_conn_lookups; | ||
| 1995 | # $sql_dataset_conn_storage->dbh_inactive(1) if $sql_dataset_conn_storage; | ||
| 1996 | # $sql_dataset_conn_lookups = $sql_dataset_conn_storage = undef; | ||
| 1997 | close_log(); | ||
| 1998 | close(main::stderr) or die "Error closing main::stderr: $!"; | ||
| 1999 | close(main::stdout) or die "Error closing main::stdout: $!"; | ||
| 2000 | close(main::STDOUT) or die "Error closing main::STDOUT: $!"; | ||
| 2001 | open(STDOUT, ">$stdout_to") | ||
| 2002 | or die "Can't reopen STDOUT on $stdout_to: $!"; | ||
| 2003 | fileno(STDOUT) == 1 | ||
| 2004 | or die ("run_command_consumer: STDOUT not fd1: ".fileno(STDOUT)); | ||
| 2005 | if ($stderr_to ne '') { | ||
| 2006 | close(STDERR) or die "Error closing STDERR: $!"; | ||
| 2007 | open(STDERR, ">$stderr_to") | ||
| 2008 | or die "Can't open STDERR to $stderr_to: $!"; | ||
| 2009 | fileno(STDERR) == 2 | ||
| 2010 | or die ("run_command_consumer: STDERR not fd2: ".fileno(STDERR)); | ||
| 2011 | } | ||
| 2012 | # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC | ||
| 2013 | { no warnings; | ||
| 2014 | exec {$cmd} ($cmd,@args) or die "Failed to exec $cmd_text: $!"; | ||
| 2015 | } | ||
| 2016 | }; | ||
| 2017 | my($err) = $@; chomp($err); | ||
| 2018 | eval { | ||
| 2019 | open_log(); # oops, exec failed, we will need logging after all... | ||
| 2020 | do_log(-2,"run_command_consumer: child process [$$]: $err\n"); | ||
| 2021 | }; | ||
| 2022 | { no warnings; | ||
| 2023 | POSIX::_exit(1); # avoid END and destructor processing | ||
| 2024 | kill('KILL',$$) # still kicking? die! | ||
| 2025 | or do_log(-3,"run_command_consumer: TROUBLE - Panic1, can't die: $!"); | ||
| 2026 | do_log(-3,"run_command_consumer: TROUBLE - Panic2, can't die"); | ||
| 2027 | exit 1; # better safe than sorry | ||
| 2028 | # NOTREACHED | ||
| 2029 | } | ||
| 2030 | } | ||
| 2031 | # parent | ||
| 2032 | do_log(5,"run_command_consumer: [$pid] $msg"); | ||
| 2033 | binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1 | ||
| 2034 | ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID | ||
| 2035 | } | ||
| 2036 | |||
| 2037 | 1; | ||
| 2038 | |||
| 2039 | # | ||
| 2040 | package Amavis::rfc2821_2822_Tools; | ||
| 2041 | use strict; | ||
| 2042 | use re 'taint'; | ||
| 2043 | |||
| 2044 | BEGIN { | ||
| 2045 | use Exporter (); | ||
| 2046 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 2047 | $VERSION = '2.043'; | ||
| 2048 | @ISA = qw(Exporter); | ||
| 2049 | @EXPORT = qw( | ||
| 2050 | &iso8601_timestamp &iso8601_utc_timestamp &rfc2822_timestamp | ||
| 2051 | &received_line &parse_received | ||
| 2052 | &fish_out_ip_from_received &split_address &split_localpart &make_query_keys | ||
| 2053 | "e_rfc2821_local &qquote_rfc2821_local &unquote_rfc2821_local | ||
| 2054 | &one_response_for_all | ||
| 2055 | &EX_OK &EX_NOUSER &EX_UNAVAILABLE &EX_TEMPFAIL &EX_NOPERM); | ||
| 2056 | } | ||
| 2057 | use subs @EXPORT; | ||
| 2058 | |||
| 2059 | use POSIX qw(locale_h strftime); | ||
| 2060 | |||
| 2061 | BEGIN { | ||
| 2062 | eval { require 'sysexits.ph' }; # try to use the installed version | ||
| 2063 | # define the most important constants if undefined | ||
| 2064 | do { sub EX_OK() {0} } unless defined(&EX_OK); | ||
| 2065 | do { sub EX_NOUSER() {67} } unless defined(&EX_NOUSER); | ||
| 2066 | do { sub EX_UNAVAILABLE() {69} } unless defined(&EX_UNAVAILABLE); | ||
| 2067 | do { sub EX_TEMPFAIL() {75} } unless defined(&EX_TEMPFAIL); | ||
| 2068 | do { sub EX_NOPERM() {77} } unless defined(&EX_NOPERM); | ||
| 2069 | } | ||
| 2070 | |||
| 2071 | BEGIN { | ||
| 2072 | import Amavis::Conf qw(:platform $myhostname c cr ca); | ||
| 2073 | import Amavis::Util qw(ll do_log); | ||
| 2074 | } | ||
| 2075 | |||
| 2076 | # Given a Unix time, return the local time zone offset at that time | ||
| 2077 | # as a string +HHMM or -HHMM, appropriate for the RFC2822 date format. | ||
| 2078 | # Works also for non-full-hour zone offsets, and on systems where strftime | ||
| 2079 | # can not return TZ offset as a number; (c) Mark Martinec, GPL | ||
| 2080 | # | ||
| 2081 | sub get_zone_offset($) { | ||
| 2082 | my($t) = @_; | ||
| 2083 | my($d) = 0; # local zone offset in seconds | ||
| 2084 | for (1..3) { # match the date (with a safety loop limit just in case) | ||
| 2085 | my($r) = sprintf("%04d%02d%02d", (localtime($t))[5, 4, 3]) cmp | ||
| 2086 | sprintf("%04d%02d%02d", (gmtime($t + $d))[5, 4, 3]); | ||
| 2087 | if ($r == 0) { last } else { $d += $r * 24 * 3600 } | ||
| 2088 | } | ||
| 2089 | my($sl,$su) = (0,0); | ||
| 2090 | for ((localtime($t))[2,1,0]) { $sl = $sl * 60 + $_ } | ||
| 2091 | for ((gmtime($t + $d))[2,1,0]) { $su = $su * 60 + $_ } | ||
| 2092 | $d += $sl - $su; # add HMS difference (in seconds) | ||
| 2093 | my($sign) = $d >= 0 ? '+' : '-'; | ||
| 2094 | $d = -$d if $d < 0; | ||
| 2095 | $d = int(($d + 30) / 60.0); # give minutes, rounded | ||
| 2096 | sprintf("%s%02d%02d", $sign, int($d / 60), $d % 60); | ||
| 2097 | } | ||
| 2098 | |||
| 2099 | # Given a Unix numeric time (seconds since 1970-01-01T00:00Z), | ||
| 2100 | # provide date-time timestamp (local time) as specified in ISO 8601 (EN 28601) | ||
| 2101 | # | ||
| 2102 | sub iso8601_timestamp($;$$) { | ||
| 2103 | my($t,$suppress_zone,$separator) = @_; | ||
| 2104 | # can't use %z because some systems do not support it (is treated as %Z) | ||
| 2105 | my($s) = strftime("%Y%m%dT%H%M%S", localtime($t)); | ||
| 2106 | $s =~ s/T/$separator/ if defined $separator; | ||
| 2107 | $s .= get_zone_offset($t) unless $suppress_zone; | ||
| 2108 | $s; | ||
| 2109 | } | ||
| 2110 | |||
| 2111 | # Given a Unix numeric time (seconds since 1970-01-01T00:00Z), | ||
| 2112 | # provide date-time timestamp (UTC) as specified in ISO 8601 (EN 28601) | ||
| 2113 | # | ||
| 2114 | sub iso8601_utc_timestamp($;$$) { | ||
| 2115 | my($t,$suppress_zone,$separator) = @_; | ||
| 2116 | my($s) = strftime("%Y%m%dT%H%M%S", gmtime($t)); | ||
| 2117 | $s =~ s/T/$separator/ if defined $separator; | ||
| 2118 | $s .= 'Z' unless $suppress_zone; | ||
| 2119 | $s; | ||
| 2120 | } | ||
| 2121 | |||
| 2122 | # Given a Unix time, provide date-time timestamp as specified in RFC 2822 | ||
| 2123 | # (local time), to be used in header fields such as 'Date:' and 'Received:' | ||
| 2124 | # | ||
| 2125 | sub rfc2822_timestamp($) { | ||
| 2126 | my($t) = @_; | ||
| 2127 | my(@lt) = localtime($t); | ||
| 2128 | # can't use %z because some systems do not support it (is treated as %Z) | ||
| 2129 | # my($old_locale) = POSIX::setlocale(LC_TIME,"C"); # English dates required! | ||
| 2130 | my($zone_name) = strftime("%Z",@lt); | ||
| 2131 | my($s) = strftime("%a, %e %b %Y %H:%M:%S ", @lt); | ||
| 2132 | $s .= get_zone_offset($t); | ||
| 2133 | $s .= " (" . $zone_name . ")" if $zone_name !~ /^\s*\z/; | ||
| 2134 | # POSIX::setlocale(LC_TIME, $old_locale); # restore the locale | ||
| 2135 | $s; | ||
| 2136 | } | ||
| 2137 | |||
| 2138 | sub received_line($$$$) { | ||
| 2139 | my($conn, $msginfo, $id, $folded) = @_; | ||
| 2140 | my($smtp_proto, $recips) = ($conn->smtp_proto, $msginfo->recips); | ||
| 2141 | my($client_ip) = $conn->client_ip; | ||
| 2142 | if ($client_ip =~ /:/ && $client_ip !~ /^IPv6:/i) { | ||
| 2143 | $client_ip = 'IPv6:' . $client_ip; | ||
| 2144 | } | ||
| 2145 | my($s) = sprintf("from %s%s\n by %s%s (amavisd-new, %s)", | ||
| 2146 | ($conn->smtp_helo eq '' ? 'unknown' : $conn->smtp_helo), | ||
| 2147 | ($client_ip eq '' ? '' : " ([$client_ip])"), | ||
| 2148 | c('localhost_name'), | ||
| 2149 | ($conn->socket_ip eq '' ? '' | ||
| 2150 | : sprintf(" (%s [%s])", $myhostname, $conn->socket_ip) ), | ||
| 2151 | ($conn->socket_port eq '' ? 'unix socket' : "port ".$conn->socket_port) ); | ||
| 2152 | $s .= "\n with $smtp_proto" if $smtp_proto=~/^(ES|S|L)MTPS?A?\z/i; # rfc3848 | ||
| 2153 | $s .= "\n id $id" if $id ne ''; | ||
| 2154 | # do not disclose recipients if more than one | ||
| 2155 | $s .= "\n for " . qquote_rfc2821_local(@$recips) if @$recips == 1; | ||
| 2156 | $s .= ";\n " . rfc2822_timestamp($msginfo->rx_time); | ||
| 2157 | $s =~ s/\n//g if !$folded; | ||
| 2158 | $s; | ||
| 2159 | } | ||
| 2160 | |||
| 2161 | sub parse_received($) { | ||
| 2162 | my($received) = @_; | ||
| 2163 | local($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11); | ||
| 2164 | $received =~ s/\n([ \t])/$1/g; # unfold | ||
| 2165 | $received =~ s/[\n\r]//g; # delete remaining newlines if any | ||
| 2166 | my(%fields); | ||
| 2167 | while ($received =~ m{\G\s* | ||
| 2168 | ( \b(from|by) \s+ ( (?: \[ (?: \\. | [^\]\\] )* \] | [^;\s\[] )+ ) | ||
| 2169 | (?: \s* \( (?: ( [^\s\[]+ ) \s+ )? | ||
| 2170 | \[ ( (?: \\. | [^\]\\] )* ) \] \s* | ||
| 2171 | \) )? | ||
| 2172 | (?: .*? ) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) # junk | ||
| 2173 | | \b(via|with|id|for) \s+ | ||
| 2174 | ( (?: " (?: \\. | [^"\\] )* " | ||
| 2175 | | \[ (?: \\. | [^\]\\] )* \] | ||
| 2176 | | \\. | [0-9a-z]+ | . # greedy words avoid deep recursion | ||
| 2177 | )+? (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) ) | ||
| 2178 | | (;) \s* ( .*? ) \s* \z # time | ||
| 2179 | | (.*?) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) # junk | ||
| 2180 | ) ( (?: \s+ | (?: \( (?: \\. | [^)\\] )* \) ) )* ) }xgcsi) | ||
| 2181 | { | ||
| 2182 | my($v1, $v2, $v3, $comment) = ('') x 4; | ||
| 2183 | my($item, $field) = ($1, lc($2 || $6 || $8)); | ||
| 2184 | $field = '' if !defined($field); # mute a warning about uninit. value | ||
| 2185 | if ($field eq 'from' || $field eq 'by') { | ||
| 2186 | ($v1, $v2, $v3, $comment) = ($3, $4, $5, $11); | ||
| 2187 | } elsif ($field eq ';') { # time | ||
| 2188 | ($v1, $comment) = ($9, $11); | ||
| 2189 | } elsif (!defined($10) || $10 eq '') { # via|with|id|for | ||
| 2190 | ($v1, $comment) = ($7, $11); | ||
| 2191 | } else { # junk | ||
| 2192 | ($v1, $comment) = ($10, $11); | ||
| 2193 | } | ||
| 2194 | $comment =~ s/^\s+//; | ||
| 2195 | $comment =~ s/\s+\z//; | ||
| 2196 | $item =~ s/^\Q$field\E\s*//i; | ||
| 2197 | if (!exists $fields{$field}) { | ||
| 2198 | $fields{$field} = [$item, $v1, $v2, $v3, $comment]; | ||
| 2199 | ll(5) && do_log(5, sprintf("parse_received: %s = %s/%s/%s/%s", | ||
| 2200 | map { !defined($_) ? '' : length($_) <= 50 ? $_ | ||
| 2201 | : substr($_,0,50)."..." } | ||
| 2202 | ($field, @{$fields{$field}}) )) if $field ne ''; | ||
| 2203 | } | ||
| 2204 | } | ||
| 2205 | \%fields; | ||
| 2206 | } | ||
| 2207 | |||
| 2208 | sub fish_out_ip_from_received($) { | ||
| 2209 | my($received) = @_; | ||
| 2210 | my($ip); | ||
| 2211 | my($fields_ref) = parse_received($received); | ||
| 2212 | if (defined $fields_ref && exists $fields_ref->{'from'}) { | ||
| 2213 | my($item, $v1, $v2, $v3, $comment) = @{$fields_ref->{'from'}}; | ||
| 2214 | for (map {defined $_ ? $_ : ''} ($v3, $v2, $v1, $comment, $item)) { | ||
| 2215 | if (/ \[ (\d{1,3} (?: \. \d{1,3}){3}) \] /x) { | ||
| 2216 | $ip = $1; last; | ||
| 2217 | } elsif (/ (\d{1,3} (?: \. \d{1,3}){3}) (?!\d) /x) { | ||
| 2218 | $ip = $1; last; | ||
| 2219 | } elsif (/ \[ (IPv6:)? ( ([0-9a-zA-Z]* : ){2,} [0-9a-zA-Z:.]* ) \] /xi) { | ||
| 2220 | $ip = $2; last; | ||
| 2221 | } | ||
| 2222 | } | ||
| 2223 | do_log(5, "fish_out_ip_from_received: $ip, $item"); | ||
| 2224 | } | ||
| 2225 | !defined($ip) ? undef : $ip; # undef need not be tainted | ||
| 2226 | } | ||
| 2227 | |||
| 2228 | # Splits unquoted fully qualified e-mail address, or an address | ||
| 2229 | # with missing domain part. Returns a pair: (localpart, domain). | ||
| 2230 | # The domain part (if nonempty) includes the '@' as the first character. | ||
| 2231 | # If the syntax is badly broken, everything ends up as the localpart. | ||
| 2232 | # The domain part can be an address literal, as specified by rfc2822. | ||
| 2233 | # Does not handle explicit route paths. | ||
| 2234 | # | ||
| 2235 | sub split_address($) { | ||
| 2236 | my($mailbox) = @_; | ||
| 2237 | $mailbox =~ /^ (.*?) ( \@ (?: \[ (?: \\. | [^\]\\] )* \] | ||
| 2238 | | [^@"<>\[\]\\\s] )* | ||
| 2239 | ) \z/xs ? ($1, $2) : ($mailbox, ''); | ||
| 2240 | } | ||
| 2241 | |||
| 2242 | # split_localpart() splits localpart of an e-mail address at the first | ||
| 2243 | # occurrence of the address extension delimiter character. (based on | ||
| 2244 | # equivalent routine in Postfix) | ||
| 2245 | # | ||
| 2246 | # Reserved addresses are not split: postmaster, mailer-daemon, | ||
| 2247 | # double-bounce. Addresses that begin with owner-, or addresses | ||
| 2248 | # that end in -request are not split when the owner_request_special | ||
| 2249 | # parameter is set. | ||
| 2250 | |||
| 2251 | sub split_localpart($$) { | ||
| 2252 | my($localpart, $delimiter) = @_; | ||
| 2253 | my($owner_request_special) = 1; # configurable ??? | ||
| 2254 | my($extension); | ||
| 2255 | if ($localpart =~ /^(postmaster|mailer-daemon|double-bounce)\z/i) { | ||
| 2256 | # do not split these, regardless of what the delimiter is | ||
| 2257 | } elsif ($delimiter eq '-' && $owner_request_special && | ||
| 2258 | $localpart =~ /^owner-.|.-request\z/si) { | ||
| 2259 | # don't split owner-foo or foo-request | ||
| 2260 | } elsif ($localpart =~ /^(.+?)\Q$delimiter\E(.*)\z/s) { | ||
| 2261 | ($localpart, $extension) = ($1, $2); | ||
| 2262 | # do not split the address if the result would have a null localpart | ||
| 2263 | } | ||
| 2264 | ($localpart, $extension); | ||
| 2265 | } | ||
| 2266 | |||
| 2267 | # For a given email address (e.g. for User+Foo@sub.exAMPLE.CoM) | ||
| 2268 | # prepare and return a list of lookup keys in the following order: | ||
| 2269 | # User+Foo@sub.exAMPLE.COM (as-is, no lowercasing) | ||
| 2270 | # user+foo@sub.example.com | ||
| 2271 | # user@sub.example.com (only if $recipient_delimiter nonempty) | ||
| 2272 | # user+foo(@) (only if $include_bare_user) | ||
| 2273 | # user(@) (only if $include_bare_user and $recipient_delimiter nonempty) | ||
| 2274 | # (@)sub.example.com | ||
| 2275 | # (@).sub.example.com | ||
| 2276 | # (@).example.com | ||
| 2277 | # (@).com | ||
| 2278 | # (@). | ||
| 2279 | # Note about (@): if $at_with_user is true the user-only keys (without domain) | ||
| 2280 | # get an '@' character appended (e.g. 'user+foo@'). Usual for lookup_hash. | ||
| 2281 | # If $at_with_user is false the domain-only (without localpart) keys | ||
| 2282 | # get a '@' prepended (e.g. '@.example.com'). Usual for SQL and LDAP lookups. | ||
| 2283 | # | ||
| 2284 | # The domain part is lowercased in all but the first item in the resulting | ||
| 2285 | # list; the localpart is lowercased iff $localpart_is_case_sensitive is true. | ||
| 2286 | # | ||
| 2287 | sub make_query_keys($$$) { | ||
| 2288 | my($addr,$at_with_user,$include_bare_user) = @_; | ||
| 2289 | my($localpart,$domain) = split_address($addr); $domain = lc($domain); | ||
| 2290 | my($saved_full_localpart) = $localpart; | ||
| 2291 | $localpart = lc($localpart) if !c('localpart_is_case_sensitive'); | ||
| 2292 | # chop off leading @, and trailing dots | ||
| 2293 | $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s; | ||
| 2294 | my($extension); my($delim) = c('recipient_delimiter'); | ||
| 2295 | if ($delim ne '') { | ||
| 2296 | ($localpart,$extension) = split_localpart($localpart,$delim); | ||
| 2297 | } | ||
| 2298 | $extension = '' if !defined($extension); # mute warnings | ||
| 2299 | my($append_to_user,$prepend_to_domain) = $at_with_user ? ('@','') : ('','@'); | ||
| 2300 | my(@keys); # a list of query keys | ||
| 2301 | push(@keys, $addr); # as is | ||
| 2302 | push(@keys, $localpart.$delim.$extension.'@'.$domain) | ||
| 2303 | if $extension ne ''; # user+foo@example.com | ||
| 2304 | push(@keys, $localpart.'@'.$domain); # user@example.com | ||
| 2305 | if ($include_bare_user) { # typically enabled for local users only | ||
| 2306 | push(@keys, $localpart.$delim.$extension.$append_to_user) | ||
| 2307 | if $extension ne ''; # user+foo(@) | ||
| 2308 | push(@keys, $localpart.$append_to_user); # user(@) | ||
| 2309 | } | ||
| 2310 | push(@keys, $prepend_to_domain.$domain); # (@)sub.example.com | ||
| 2311 | if ($domain =~ /\[/) { # don't split address literals | ||
| 2312 | push(@keys, $prepend_to_domain.'.'); # (@). | ||
| 2313 | } else { | ||
| 2314 | my(@dkeys); my($d) = $domain; | ||
| 2315 | for (;;) { # (@).sub.example.com (@).example.com (@).com (@). | ||
| 2316 | push(@dkeys, $prepend_to_domain.'.'.$d); | ||
| 2317 | last if $d eq ''; | ||
| 2318 | $d = ($d =~ /^([^.]*)\.(.*)\z/s) ? $2 : ''; | ||
| 2319 | } | ||
| 2320 | if (@dkeys > 10) { @dkeys = @dkeys[$#dkeys-9 .. $#dkeys] } # sanity limit | ||
| 2321 | push(@keys,@dkeys); | ||
| 2322 | } | ||
| 2323 | my($keys_ref) = []; # remove duplicates | ||
| 2324 | for my $k (@keys) { push(@$keys_ref,$k) if !grep {$k eq $_} @$keys_ref } | ||
| 2325 | ll(5) && do_log(5,"query_keys: ".join(', ',@$keys_ref)); | ||
| 2326 | # the rhs replacement strings are similar to what would be obtained | ||
| 2327 | # by lookup_re() given the following regular expression: | ||
| 2328 | # /^( ( ( [^@]*? ) ( \Q$delim\E [^@]* )? ) (?: \@ (.*) ) )$/xs | ||
| 2329 | my($rhs) = [ # a list of right-hand side replacement strings | ||
| 2330 | $addr, # $1 = User+Foo@Sub.Example.COM | ||
| 2331 | $saved_full_localpart, # $2 = User+Foo | ||
| 2332 | $localpart, # $3 = user | ||
| 2333 | $delim.$extension, # $4 = +foo | ||
| 2334 | $domain, # $5 = sub.example.com | ||
| 2335 | ]; | ||
| 2336 | ($keys_ref, $rhs); | ||
| 2337 | } | ||
| 2338 | |||
| 2339 | # quote_rfc2821_local() quotes the local part of a mailbox address | ||
| 2340 | # (given in internal (unquoted) form), and returns external (quoted) | ||
| 2341 | # mailbox address, as per rfc2821. | ||
| 2342 | # | ||
| 2343 | # Internal (unquoted) form is used internally by amavisd-new and other mail sw, | ||
| 2344 | # external (quoted) form is used in SMTP commands and message headers. | ||
| 2345 | # | ||
| 2346 | # The quote_rfc2821_local() conversion is necessary because addresses | ||
| 2347 | # we get from certain MTAs are raw, with stripped-off quoting. | ||
| 2348 | # To re-insert message back via SMTP, the local-part of the address needs | ||
| 2349 | # to be quoted again if it contains reserved characters or otherwise | ||
| 2350 | # does not obey the dot-atom syntax, as specified in rfc2821. | ||
| 2351 | # Failing to do that gets us into trouble: amavis accepts message from MTA, | ||
| 2352 | # but is unable to hand it back to MTA after checking, receiving | ||
| 2353 | # '501 Bad address syntax' with every attempt. | ||
| 2354 | # | ||
| 2355 | sub quote_rfc2821_local($) { | ||
| 2356 | my($mailbox) = @_; | ||
| 2357 | # atext: any character except controls, SP, and specials (rfc2821/rfc2822) | ||
| 2358 | my($atext) = "a-zA-Z0-9!#\$%&'*/=?^_`{|}~+-"; | ||
| 2359 | # my($specials) = '()<>\[\]\\\\@:;,."'; | ||
| 2360 | my($localpart,$domain) = split_address($mailbox); | ||
| 2361 | if ($localpart !~ /^[$atext]+(\.[$atext]+)*\z/so) { # not dot-atom | ||
| 2362 | $localpart =~ s/(["\\])/\\$1/g; # quoted-pair | ||
| 2363 | # special case: Postfix hates ""@domain but is not so harsh on @domain | ||
| 2364 | $localpart = '"'.$localpart.'"' if $localpart ne ''; # make it a qcontent | ||
| 2365 | } | ||
| 2366 | $domain = '' if $domain eq '@'; # strip off empty domain entirely | ||
| 2367 | $localpart . $domain; | ||
| 2368 | } | ||
| 2369 | |||
| 2370 | # wraps the result of quote_rfc2821_local into angle brackets <...> ; | ||
| 2371 | # If given a list, it returns a list (possibly converted to | ||
| 2372 | # comma-separated scalar if invoked in scalar context), quoting each element; | ||
| 2373 | # | ||
| 2374 | sub qquote_rfc2821_local(@) { | ||
| 2375 | my(@r) = map { $_ eq '' ? '<>' : ('<' . quote_rfc2821_local($_) . '>') } @_; | ||
| 2376 | wantarray ? @r : join(', ', @r); | ||
| 2377 | } | ||
| 2378 | |||
| 2379 | # unquote_rfc2821_local() strips away the quoting from the local part | ||
| 2380 | # of an external (quoted) mailbox address, and returns internal (unquoted) | ||
| 2381 | # mailbox address, as per rfc2821. | ||
| 2382 | # | ||
| 2383 | # Internal (unquoted) form is used internally by amavisd-new and other mail sw, | ||
| 2384 | # external (quoted) form is used in SMTP commands and message headers. | ||
| 2385 | # | ||
| 2386 | sub unquote_rfc2821_local($) { | ||
| 2387 | my($mailbox) = @_; | ||
| 2388 | # the angle-bracket stripping is not really a duty of this subroutine, | ||
| 2389 | # as it should have been already done elsewhere, but for the time being | ||
| 2390 | # we do it here: | ||
| 2391 | $mailbox = $1 if $mailbox =~ /^ \s* < ( .* ) > \s* \z/xs; | ||
| 2392 | my($localpart,$domain) = split_address($mailbox); | ||
| 2393 | $localpart =~ s/ " | \\ (.) | \\ \z /$1/xsg; # unquote quoted-pairs | ||
| 2394 | $localpart . $domain; | ||
| 2395 | } | ||
| 2396 | |||
| 2397 | # Prepare a single SMTP response and an exit status as per sysexits.h | ||
| 2398 | # from individual per-recipient response codes, taking into account | ||
| 2399 | # sendmail milter specifics. Returns a triple: (smtp response, exit status, | ||
| 2400 | # an indication whether DSN is needed). | ||
| 2401 | # | ||
| 2402 | sub one_response_for_all($$$) { | ||
| 2403 | my($msginfo, $dsn_per_recip_capable, $am_id) = @_; | ||
| 2404 | my($smtp_resp, $exit_code, $dsn_needed); | ||
| 2405 | |||
| 2406 | my($delivery_method) = $msginfo->delivery_method; | ||
| 2407 | my($sender) = $msginfo->sender; | ||
| 2408 | my($per_recip_data) = $msginfo->per_recip_data; | ||
| 2409 | my($any_not_done) = scalar(grep { !$_->recip_done } @$per_recip_data); | ||
| 2410 | if ($delivery_method ne '' && $any_not_done) | ||
| 2411 | { die "Explicit forwarding, but not all recips done" } | ||
| 2412 | if (!@$per_recip_data) { # no recipients, nothing to do | ||
| 2413 | $smtp_resp = "250 2.5.0 Ok, id=$am_id"; $exit_code = EX_OK; | ||
| 2414 | do_log(5, "one_response_for_all <$sender>: no recipients, '$smtp_resp'"); | ||
| 2415 | } | ||
| 2416 | if (!defined $smtp_resp) { | ||
| 2417 | for my $r (@$per_recip_data) { # any 4xx code ? | ||
| 2418 | if ($r->recip_smtp_response =~ /^4/) # pick the first 4xx code | ||
| 2419 | { $smtp_resp = $r->recip_smtp_response; last } | ||
| 2420 | } | ||
| 2421 | if (!defined $smtp_resp) { | ||
| 2422 | for my $r (@$per_recip_data) { # any invalid code ? | ||
| 2423 | if ($r->recip_done && $r->recip_smtp_response !~ /^[245]/) { | ||
| 2424 | $smtp_resp = '451 4.5.0 Bad SMTP response code??? "' | ||
| 2425 | . $r->recip_smtp_response . '"'; | ||
| 2426 | last; # pick the first | ||
| 2427 | } | ||
| 2428 | } | ||
| 2429 | } | ||
| 2430 | if (defined $smtp_resp) { | ||
| 2431 | $exit_code = EX_TEMPFAIL; | ||
| 2432 | do_log(5, "one_response_for_all <$sender>: 4xx found, '$smtp_resp'"); | ||
| 2433 | } | ||
| 2434 | } | ||
| 2435 | # NOTE: a 2xx SMTP response code is set both by internal Discard | ||
| 2436 | # and by a genuine successful delivery. To distinguish between the two | ||
| 2437 | # we need to check $r->recip_destiny as well. | ||
| 2438 | # | ||
| 2439 | if (!defined $smtp_resp) { | ||
| 2440 | # if destiny for _all_ recipients is D_DISCARD, give Discard | ||
| 2441 | my($notall); | ||
| 2442 | for my $r (@$per_recip_data) { | ||
| 2443 | if ($r->recip_destiny == D_DISCARD) # pick the first DISCARD code | ||
| 2444 | { $smtp_resp = $r->recip_smtp_response if !defined $smtp_resp } | ||
| 2445 | else { $notall++; last } # one is not a discard, nogood | ||
| 2446 | } | ||
| 2447 | if ($notall) { $smtp_resp = undef } | ||
| 2448 | if (defined $smtp_resp) { | ||
| 2449 | # helper program will interpret 99 as discard | ||
| 2450 | $exit_code = $delivery_method eq '' ? 99 : EX_OK; | ||
| 2451 | do_log(5, "one_response_for_all <$sender>: all DISCARD, '$smtp_resp'"); | ||
| 2452 | } | ||
| 2453 | } | ||
| 2454 | if (!defined $smtp_resp) { | ||
| 2455 | # destiny for _all_ recipients is Discard or Reject, give 5xx | ||
| 2456 | # (and there is at least one Reject) | ||
| 2457 | my($notall, $done_level); | ||
| 2458 | my($bounce_cnt) = 0; | ||
| 2459 | for my $r (@$per_recip_data) { | ||
| 2460 | my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response); | ||
| 2461 | if ($dest == D_DISCARD) { | ||
| 2462 | # ok, this one is discard, let's see the rest | ||
| 2463 | } elsif ($resp =~ /^5/ && $dest != D_BOUNCE) { | ||
| 2464 | # prefer to report SMTP response code of genuine rejects | ||
| 2465 | # from MTA, over internal rejects by content filters | ||
| 2466 | if (!defined $smtp_resp || $r->recip_done > $done_level) | ||
| 2467 | { $smtp_resp = $resp; $done_level = $r->recip_done } | ||
| 2468 | } else { $notall++; last } # one is Pass or Bounce, nogood | ||
| 2469 | } | ||
| 2470 | if ($notall) { $smtp_resp = undef } | ||
| 2471 | if (defined $smtp_resp) { | ||
| 2472 | $exit_code = EX_UNAVAILABLE; | ||
| 2473 | do_log(5, "one_response_for_all <$sender>: REJECTs, '$smtp_resp'"); | ||
| 2474 | } | ||
| 2475 | } | ||
| 2476 | if (!defined $smtp_resp) { | ||
| 2477 | # mixed destiny => 2xx, but generate dsn for bounces and rejects | ||
| 2478 | my($rej_cnt) = 0; my($bounce_cnt) = 0; my($drop_cnt) = 0; | ||
| 2479 | for my $r (@$per_recip_data) { | ||
| 2480 | my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response); | ||
| 2481 | if ($resp =~ /^2/ && $dest == D_PASS) # genuine successful delivery | ||
| 2482 | { $smtp_resp = $resp if !defined $smtp_resp } | ||
| 2483 | $drop_cnt++ if $dest == D_DISCARD; | ||
| 2484 | if ($resp =~ /^5/) | ||
| 2485 | { if ($dest == D_BOUNCE) { $bounce_cnt++ } else { $rej_cnt++ } } | ||
| 2486 | } | ||
| 2487 | $exit_code = EX_OK; | ||
| 2488 | if (!defined $smtp_resp) { # no genuine Pass/2xx | ||
| 2489 | # declare success, we'll handle bounce | ||
| 2490 | $smtp_resp = "250 2.5.0 Ok, id=$am_id"; | ||
| 2491 | if ($any_not_done) { $smtp_resp .= ", continue delivery" } | ||
| 2492 | elsif ($delivery_method eq '') { $exit_code = 99 } # milter DISCARD | ||
| 2493 | } | ||
| 2494 | if ($rej_cnt + $bounce_cnt + $drop_cnt > 0) { | ||
| 2495 | $smtp_resp .= ", "; | ||
| 2496 | $smtp_resp .= "but " if $rej_cnt+$bounce_cnt+$drop_cnt<@$per_recip_data; | ||
| 2497 | $smtp_resp .= join ", and ", | ||
| 2498 | map { my($cnt, $nm) = @$_; | ||
| 2499 | !$cnt ? () : $cnt == @$per_recip_data ? $nm : "$cnt $nm" | ||
| 2500 | } ([$rej_cnt,'REJECT'], [$bounce_cnt,'BOUNCE'], [$drop_cnt,'DISCARD']); | ||
| 2501 | } | ||
| 2502 | $dsn_needed = | ||
| 2503 | ($bounce_cnt > 0 || ($rej_cnt > 0 && !$dsn_per_recip_capable)) ? 1 : 0; | ||
| 2504 | ll(5) && do_log(5,"one_response_for_all <$sender>: " | ||
| 2505 | . ($rej_cnt + $bounce_cnt + $drop_cnt > 0 ? 'mixed' : 'success') | ||
| 2506 | . ", r=$rej_cnt,b=$bounce_cnt,d=$drop_cnt" | ||
| 2507 | . ", dsn_needed=$dsn_needed, '$smtp_resp'"); | ||
| 2508 | } | ||
| 2509 | ($smtp_resp, $exit_code, $dsn_needed); | ||
| 2510 | } | ||
| 2511 | |||
| 2512 | 1; | ||
| 2513 | |||
| 2514 | # | ||
| 2515 | package Amavis::Lookup::RE; | ||
| 2516 | use strict; | ||
| 2517 | use re 'taint'; | ||
| 2518 | |||
| 2519 | BEGIN { | ||
| 2520 | use Exporter (); | ||
| 2521 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 2522 | $VERSION = '2.043'; | ||
| 2523 | @ISA = qw(Exporter); | ||
| 2524 | } | ||
| 2525 | BEGIN { import Amavis::Util qw(ll do_log fmt_struct) } | ||
| 2526 | |||
| 2527 | # Make an object out of the supplied lookup list | ||
| 2528 | # to make it distinguishable from simple ACL array | ||
| 2529 | sub new($$) { my($class) = shift; bless [@_], $class } | ||
| 2530 | |||
| 2531 | # lookup_re() performs a lookup for an e-mail address or other key string | ||
| 2532 | # against a list made up of regular expressions. | ||
| 2533 | # | ||
| 2534 | # A full unmodified e-mail address is always used, so splitting to localpart | ||
| 2535 | # and domain or lowercasing is NOT performed. The regexp is powerful enough | ||
| 2536 | # that this can be accomplished by its mechanisms. The routine is useful for | ||
| 2537 | # other RE tests besides the usual e-mail addresses, such as looking for | ||
| 2538 | # banned file names. | ||
| 2539 | # | ||
| 2540 | # Each element of the list can be ref to a pair, or directly a regexp | ||
| 2541 | # ('Regexp' object created by a qr operator, or just a (less efficient) | ||
| 2542 | # string containing a regular expression). If it is a pair, the first | ||
| 2543 | # element is treated as a regexp, and the second provides a value in case | ||
| 2544 | # the regexp matches. If not a pair, the implied result of a match is 1. | ||
| 2545 | # | ||
| 2546 | # The regular expression is taken as-is, no implicit anchoring or setting | ||
| 2547 | # case insensitivity is done, so do use a qr'(?i)^user@example\.com$', | ||
| 2548 | # and not a sloppy qr'user@example.com', which can easily backfire. | ||
| 2549 | # Also, if qr is used with a delimiter other than ' (apostrophe), make sure | ||
| 2550 | # to quote the @ and $ . | ||
| 2551 | # | ||
| 2552 | # The pattern allows for capturing of parenthesized substrings, which can | ||
| 2553 | # then be referenced from the result string using the $1, $2, ... notation, | ||
| 2554 | # as with the Perl m// operator. The number after a $ may be a multi-digit | ||
| 2555 | # decimal number. To avoid possible ambiguity the ${n} or $(n) form may be used | ||
| 2556 | # Substring numbering starts with 1. Nonexistent references evaluate to empty | ||
| 2557 | # strings. If any substitution is done, the result inherits the taintedness | ||
| 2558 | # of $addr. Keep in mind that $ and @ characters needs to be backslash-quoted | ||
| 2559 | # in qq() strings. Example: | ||
| 2560 | # $virus_quarantine_to = new_RE( | ||
| 2561 | # [ qr'^(.*)@example\.com$'i => 'virus-${1}@example.com' ], | ||
| 2562 | # [ qr'^(.*)(@[^@]*)?$'i => 'virus-${1}${2}' ] ); | ||
| 2563 | # | ||
| 2564 | # Example (equivalent to the example in lookup_acl): | ||
| 2565 | # $acl_re = Amavis::Lookup::RE->new( | ||
| 2566 | # qr'@me\.ac\.uk$'i, [qr'[@.]ac\.uk$'i=>0], qr'\.uk$'i ); | ||
| 2567 | # ($r,$k) = $acl_re->lookup_re('user@me.ac.uk'); | ||
| 2568 | # or $r = lookup(0, 'user@me.ac.uk', $acl_re); | ||
| 2569 | # | ||
| 2570 | # 'user@me.ac.uk' matches me.ac.uk, returns true and search stops | ||
| 2571 | # 'user@you.ac.uk' matches .ac.uk, returns false (because of =>0) and search stops | ||
| 2572 | # 'user@them.co.uk' matches .uk, returns true and search stops | ||
| 2573 | # 'user@some.com' does not match anything, falls through and returns false (undef) | ||
| 2574 | # | ||
| 2575 | # As a special allowance, the $addr argument may be a ref to a list of search | ||
| 2576 | # keys. At each step in traversing the supplied regexp list, all elements of | ||
| 2577 | # @$addr are tried. If any of them matches, the search stops. This is currently | ||
| 2578 | # used in banned names lookups, where all attributes of a part are given as a | ||
| 2579 | # list @$addr. | ||
| 2580 | |||
| 2581 | sub lookup_re($$;$) { | ||
| 2582 | my($self, $addr,$get_all) = @_; | ||
| 2583 | local($1,$2,$3,$4); my(@matchingkey,@result); | ||
| 2584 | for my $e (@$self) { # try each regexp in the list | ||
| 2585 | my($key,$r); | ||
| 2586 | if (ref($e) eq 'ARRAY') { # a pair: (regexp,result) | ||
| 2587 | ($key,$r) = ($e->[0], @$e < 2 ? 1 : $e->[1]); | ||
| 2588 | } else { # a single regexp (not a pair), implies result 1 | ||
| 2589 | ($key,$r) = ($e, 1); | ||
| 2590 | } | ||
| 2591 | ""=~/x{0}/; # braindead Perl: serves as explicit deflt for an empty regexp | ||
| 2592 | my(@rhs); # match, capturing parenthesized subpatterns in @rhs | ||
| 2593 | if (!ref($addr)) { @rhs = $addr =~ /$key/ } | ||
| 2594 | else { for (@$addr) { @rhs = /$key/; last if @rhs } } | ||
| 2595 | if (@rhs) { # regexp matches | ||
| 2596 | # do the righthand side replacements if any $n, ${n} or $(n) is specified | ||
| 2597 | if (!ref($r) && $r=~/\$/) { | ||
| 2598 | my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) } | ||
| 2599 | { my($j)=$2+$3+$4; $j<1 ? '' : $rhs[$j-1] }gxse; | ||
| 2600 | # bring taintedness of input to the result | ||
| 2601 | $r .= substr($addr,0,0) if $any; | ||
| 2602 | } | ||
| 2603 | push(@result,$r); push(@matchingkey,$key); | ||
| 2604 | last if !$get_all; | ||
| 2605 | } | ||
| 2606 | } | ||
| 2607 | if (!ll(5)) { | ||
| 2608 | # don't bother preparing log report which will not be printed | ||
| 2609 | } elsif (!@result) { | ||
| 2610 | do_log(5,sprintf("lookup_re(%s), no matches", fmt_struct($addr))); | ||
| 2611 | } else { # pretty logging | ||
| 2612 | my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b", | ||
| 2613 | e => "\e", a => "\a", t => "\t"); | ||
| 2614 | my(@mk) = @matchingkey; | ||
| 2615 | for my $mk (@mk) # undo the \-quoting, will be redone by logging routines | ||
| 2616 | { $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : $1 }egsx } | ||
| 2617 | if (!$get_all) { # first match wins | ||
| 2618 | do_log(5,sprintf('lookup_re(%s) matches key "%s", result=%s', | ||
| 2619 | fmt_struct($addr), $mk[0], fmt_struct($result[0]))); | ||
| 2620 | } else { # want all matches | ||
| 2621 | do_log(5,sprintf("lookup_re(%s) matches keys: %s", fmt_struct($addr), | ||
| 2622 | join(', ', map {sprintf('"%s"=>%s', $mk[$_],fmt_struct($result[$_]))} | ||
| 2623 | (0..$#result)))); | ||
| 2624 | } | ||
| 2625 | } | ||
| 2626 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } | ||
| 2627 | else { !wantarray ? \@result : (\@result, \@matchingkey) } | ||
| 2628 | } | ||
| 2629 | |||
| 2630 | 1; | ||
| 2631 | |||
| 2632 | # | ||
| 2633 | package Amavis::Lookup::IP; | ||
| 2634 | use strict; | ||
| 2635 | use re 'taint'; | ||
| 2636 | |||
| 2637 | BEGIN { | ||
| 2638 | use Exporter (); | ||
| 2639 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 2640 | $VERSION = '2.043'; | ||
| 2641 | @ISA = qw(Exporter); | ||
| 2642 | @EXPORT_OK = qw(&lookup_ip_acl); | ||
| 2643 | } | ||
| 2644 | use subs @EXPORT_OK; | ||
| 2645 | |||
| 2646 | BEGIN { | ||
| 2647 | import Amavis::Util qw(ll do_log); | ||
| 2648 | } | ||
| 2649 | |||
| 2650 | # ip_to_vec() takes IPv6 or IPv4 IP address with optional prefix length | ||
| 2651 | # (or IPv4 mask), parses and validates it, and returns it as a 128-bit | ||
| 2652 | # vector string that can be used as operand to Perl bitwise string operators. | ||
| 2653 | # Syntax and other errors in the argument throw exception (die). | ||
| 2654 | # If the second argument $allow_mask is 0, the prefix length or mask | ||
| 2655 | # specification is not allowed as part of the IP address. | ||
| 2656 | # | ||
| 2657 | # The IPv6 syntax parsing and validation adheres to rfc3513. | ||
| 2658 | # All the following IPv6 address forms are supported: | ||
| 2659 | # x:x:x:x:x:x:x:x preferred form | ||
| 2660 | # x:x:x:x:x:x:d.d.d.d alternative form | ||
| 2661 | # ...::... zero-compressed form | ||
| 2662 | # addr/prefix-length prefix length may be specified (defaults to 128) | ||
| 2663 | # Optionally an "IPv6:" prefix may be prepended to the IPv6 address | ||
| 2664 | # as specified by rfc2821. Brackets enclosing the address are allowed | ||
| 2665 | # for Postfix compatibility, e.g. [::1]/128 . | ||
| 2666 | # | ||
| 2667 | # The following IPv4 forms are allowed: | ||
| 2668 | # d.d.d.d | ||
| 2669 | # d.d.d.d/prefix-length CIDR mask length is allowed (defaults to 32) | ||
| 2670 | # d.d.d.d/m.m.m.m network mask (gets converted to prefix-length) | ||
| 2671 | # If prefix-length or a mask is specified with an IPv4 address, the address | ||
| 2672 | # may be shortened to d.d.d/n or d.d/n or d/n. Such truncation is allowed | ||
| 2673 | # for compatibility with earlier version, but is deprecated and is not | ||
| 2674 | # allowed for IPv6 addresses. | ||
| 2675 | # | ||
| 2676 | # IPv4 addresses and masks are converted to IPv4-mapped IPv6 addresses | ||
| 2677 | # of the form ::FFFF:d.d.d.d, The CIDR mask length (0..32) is converted | ||
| 2678 | # to IPv6 prefix-length (96..128). The returned vector strings resulting | ||
| 2679 | # from IPv4 and IPv6 forms are indistinguishable. | ||
| 2680 | # | ||
| 2681 | # NOTE: | ||
| 2682 | # d.d.d.d is equivalent to ::FFFF:d.d.d.d (IPv4-mapped IPv6 address) | ||
| 2683 | # which is not the same as ::d.d.d.d (IPv4-compatible IPv6 address) | ||
| 2684 | # | ||
| 2685 | # A triple is returned: | ||
| 2686 | # - IP address represented as a 128-bit vector (a string) | ||
| 2687 | # - network mask derived from prefix length, a 128-bit vector (string) | ||
| 2688 | # - prefix length as an integer (0..128) | ||
| 2689 | # | ||
| 2690 | sub ip_to_vec($;$) { | ||
| 2691 | my($ip,$allow_mask) = @_; | ||
| 2692 | my($ip_len); my(@ip_fields); | ||
| 2693 | local($1,$2,$3,$4,$5,$6); | ||
| 2694 | $ip =~ s/^[ \t]+//; $ip =~ s/[ \t\n]+\z//s; # trim | ||
| 2695 | my($ipa) = $ip; | ||
| 2696 | ($ipa,$ip_len) = ($1,$2) if $allow_mask && $ip =~ m{^([^/]*)/(.*)\z}s; | ||
| 2697 | $ipa = $1 if $ipa =~ m{^ \[ (.*) \] \z}xs; # discard optional brackets | ||
| 2698 | $ipa = $1 if $ipa =~ m{^(.*)%[A-Za-z0-9]+\z}s; # discard interface spec | ||
| 2699 | if ($ipa =~ m{^(IPv6:)?(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z}si){ | ||
| 2700 | # IPv6 alternative form x:x:x:x:x:x:d.d.d.d | ||
| 2701 | my(@d) = ($3,$4,$5,$6); | ||
| 2702 | !grep {$_ > 255} @d | ||
| 2703 | or die "Invalid decimal field value in IPv6 address: [$ip]\n"; | ||
| 2704 | $ipa = $2 . sprintf("%02X%02X:%02X%02X", @d); | ||
| 2705 | } elsif ($ipa =~ m{^\d{1,3}(?:\.\d{1,3}){0,3}\z}) { # IPv4 form | ||
| 2706 | my(@d) = split(/\./,$ipa,-1); | ||
| 2707 | !grep {$_ > 255} @d | ||
| 2708 | or die "Invalid field value in IPv4 address: [$ip]\n"; | ||
| 2709 | defined($ip_len) || @d==4 | ||
| 2710 | or die "IPv4 address [$ip] contains fewer than 4 fields\n"; | ||
| 2711 | $ipa = '::FFFF:' . sprintf("%02X%02X:%02X%02X", @d); # IPv4-mapped IPv6 | ||
| 2712 | if (!defined($ip_len)) { $ip_len = 32; # no length, defaults to /32 | ||
| 2713 | } elsif ($ip_len =~ /^\d{1,9}\z/) { # /n, IPv4 CIDR notation | ||
| 2714 | } elsif ($ip_len =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) { | ||
| 2715 | !grep {$_ > 255} ($1,$2,$3,$4) | ||
| 2716 | or die "Illegal field value in IPv4 mask: [$ip]\n"; | ||
| 2717 | my($mask1) = pack('C4',$1,$2,$3,$4); # /m.m.m.m | ||
| 2718 | my($len) = unpack("%b*",$mask1); # count ones | ||
| 2719 | my($mask2) = pack('B32', '1' x $len); # reconstruct mask from count | ||
| 2720 | $mask1 eq $mask2 | ||
| 2721 | or die "IPv4 mask not representing valid CIDR mask: [$ip]\n"; | ||
| 2722 | $ip_len = $len; | ||
| 2723 | } else { | ||
| 2724 | die "Invalid IPv4 network mask or CIDR prefix length: [$ip]\n"; | ||
| 2725 | } | ||
| 2726 | $ip_len<=32 or die "IPv4 network prefix length greater than 32: [$ip]\n"; | ||
| 2727 | $ip_len += 128-32; # convert IPv4 net mask length to IPv6 prefix length | ||
| 2728 | } | ||
| 2729 | $ip_len = 128 if !defined($ip_len); | ||
| 2730 | $ip_len<=128 or die "IPv6 network prefix length greater than 128: [$ip]\n"; | ||
| 2731 | $ipa =~ s/^IPv6://i; | ||
| 2732 | # now we presumably have an IPv6 preferred form x:x:x:x:x:x:x:x | ||
| 2733 | if ($ipa !~ /^(.*?)::(.*)\z/s) { # zero-compressing form used? | ||
| 2734 | @ip_fields = split(/:/,$ipa,-1); # no | ||
| 2735 | } else { # expand zero-compressing form | ||
| 2736 | my(@a) = split(/:/,$1,-1); my(@b) = split(/:/,$2,-1); | ||
| 2737 | my($missing_cnt) = 8-(@a+@b); $missing_cnt = 1 if $missing_cnt<1; | ||
| 2738 | @ip_fields = (@a, (0) x $missing_cnt, @b); | ||
| 2739 | } | ||
| 2740 | !grep { !/^[0-9a-zA-Z]{1,4}\z/ } @ip_fields # this is quite slow | ||
| 2741 | or die "Invalid syntax of IPv6 address: [$ip]\n"; | ||
| 2742 | @ip_fields<8 and die "IPv6 address [$ip] contains fewer than 8 fields\n"; | ||
| 2743 | @ip_fields>8 and die "IPv6 address [$ip] contains more than 8 fields\n"; | ||
| 2744 | my($vec) = pack("n8", map {hex} @ip_fields); | ||
| 2745 | $ip_len=~/^\d{1,3}\z/ | ||
| 2746 | or die "Invalid prefix length syntax in IP address: [$ip]\n"; | ||
| 2747 | $ip_len<=128 or die "Invalid prefix length in IPv6 address: [$ip]\n"; | ||
| 2748 | my($mask) = pack('B128', '1' x $ip_len); | ||
| 2749 | # do_log(5,sprintf("ip_to_vec: %s => %s/%d\n", $ip,unpack("B*",$vec),$ip_len)); | ||
| 2750 | ($vec,$mask,$ip_len); | ||
| 2751 | } | ||
| 2752 | |||
| 2753 | # lookup_ip_acl() performs a lookup for an IPv4 or IPv6 address | ||
| 2754 | # against access control list or a hash of network or host addresses. | ||
| 2755 | # | ||
| 2756 | # IP address is compared to each member of an access list in turn, | ||
| 2757 | # the first match wins (terminates the search), and its value decides | ||
| 2758 | # whether the result is true (yes, permit, pass) or false (no, deny, drop). | ||
| 2759 | # Falling through without a match produces false (undef). | ||
| 2760 | # | ||
| 2761 | # The presence of character '!' prepended to a list member decides | ||
| 2762 | # whether the result will be true (without a '!') or false (with '!') | ||
| 2763 | # in case this list member matches and terminates the search. | ||
| 2764 | # | ||
| 2765 | # Because search stops at the first match, it only makes sense | ||
| 2766 | # to place more specific patterns before the more general ones. | ||
| 2767 | # | ||
| 2768 | # For IPv4 a network address can be specified in classless notation | ||
| 2769 | # n.n.n.n/k, or using a mask n.n.n.n/m.m.m.m . Missing mask implies /32, | ||
| 2770 | # i.e. a host address. For IPv6 addresses all rfc3513 forms are allowed. | ||
| 2771 | # See also comments at ip_to_vec(). | ||
| 2772 | # | ||
| 2773 | # Although not a special case, it is good to remember that '::/0' | ||
| 2774 | # always matches any IPv4 or IPv6 address (even syntactically invalid address). | ||
| 2775 | # | ||
| 2776 | # The '0/0' is equivalent to '::FFFF:0:0/96' and matches any syntactically | ||
| 2777 | # valid IPv4 address (including IPv4-mapped IPv6 addresses), but not other | ||
| 2778 | # IPv6 addresses! | ||
| 2779 | # | ||
| 2780 | # Example | ||
| 2781 | # given: @acl = qw( !192.168.1.12 172.16.3.3 !172.16.3.0/255.255.255.0 | ||
| 2782 | # 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 | ||
| 2783 | # !0.0.0.0/8 !:: 127.0.0.0/8 ::1 ); | ||
| 2784 | # matches rfc1918 private address space except host 192.168.1.12 | ||
| 2785 | # and net 172.16.3/24 (but host 172.16.3.3 within 172.16.3/24 still matches). | ||
| 2786 | # In addition, the 'unspecified' (null, i.e. all zeros) IPv4 and IPv6 | ||
| 2787 | # addresses return false, and IPv4 and IPv6 loopback addresses match | ||
| 2788 | # and return true. | ||
| 2789 | # | ||
| 2790 | # If the supplied lookup table is a hash reference, match a canonical IP | ||
| 2791 | # address: dot-quad IPv4, or preferred IPv6 form, against hash keys. For IPv4 | ||
| 2792 | # addresses a simple classful subnet specification is allowed in hash keys | ||
| 2793 | # by truncating trailing bytes from the looked up IPv4 address. A syntactically | ||
| 2794 | # invalid IP address can only match a hash entry with an undef key. | ||
| 2795 | # | ||
| 2796 | sub lookup_ip_acl($@) { | ||
| 2797 | my($ip, @nets_ref) = @_; | ||
| 2798 | my($ip_vec,$ip_mask) = eval { ip_to_vec($ip,0) }; my($eval_stat) = $@; | ||
| 2799 | my($label,$fullkey,$result); my($found) = 0; | ||
| 2800 | for my $tb (@nets_ref) { | ||
| 2801 | my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection | ||
| 2802 | if (!ref($t) || ref($t) eq 'SCALAR') { # a scalar always matches | ||
| 2803 | my($r) = ref($t) ? $$t : $t; # allow direct or indirect reference | ||
| 2804 | $result = $r; $fullkey = "(constant:$r)"; | ||
| 2805 | $found++ if defined $result; | ||
| 2806 | } elsif (ref($t) eq 'HASH') { | ||
| 2807 | if (!defined $ip_vec) { # syntactically invalid IP address | ||
| 2808 | $fullkey = undef; $result = $t->{$fullkey}; | ||
| 2809 | $found++ if defined $result; | ||
| 2810 | } else { # valid IP address | ||
| 2811 | # match the canonical IP address: dot-quad IPv4, or preferred IPv6 form | ||
| 2812 | my($ip_c); # IP address in the canonical form: x:x:x:x:x:x:x:x | ||
| 2813 | my($ip_dq); # IPv4 in a dotted-quad form if IPv4-mapped, or undef | ||
| 2814 | $ip_c = join(':', map {sprintf('%04x',$_)} unpack('n8',$ip_vec)); | ||
| 2815 | my($ipv4_vec,$ipv4_mask) = ip_to_vec('::FFFF:0:0/96',1); | ||
| 2816 | if ( ($ip_vec & $ipv4_mask) eq ($ipv4_vec & $ipv4_mask) ) { | ||
| 2817 | # is an IPv4-mapped IPv6 address, format it in a dot-quad form | ||
| 2818 | $ip_dq = join('.', unpack('C4',substr($ip_vec,12,4))); # last 32 bits | ||
| 2819 | } | ||
| 2820 | do_log(5, "lookup_ip_acl keys: \"$ip_dq\", \"$ip_c\""); | ||
| 2821 | if (defined $ip_dq) { # try dot-quad if applicable | ||
| 2822 | for (my(@f)=split(/\./,$ip_dq); @f && !$found; $#f--) { | ||
| 2823 | $fullkey = join('.',@f); $result = $t->{$fullkey}; | ||
| 2824 | $found++ if defined $result; | ||
| 2825 | } | ||
| 2826 | } | ||
| 2827 | if (!$found) { # try the 'preferred IPv6 form' | ||
| 2828 | $fullkey = $ip_c; $result = $t->{$fullkey}; | ||
| 2829 | $found++ if defined $result; | ||
| 2830 | } | ||
| 2831 | } | ||
| 2832 | } elsif (ref($t) eq 'ARRAY') { | ||
| 2833 | my($key, $acl_ip_vec, $acl_mask, $acl_mask_len); | ||
| 2834 | for my $net (@$t) { | ||
| 2835 | $fullkey = $key = $net; $result = 1; | ||
| 2836 | if ($key =~ /^(!+)(.*)\z/s) { # starts with exclamation mark(s) | ||
| 2837 | $key = $2; | ||
| 2838 | $result = 1 - $result if (length($1) & 1); # negate if odd | ||
| 2839 | } | ||
| 2840 | ($acl_ip_vec, $acl_mask, $acl_mask_len) = ip_to_vec($key,1); | ||
| 2841 | if ($acl_mask_len == 0) { $found++ } # even invalid address matches /0 | ||
| 2842 | elsif (!defined($ip_vec)) {} # no other matches for invalid address | ||
| 2843 | elsif (($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask)) { $found++ } | ||
| 2844 | last if $found; | ||
| 2845 | } | ||
| 2846 | } elsif ($t->isa('Amavis::Lookup::IP')) { # pre-parsed IP lookup array obj | ||
| 2847 | my($acl_ip_vec, $acl_mask, $acl_mask_len); | ||
| 2848 | for my $e (@$t) { | ||
| 2849 | ($fullkey, $acl_ip_vec, $acl_mask, $acl_mask_len, $result) = @$e; | ||
| 2850 | if ($acl_mask_len == 0) { $found++ } # even invalid address matches /0 | ||
| 2851 | elsif (!defined($ip_vec)) {} # no other matches for invalid address | ||
| 2852 | elsif (($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask)) { $found++ } | ||
| 2853 | last if $found; | ||
| 2854 | } | ||
| 2855 | } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label | ||
| 2856 | # just a convenience for logging purposes, not a real lookup method | ||
| 2857 | $label = $t->display; # grab the name, and proceed with the next table | ||
| 2858 | } else { | ||
| 2859 | die "TROUBLE: lookup table is an unknown object: " . ref($t); | ||
| 2860 | } | ||
| 2861 | last if $found; | ||
| 2862 | } | ||
| 2863 | $fullkey = $result = undef if !$found; | ||
| 2864 | if ($label ne '') { $label = " ($label)" } | ||
| 2865 | ll(4) && do_log(4, "lookup_ip_acl$label: key=\"$ip\"" | ||
| 2866 | . (!$found ? ", no match" : " matches \"$fullkey\", result=$result")); | ||
| 2867 | if ($eval_stat eq '') { $eval_stat = undef } | ||
| 2868 | else { | ||
| 2869 | chomp($eval_stat); $eval_stat = "lookup_ip_acl$label: $eval_stat"; | ||
| 2870 | do_log(2, $eval_stat); | ||
| 2871 | } | ||
| 2872 | !wantarray ? $result : ($result, $fullkey, $eval_stat); | ||
| 2873 | } | ||
| 2874 | |||
| 2875 | # create a pre-parsed object from a list of IP networks, | ||
| 2876 | # which may be used as an argument to lookup_ip_acl to speed up its searches | ||
| 2877 | sub new($@) { | ||
| 2878 | my($class,@nets) = @_; | ||
| 2879 | my(@list); | ||
| 2880 | for my $net (@nets) { | ||
| 2881 | my($key) = $net; my($result) = 1; | ||
| 2882 | if ($key =~ /^(!+)(.*)\z/s) { # starts with exclamation mark(s) | ||
| 2883 | $key = $2; | ||
| 2884 | $result = 1 - $result if (length($1) & 1); # negate if odd | ||
| 2885 | } | ||
| 2886 | my($ip_vec, $ip_mask, $ip_mask_len) = ip_to_vec($key,1); | ||
| 2887 | push(@list, [$net, $ip_vec, $ip_mask, $ip_mask_len, $result]); | ||
| 2888 | } | ||
| 2889 | bless \@list, $class; | ||
| 2890 | } | ||
| 2891 | |||
| 2892 | 1; | ||
| 2893 | |||
| 2894 | # | ||
| 2895 | package Amavis::Lookup::Label; | ||
| 2896 | use strict; | ||
| 2897 | use re 'taint'; | ||
| 2898 | |||
| 2899 | # Make an object out of the supplied string, to serve as label | ||
| 2900 | # in log messages generated by sub lookup | ||
| 2901 | sub new($$) { my($class) = shift; my($str) = shift; bless \$str, $class } | ||
| 2902 | sub display($) { my($self) = shift; $$self } | ||
| 2903 | |||
| 2904 | 1; | ||
| 2905 | |||
| 2906 | # | ||
| 2907 | package Amavis::Lookup; | ||
| 2908 | use strict; | ||
| 2909 | use re 'taint'; | ||
| 2910 | |||
| 2911 | BEGIN { | ||
| 2912 | use Exporter (); | ||
| 2913 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 2914 | $VERSION = '2.043'; | ||
| 2915 | @ISA = qw(Exporter); | ||
| 2916 | @EXPORT_OK = qw(&lookup); | ||
| 2917 | } | ||
| 2918 | use subs @EXPORT_OK; | ||
| 2919 | |||
| 2920 | BEGIN { | ||
| 2921 | import Amavis::Util qw(ll do_log fmt_struct); | ||
| 2922 | import Amavis::Conf qw(:platform c cr ca); | ||
| 2923 | import Amavis::Timing qw(section_time); | ||
| 2924 | import Amavis::rfc2821_2822_Tools qw(split_address make_query_keys); | ||
| 2925 | } | ||
| 2926 | |||
| 2927 | # lookup_hash() performs a lookup for an e-mail address against a hash map. | ||
| 2928 | # If a match is found (a hash key exists in the Perl hash) the function returns | ||
| 2929 | # whatever the map returns, otherwise undef is returned. First match wins, | ||
| 2930 | # aborting further search sequence. | ||
| 2931 | # | ||
| 2932 | sub lookup_hash($$;$) { | ||
| 2933 | my($addr, $hash_ref,$get_all) = @_; | ||
| 2934 | (ref($hash_ref) eq 'HASH') | ||
| 2935 | or die "lookup_hash: arg2 must be a hash ref: $hash_ref"; | ||
| 2936 | local($1,$2,$3,$4); my(@matchingkey,@result); | ||
| 2937 | my($keys_ref,$rhs_ref) = make_query_keys($addr,1,1); | ||
| 2938 | for my $key (@$keys_ref) { # do the search | ||
| 2939 | if (exists $$hash_ref{$key}) { # got it | ||
| 2940 | push(@result,$$hash_ref{$key}); push(@matchingkey,$key); | ||
| 2941 | last if !$get_all; | ||
| 2942 | } | ||
| 2943 | } | ||
| 2944 | # do the right-hand side replacements if any $n, ${n} or $(n) is specified | ||
| 2945 | for my $r (@result) { # remember that $r is just an alias to array elements | ||
| 2946 | if (!ref($r) && $r=~/\$/) { # is a plain string containing a '$' | ||
| 2947 | my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) } | ||
| 2948 | { my($j)=$2+$3+$4; $j<1 ? '' : $rhs_ref->[$j-1] }gxse; | ||
| 2949 | # bring taintedness of input to the result | ||
| 2950 | $r .= substr($addr,0,0) if $any; | ||
| 2951 | } | ||
| 2952 | } | ||
| 2953 | if (!ll(5)) { | ||
| 2954 | # only bother with logging when needed | ||
| 2955 | } elsif (!@result) { | ||
| 2956 | do_log(5,"lookup_hash($addr), no matches"); | ||
| 2957 | } elsif (!$get_all) { # first match wins | ||
| 2958 | do_log(5,sprintf('lookup_hash(%s) matches key "%s", result=%s', | ||
| 2959 | $addr,$matchingkey[0],$result[0])); | ||
| 2960 | } else { # want all matches | ||
| 2961 | do_log(5,"lookup_hash($addr) matches keys: ". | ||
| 2962 | join(', ', map {sprintf('"%s"=>%s',$matchingkey[$_],$result[$_])} | ||
| 2963 | (0..$#result))); | ||
| 2964 | } | ||
| 2965 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } | ||
| 2966 | else { !wantarray ? \@result : (\@result, \@matchingkey) } | ||
| 2967 | } | ||
| 2968 | |||
| 2969 | # lookup_acl() performs a lookup for an e-mail address against | ||
| 2970 | # access control list. | ||
| 2971 | # | ||
| 2972 | # The supplied e-mail address is compared with each member of the | ||
| 2973 | # lookup list in turn, the first match wins (terminates the search), | ||
| 2974 | # and its value decides whether the result is true (yes, permit, pass) | ||
| 2975 | # or false (no, deny, drop). Falling through without a match | ||
| 2976 | # produces false (undef). Search is case-insensitive. | ||
| 2977 | # | ||
| 2978 | # lookup_acl is not aware of address extensions and they are not | ||
| 2979 | # handled specially. | ||
| 2980 | # | ||
| 2981 | # If a list element contains a '@', the full e-mail address is compared, | ||
| 2982 | # otherwise if a list element has a leading dot, the domain name part is | ||
| 2983 | # matched only, and the domain as well as its subdomains can match. If there | ||
| 2984 | # is no leading dot, the domain must match exactly (subdomains do not match). | ||
| 2985 | # | ||
| 2986 | # The presence of character '!' prepended to a list element decides | ||
| 2987 | # whether the result will be true (without a '!') or false (with '!') | ||
| 2988 | # in case this list element matches and terminates the search. | ||
| 2989 | # | ||
| 2990 | # Because search stops at the first match, it only makes sense | ||
| 2991 | # to place more specific patterns before the more general ones. | ||
| 2992 | # | ||
| 2993 | # Although not a special case, it is good to remember that '.' always matches, | ||
| 2994 | # so a '.' would stop the search and return true, whereas '!.' would stop the | ||
| 2995 | # search and return false (0). | ||
| 2996 | # | ||
| 2997 | # Examples: | ||
| 2998 | # | ||
| 2999 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) | ||
| 3000 | # 'me.ac.uk' matches me.ac.uk, returns true and search stops | ||
| 3001 | # | ||
| 3002 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) | ||
| 3003 | # 'you.ac.uk' matches .ac.uk, returns false (because of '!') and search stops | ||
| 3004 | # | ||
| 3005 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) | ||
| 3006 | # 'them.co.uk' matches .uk, returns true and search stops | ||
| 3007 | # | ||
| 3008 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) | ||
| 3009 | # 'some.com' does not match anything, falls through and returns false (undef) | ||
| 3010 | # | ||
| 3011 | # given: @acl = qw( me.ac.uk !.ac.uk .uk !. ) | ||
| 3012 | # 'some.com' similar to previous, except it returns 0 instead of undef, | ||
| 3013 | # which would only make a difference if this ACL is not the last argument | ||
| 3014 | # in a call to lookup() | ||
| 3015 | # | ||
| 3016 | # given: @acl = qw( me.ac.uk !.ac.uk .uk . ) | ||
| 3017 | # 'some.com' matches catchall ".", and returns true. The ".uk" is redundant | ||
| 3018 | # | ||
| 3019 | # more complex example: @acl = qw( | ||
| 3020 | # !The.Boss@dept1.xxx.com .dept1.xxx.com | ||
| 3021 | # .dept2.xxx.com .dept3.xxx.com lab.dept4.xxx.com | ||
| 3022 | # sub.xxx.com !.sub.xxx.com | ||
| 3023 | # me.d.aaa.com him.d.aaa.com !.d.aaa.com .aaa.com | ||
| 3024 | # ); | ||
| 3025 | |||
| 3026 | sub lookup_acl($$) { | ||
| 3027 | my($addr, $acl_ref) = @_; | ||
| 3028 | (ref($acl_ref) eq 'ARRAY') | ||
| 3029 | or die "lookup_acl: arg2 must be a list ref: $acl_ref"; | ||
| 3030 | return undef if !@$acl_ref; # empty list can't match anything | ||
| 3031 | my($lpcs) = c('localpart_is_case_sensitive'); | ||
| 3032 | my($localpart,$domain) = split_address($addr); $domain = lc($domain); | ||
| 3033 | $localpart = lc($localpart) if !$lpcs; | ||
| 3034 | local($1,$2); | ||
| 3035 | # chop off leading @ and trailing dots | ||
| 3036 | $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s; | ||
| 3037 | my($lcaddr) = $localpart . '@' . $domain; | ||
| 3038 | my($matchingkey, $result); my($found) = 0; | ||
| 3039 | for my $e (@$acl_ref) { | ||
| 3040 | $result = 1; $matchingkey = $e; my($key) = $e; | ||
| 3041 | if ($key =~ /^(!+)(.*)\z/s) { # starts with an exclamation mark(s) | ||
| 3042 | $key = $2; | ||
| 3043 | $result = 1-$result if (length($1) & 1); # negate if odd | ||
| 3044 | } | ||
| 3045 | if ($key =~ /^(.*?)\@([^@]*)\z/s) { # contains '@', check full address | ||
| 3046 | $found++ if $localpart eq ($lpcs?$1:lc($1)) && $domain eq lc($2); | ||
| 3047 | } elsif ($key =~ /^\.(.*)\z/s) { # leading dot: domain or subdomain | ||
| 3048 | my($key_t) = lc($1); | ||
| 3049 | $found++ if $domain eq $key_t || $domain =~ /(\.|\z)\Q$key_t\E\z/s; | ||
| 3050 | } else { # match domain (but not its subdomains) | ||
| 3051 | $found++ if $domain eq lc($key); | ||
| 3052 | } | ||
| 3053 | last if $found; | ||
| 3054 | } | ||
| 3055 | $matchingkey = $result = undef if !$found; | ||
| 3056 | do_log(5, "lookup_acl($addr)". | ||
| 3057 | (!$found?", no match":" matches key \"$matchingkey\", result=$result")); | ||
| 3058 | !wantarray ? $result : ($result, $matchingkey); | ||
| 3059 | } | ||
| 3060 | |||
| 3061 | # Perform a lookup for an e-mail address against any number of supplied maps: | ||
| 3062 | # - SQL map, | ||
| 3063 | # - LDAP map, | ||
| 3064 | # - hash map (associative array), | ||
| 3065 | # - (access control) list, | ||
| 3066 | # - a list of regular expressions (an Amavis::Lookup::RE object), | ||
| 3067 | # - a (defined) scalar always matches, and returns itself as the 'map' value | ||
| 3068 | # (useful as a catchall for final 'pass' or 'fail'); | ||
| 3069 | # (see lookup_hash, lookup_acl, lookup_sql and lookup_ldap for details). | ||
| 3070 | # | ||
| 3071 | # when $get_all is 0 (the common usage): | ||
| 3072 | # If a match is found (a defined value), returns whatever the map returns, | ||
| 3073 | # otherwise returns undef. FIRST match aborts further search sequence. | ||
| 3074 | # when $get_all is true: | ||
| 3075 | # Collects a list of results from ALL matching tables, and within each | ||
| 3076 | # table from ALL matching key. Returns a ref to the a list of results | ||
| 3077 | # (and a ref to a list of matching keys if returning a pair). | ||
| 3078 | # The first element of both lists is supposed to be what lookup() would | ||
| 3079 | # have returned if $get_all were 0. The order of returned elements | ||
| 3080 | # corresponds to the order of the search. | ||
| 3081 | # | ||
| 3082 | sub lookup($$@) { | ||
| 3083 | my($get_all, $addr, @tables) = @_; | ||
| 3084 | my($label, @result,@matchingkey); | ||
| 3085 | for my $tb (@tables) { | ||
| 3086 | my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection | ||
| 3087 | if (!ref($t) || ref($t) eq 'SCALAR') { # a scalar always matches | ||
| 3088 | my($r) = ref($t) ? $$t : $t; # allow direct or indirect reference | ||
| 3089 | if (defined $r) { | ||
| 3090 | do_log(5,"lookup: (scalar) matches, result=\"$r\""); | ||
| 3091 | push(@result,$r); push(@matchingkey,"(constant:$r)"); | ||
| 3092 | } | ||
| 3093 | } elsif (ref($t) eq 'HASH') { | ||
| 3094 | my($r,$mk) = lookup_hash($addr,$t,$get_all); | ||
| 3095 | if (!defined $r) {} | ||
| 3096 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } | ||
| 3097 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } | ||
| 3098 | } elsif (ref($t) eq 'ARRAY') { | ||
| 3099 | my($r,$mk) = lookup_acl($addr,$t); | ||
| 3100 | if (defined $r) { push(@result,$r); push(@matchingkey,$mk) } | ||
| 3101 | } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label | ||
| 3102 | # just a convenience for logging purposes, not a real lookup method | ||
| 3103 | $label = $t->display; # grab the name, and proceed with the next table | ||
| 3104 | } elsif ($t->isa('Amavis::Lookup::RE')) { | ||
| 3105 | my($r,$mk) = $t->lookup_re($addr,$get_all); | ||
| 3106 | if (!defined $r) {} | ||
| 3107 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } | ||
| 3108 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } | ||
| 3109 | } elsif ($t->isa('Amavis::Lookup::SQL')) { | ||
| 3110 | my($r,$mk) = $t->lookup_sql($addr,$get_all); | ||
| 3111 | if (!defined $r) {} | ||
| 3112 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } | ||
| 3113 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } | ||
| 3114 | } elsif ($t->isa('Amavis::Lookup::SQLfield')) { | ||
| 3115 | my($r,$mk) = $t->lookup_sql_field($addr,$get_all); | ||
| 3116 | if (!defined $r) {} | ||
| 3117 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } | ||
| 3118 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } | ||
| 3119 | } elsif ($t->isa('Amavis::Lookup::LDAP')) { | ||
| 3120 | my($r,$mk) = $t->lookup_ldap($addr,$get_all); | ||
| 3121 | if (!defined $r) {} | ||
| 3122 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } | ||
| 3123 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } | ||
| 3124 | } elsif ($t->isa('Amavis::Lookup::LDAPattr')) { | ||
| 3125 | my($r,$mk) = $t->lookup_ldap_attr($addr,$get_all); | ||
| 3126 | if (!defined $r) {} | ||
| 3127 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } | ||
| 3128 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } | ||
| 3129 | } else { | ||
| 3130 | die "TROUBLE: lookup table is an unknown object: " . ref($t); | ||
| 3131 | } | ||
| 3132 | last if @result && !$get_all; | ||
| 3133 | } | ||
| 3134 | # pretty logging | ||
| 3135 | if (ll(4)) { # only bother preparing log report which will be printed | ||
| 3136 | if (defined $label && $label ne '') { $label = " ($label)" } | ||
| 3137 | if (!@tables) { | ||
| 3138 | do_log(4,sprintf("lookup%s => undef, %s, no lookup tables", | ||
| 3139 | $label, fmt_struct($addr))); | ||
| 3140 | } elsif (!@result) { | ||
| 3141 | do_log(4,sprintf("lookup%s => undef, %s does not match", | ||
| 3142 | $label, fmt_struct($addr))); | ||
| 3143 | } elsif (!$get_all) { # first match wins | ||
| 3144 | do_log(4,sprintf( | ||
| 3145 | 'lookup%s => %-6s %s matches, result=%s, matching_key="%s"', | ||
| 3146 | $label, $result[0] ? 'true,' : 'false,', | ||
| 3147 | fmt_struct($addr), fmt_struct($result[0]), $matchingkey[0])); | ||
| 3148 | } else { # want all matches | ||
| 3149 | do_log(4,sprintf('lookup%s, %d matches for %s, results: %s', | ||
| 3150 | $label, scalar(@result), fmt_struct($addr), | ||
| 3151 | join(', ',map { sprintf('"%s"=>%s', | ||
| 3152 | $matchingkey[$_], fmt_struct($result[$_])) | ||
| 3153 | } (0..$#result) ))); | ||
| 3154 | } | ||
| 3155 | } | ||
| 3156 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } | ||
| 3157 | else { !wantarray ? \@result : (\@result, \@matchingkey) } | ||
| 3158 | } | ||
| 3159 | |||
| 3160 | 1; | ||
| 3161 | |||
| 3162 | # | ||
| 3163 | package Amavis::Expand; | ||
| 3164 | use strict; | ||
| 3165 | use re 'taint'; | ||
| 3166 | |||
| 3167 | BEGIN { | ||
| 3168 | use Exporter (); | ||
| 3169 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 3170 | $VERSION = '2.043'; | ||
| 3171 | @ISA = qw(Exporter); | ||
| 3172 | @EXPORT_OK = qw(&expand); | ||
| 3173 | } | ||
| 3174 | use subs @EXPORT_OK; | ||
| 3175 | BEGIN { | ||
| 3176 | import Amavis::Util qw(ll do_log); | ||
| 3177 | } | ||
| 3178 | |||
| 3179 | # Given a string reference and a hashref of predefined (builtin) macros, | ||
| 3180 | # expand() performs a macro expansion and returns a ref to the resulting string | ||
| 3181 | # | ||
| 3182 | # This is a simple, yet fully fledged macro processor with proper lexical | ||
| 3183 | # analysis, call stack, implied quoting levels, user supplied builtin macros, | ||
| 3184 | # two builtin flow-control macros: selector and iterator, plus a macro #, | ||
| 3185 | # which discards input tokens until NEWLINE (like 'dnl' in m4). | ||
| 3186 | # Also recognized are the usual \c and \nnn forms for specifying special | ||
| 3187 | # characters, where c can be any of: r, n, f, b, e, a, t. Lexical analysis | ||
| 3188 | # of the input string is performed only once, macro result values are not | ||
| 3189 | # in danger of being lexically re-parsed and are treated as plain characters, | ||
| 3190 | # loosing any special meaning they might have. No new macros can be defined | ||
| 3191 | # by processing input string (at least in this version). | ||
| 3192 | # | ||
| 3193 | # Simple caller-provided macros have a single character name (usually a letter) | ||
| 3194 | # and can evaluate to a string (possibly empty or undef), or an array of | ||
| 3195 | # strings. It can also be a subroutine reference, in which case the subroutine | ||
| 3196 | # will be called whenever macro value is needed. The subroutine must return | ||
| 3197 | # a scalar: a string, or an array reference. The result will be treated as if | ||
| 3198 | # it were specified directly. | ||
| 3199 | # | ||
| 3200 | # Two forms of simple macro calls are known: %x and %#x (where x is a single | ||
| 3201 | # letter macro name, i.e. a key in a user-supplied associative array): | ||
| 3202 | # %x evaluates to the hash value associated with the name x; | ||
| 3203 | # if the value is an array ref, the result is a single concatenated | ||
| 3204 | # string of values separated with comma-space pairs; | ||
| 3205 | # %#x evaluates to a number: if a macro value is a scalar, returns 0 | ||
| 3206 | # for all-whitespace value, and 1 otherwise. If a value is an array ref, | ||
| 3207 | # evaluates to the number of elements in the array. | ||
| 3208 | # A macro is evaluated only in nonquoted context, i.e. top-level text or in | ||
| 3209 | # the first argument of a top-level selector (see below). A literal percent | ||
| 3210 | # character can be produced by %% or \%. | ||
| 3211 | # | ||
| 3212 | # More powerful expansion is provided by two builtin macros, using syntax: | ||
| 3213 | # [? arg1 | arg2 | ... ] a selector | ||
| 3214 | # [ arg1 | arg2 | ... ] an iterator | ||
| 3215 | # where [, [?, | and ] are required tokens. To take away the special meaning | ||
| 3216 | # of these characters they can be quoted by a backslash, e.g. \[ or \\ . | ||
| 3217 | # Arguments are arbitrary text, possibly multiline, whitespace counts. | ||
| 3218 | # Nested macro calls are permitted, proper bracket nesting must be observed. | ||
| 3219 | # | ||
| 3220 | # SELECTOR lets its first argument be evaluated immediately, and implicitly | ||
| 3221 | # protects the remaining arguments. The evaluated first argument chooses which | ||
| 3222 | # of the remaining arguments is selected as a result value. The chosen result | ||
| 3223 | # is only then evaluated, remaining arguments are discarded without evaluation. | ||
| 3224 | # The first argument is usually a number (with optional leading and trailing | ||
| 3225 | # whitespace). If it is a non-numeric string, it is treated as 0 for | ||
| 3226 | # all-whitespace, and as 1 otherwise. Value 0 selects the very next (second) | ||
| 3227 | # argument, value 1 selects the one after it, etc. If the value is greater than | ||
| 3228 | # the number of available arguments, the last one (unless it is the only one) | ||
| 3229 | # is selected. If there is only one (the first) alternative available but the | ||
| 3230 | # value is greater than 0, an empty string is returned. | ||
| 3231 | # Examples: | ||
| 3232 | # [? 2 | zero | one | two | three ] -> two | ||
| 3233 | # [? foo | none | any | two | three ] -> any | ||
| 3234 | # [? 24 | 0 | one | many ] -> many | ||
| 3235 | # [? 2 |No recipients] -> (empty string) | ||
| 3236 | # [? %#R |No recipients|One recipient|%#R recipients] | ||
| 3237 | # [? %q |No quarantine|Quarantined as %q] | ||
| 3238 | # Note that a selector macro call can be considered a form of if-then-else, | ||
| 3239 | # except that the 'then' and 'else' parts are swapped! | ||
| 3240 | # | ||
| 3241 | # ITERATOR in its full form takes three arguments (and ignores any extra | ||
| 3242 | # arguments after that): | ||
| 3243 | # [ %x | body-usually-containing-%x | separator ] | ||
| 3244 | # All iterator's arguments are implicitly quoted, iterator performs its own | ||
| 3245 | # substitutions on provided arguments, as described below. The result of an | ||
| 3246 | # iterator call is a body (the second argument) repeated as many times as | ||
| 3247 | # there are elements in the array denoted by the first argument. In each | ||
| 3248 | # instance of a body all occurrences of a token %x in the body are replaced | ||
| 3249 | # with each consecutive element of the array. Resulting body instances are | ||
| 3250 | # then glued together with a string given as the third argument. The result | ||
| 3251 | # is finally evaluated as any top-level text for possible further expansion. | ||
| 3252 | # | ||
| 3253 | # There are two simplified forms of iterator call: | ||
| 3254 | # [ body | separator ] | ||
| 3255 | # or [ body ] | ||
| 3256 | # where missing separator is considered a null string, and a missing formal | ||
| 3257 | # argument name is obtained by looking for the first token of the form %x | ||
| 3258 | # in the body. If there is no formal argument specified (neither explicitly | ||
| 3259 | # nor in the body), the result is an empty string, which is potentially useful | ||
| 3260 | # as a null lexical separator. | ||
| 3261 | # | ||
| 3262 | # Examples: | ||
| 3263 | # [%V| ] a space-separated list of virus names | ||
| 3264 | # | ||
| 3265 | # [%V|\n] a newline-separated list of virus names | ||
| 3266 | # | ||
| 3267 | # [%V| | ||
| 3268 | # ] same thing: a newline-separated list of virus names | ||
| 3269 | # | ||
| 3270 | # [ | ||
| 3271 | # %V] a list of virus names, each preceeded by NL and spaces | ||
| 3272 | # | ||
| 3273 | # [ %R |%s --> <%R>|, ] a comma-space separated list of sender/recipient | ||
| 3274 | # name pairs where recipient is iterated over the list | ||
| 3275 | # of recipients. (Only the (first) token %x in the first | ||
| 3276 | # argument is significant, other characters are ignored.) | ||
| 3277 | # | ||
| 3278 | # [%V|[%R|%R + %V|, ]|; ] produce all combinations of %R + %V elements | ||
| 3279 | # | ||
| 3280 | # A combined example: | ||
| 3281 | # [? %#C |#|Cc: [<%C>|, ]] | ||
| 3282 | # [? %#C ||Cc: [<%C>|, ]\n]# ... same thing | ||
| 3283 | # evaluates to an empty string if there are no elements in the %C array, | ||
| 3284 | # otherwise it evaluates to a line: Cc: <addr1>, <addr2>, ...\n | ||
| 3285 | # The '#' removes input characters until and including newline after it. | ||
| 3286 | # It can be used for clarity to allow newlines be placed in the source text | ||
| 3287 | # but not resulting in empty lines in the expanded text. In the second example | ||
| 3288 | # above, a backslash at the end of the line would achieve the same result, | ||
| 3289 | # although the method is different: \NEWLINE is removed during initial lexical | ||
| 3290 | # analysis, while # is an internal macro which, when called, actively discards | ||
| 3291 | # tokens following it, until NEWLINE (or end of input) is encountered. | ||
| 3292 | # Whitespace (including newlines) around the first argument %#C of selector | ||
| 3293 | # call is ignored and can be used for clarity. | ||
| 3294 | # | ||
| 3295 | # These all produce the same result: | ||
| 3296 | # To: [%T|%T|, ] | ||
| 3297 | # To: [%T|, ] | ||
| 3298 | # To: %T | ||
| 3299 | # | ||
| 3300 | # See further practical examples in the supplied notification messages; | ||
| 3301 | # see also README.customize file. | ||
| 3302 | # | ||
| 3303 | # Author: Mark Martinec <Mark.Martinec@ijs.si>, 2002 | ||
| 3304 | # | ||
| 3305 | sub expand($$) { | ||
| 3306 | my($str_ref) = shift; # a ref to a source string to be macro expanded; | ||
| 3307 | my($builtins_href) = shift; # a hashref, mapping builtin macro names (single | ||
| 3308 | # char) to macro values: strings or array refs | ||
| 3309 | my($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) = | ||
| 3310 | \('[', '[?', ']', '|', '#'); # lexical elements to be used as references | ||
| 3311 | my(%lexmap); # maps string to reference in order to protect lexels | ||
| 3312 | for (keys(%$builtins_href)) | ||
| 3313 | { $lexmap{"%$_"} = \"%$_"; $lexmap{"%#$_"} = \"%#$_" } | ||
| 3314 | for ($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) { $lexmap{$$_} = $_ } | ||
| 3315 | # parse lexically | ||
| 3316 | my(@tokens) = $$str_ref =~ /\G \# | \[\?? | [\]|] | % \#? . | \\ [^0-7] | | ||
| 3317 | \\ [0-7]{1,3} | [^\[\]\\|%\n#]+ | [^\n]+? | \n /gcsx; | ||
| 3318 | # replace lexical element strings with object references, | ||
| 3319 | # unquote backslash-quoted characters and %%, and drop backslash-newlines | ||
| 3320 | my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b", | ||
| 3321 | e => "\e", a => "\a", t => "\t"); | ||
| 3322 | for (@tokens) { | ||
| 3323 | if (exists $lexmap{$_}) { $_ = $lexmap{$_} } # replace with refs | ||
| 3324 | elsif ($_ eq "\\\n") { $_ = '' } # drop \NEWLINE | ||
| 3325 | elsif (/^%(%)\z/) { $_ = $1 } # %% -> % | ||
| 3326 | elsif (/^(%#?.)\z/s) { $_ = \$1 } # unknown builtins | ||
| 3327 | elsif (/^\\([0-7]{1,3})\z/) { $_ = chr(oct($1)) } # \nnn | ||
| 3328 | elsif (/^\\(.)\z/s) { $_ = (exists($esc{$1}) ? $esc{$1} : $1) } | ||
| 3329 | } | ||
| 3330 | my($call_level) = 0; my($quote_level) = 0; my(@macro_type, @arg); | ||
| 3331 | my(%builtins_cached); my($output_str) = ''; my($whereto) = \$output_str; | ||
| 3332 | while (@tokens) { | ||
| 3333 | my($t) = shift(@tokens); | ||
| 3334 | if ($t eq '') { # ignore leftovers | ||
| 3335 | } elsif ($quote_level>0 && ref($t) && ($t == $lex_lbr || $t == $lex_lbrq)){ | ||
| 3336 | $quote_level++; | ||
| 3337 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t); | ||
| 3338 | } elsif (ref($t) && $t == $lex_lbr) { # begin iterator macro call | ||
| 3339 | $quote_level++; $call_level++; | ||
| 3340 | unshift(@arg, [[]]); unshift(@macro_type, ''); $whereto = $arg[0][0]; | ||
| 3341 | } elsif (ref($t) && $t == $lex_lbrq) { # begin selector macro call | ||
| 3342 | $call_level++; unshift(@arg, [[]]); unshift(@macro_type, ''); | ||
| 3343 | $whereto = $arg[0][0]; $macro_type[0] = 'select'; | ||
| 3344 | } elsif ($quote_level > 1 && ref($t) && $t == $lex_rbr) { | ||
| 3345 | $quote_level--; | ||
| 3346 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t); | ||
| 3347 | } elsif ($call_level > 0 && ref($t) && $t == $lex_sep) { # next argument | ||
| 3348 | if ($quote_level == 0 && $macro_type[0] eq 'select' && @{$arg[0]} == 1) { | ||
| 3349 | $quote_level++; | ||
| 3350 | } | ||
| 3351 | if ($quote_level == 1) { | ||
| 3352 | unshift(@{$arg[0]}, []); $whereto = $arg[0][0]; # begin next arg | ||
| 3353 | } else { | ||
| 3354 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t); | ||
| 3355 | } | ||
| 3356 | } elsif ($quote_level > 0 && ref($t) && $t == $lex_rbr) { | ||
| 3357 | $quote_level--; # quote level just dropped to 0, this is now a call | ||
| 3358 | $call_level-- if $call_level > 0; | ||
| 3359 | my(@result); | ||
| 3360 | if ($macro_type[0] eq 'select') { | ||
| 3361 | my($sel, @alternatives) = reverse @{$arg[0]}; # list of refs | ||
| 3362 | # turn reference into a string, avoid warnings about uninitialized val. | ||
| 3363 | $sel = !ref($sel) ? '' : join('', map {defined $_ ? $_ : ''} @$sel); | ||
| 3364 | if ($sel =~ /^\s*\z/) { $sel = 0 } | ||
| 3365 | elsif ($sel =~ /^\s*(\d+)\s*\z/) { $sel = 0+$1 } # make numeric | ||
| 3366 | else { $sel = 1 } | ||
| 3367 | # provide an empty second alternative if we only have one specified | ||
| 3368 | push(@alternatives, []) if @alternatives < 2 && $sel > 0; | ||
| 3369 | if ($sel < 0) { $sel = 0 } | ||
| 3370 | elsif ($sel > $#alternatives) { $sel = $#alternatives } | ||
| 3371 | @result = @{$alternatives[$sel]}; | ||
| 3372 | } else { # iterator | ||
| 3373 | my($cvar_r, $sep_r, $body_r, $cvar); # give meaning to arguments | ||
| 3374 | if (@{$arg[0]} >= 3) { ($cvar_r,$body_r,$sep_r) = reverse @{$arg[0]} } | ||
| 3375 | else { ($body_r, $sep_r) = reverse @{$arg[0]}; $cvar_r = $body_r } | ||
| 3376 | # find the formal argument name (iterator) | ||
| 3377 | for (@$cvar_r) { | ||
| 3378 | if (ref && $$_ =~ /^%(.)\z/s) { $cvar = $1; last } | ||
| 3379 | } | ||
| 3380 | if (exists($builtins_href->{$cvar})) { | ||
| 3381 | my($values_r); | ||
| 3382 | if (exists($builtins_cached{$cvar})) { | ||
| 3383 | $values_r = $builtins_cached{$cvar}; | ||
| 3384 | } else { | ||
| 3385 | $values_r = $builtins_href->{$cvar}; | ||
| 3386 | while (ref($values_r) eq 'CODE') { $values_r = &$values_r } | ||
| 3387 | $builtins_cached{$cvar} = $values_r; | ||
| 3388 | } | ||
| 3389 | $values_r = [$values_r] if !ref($values_r); | ||
| 3390 | my($ind); | ||
| 3391 | my($re) = qr/^%\Q$cvar\E\z/; | ||
| 3392 | for my $val (@$values_r) { | ||
| 3393 | push(@result, @$sep_r) if ++$ind > 1 && ref($sep_r); | ||
| 3394 | push(@result, map { (ref && $$_ =~ /$re/) ? $val : $_ } @$body_r); | ||
| 3395 | } | ||
| 3396 | } | ||
| 3397 | } | ||
| 3398 | shift(@macro_type); # pop the call stack | ||
| 3399 | shift(@arg); | ||
| 3400 | $whereto = $call_level > 0 ? $arg[0][0] : \$output_str; | ||
| 3401 | unshift(@tokens, @result); # active macro call, evaluate result | ||
| 3402 | } else { # quoted, plain string, simple macro call, or a misplaced token | ||
| 3403 | my($s) = ''; | ||
| 3404 | if ($quote_level > 0 || !ref($t)) { | ||
| 3405 | $s = $t; # quoted or string | ||
| 3406 | } elsif ($t == $lex_h) { # discard tokens to (and including) newline | ||
| 3407 | while (@tokens) { last if shift(@tokens) eq "\n" } | ||
| 3408 | } elsif ($$t =~ /^%(\#)?(.)\z/s) { # macro call %#x or %x | ||
| 3409 | my($num,$m) = ($1,$2); | ||
| 3410 | if (!exists($builtins_href->{$m})) { $s = '' } # no such | ||
| 3411 | elsif (exists($builtins_cached{$m})) { $s = $builtins_cached{$m} } | ||
| 3412 | else { | ||
| 3413 | $s = $builtins_href->{$m}; | ||
| 3414 | while (ref($s) eq 'CODE') { $s = &$s } # subroutine callback | ||
| 3415 | $builtins_cached{$m} = $s; | ||
| 3416 | } | ||
| 3417 | if (defined $num && $num eq '#') { # macro call form %#x | ||
| 3418 | # for array: number of elements; for scalar: nonwhite=1, other 0 | ||
| 3419 | $s = ref($s) ? @$s : $s !~ /^\s*\z/ ? 1 : 0; | ||
| 3420 | } else { # macro call %x evaluates to the value of macro x | ||
| 3421 | $s = join(', ', @$s) if ref $s; | ||
| 3422 | } | ||
| 3423 | } else { $s = $$t } # misplaced token, e.g. a top level | or ] | ||
| 3424 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $s) : ($$whereto .= $s); | ||
| 3425 | } | ||
| 3426 | } | ||
| 3427 | \$output_str; | ||
| 3428 | } | ||
| 3429 | |||
| 3430 | 1; | ||
| 3431 | |||
| 3432 | # | ||
| 3433 | package Amavis::IO::Zlib; | ||
| 3434 | |||
| 3435 | # A simple IO::File -compatible wrapper around Compress::Zlib, | ||
| 3436 | # much like IO::Zlib but simpler: does only what we need and does it carefully | ||
| 3437 | |||
| 3438 | use strict; | ||
| 3439 | use re 'taint'; | ||
| 3440 | |||
| 3441 | BEGIN { | ||
| 3442 | use Exporter (); | ||
| 3443 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 3444 | $VERSION = '2.043'; | ||
| 3445 | @ISA = qw(Exporter); | ||
| 3446 | } | ||
| 3447 | use Errno qw(EIO); | ||
| 3448 | use Compress::Zlib; | ||
| 3449 | |||
| 3450 | sub new { | ||
| 3451 | my($class) = shift; my($self) = bless {}, $class; | ||
| 3452 | if (@_) { $self->open(@_) or return undef } | ||
| 3453 | $self; | ||
| 3454 | } | ||
| 3455 | |||
| 3456 | sub close { | ||
| 3457 | my($self) = shift; | ||
| 3458 | my($status); eval { $status = $self->{fh}->gzclose }; delete $self->{fh}; | ||
| 3459 | if ($status != Z_OK || $@ ne '') { | ||
| 3460 | die "gzclose error: $gzerrno"; # can't stash arbitrary text into $! | ||
| 3461 | $! = EIO; return undef; # not reached | ||
| 3462 | } | ||
| 3463 | 1; | ||
| 3464 | } | ||
| 3465 | |||
| 3466 | sub DESTROY { | ||
| 3467 | my($self) = shift; | ||
| 3468 | if (ref $self && $self->{fh}) { eval { $self->close } } | ||
| 3469 | } | ||
| 3470 | |||
| 3471 | sub open { | ||
| 3472 | my($self,$fname,$mode) = @_; | ||
| 3473 | delete $self->{fh}; | ||
| 3474 | $self->{fname} = $fname; $self->{mode} = $mode; $self->{pos} = 0; | ||
| 3475 | my($gz) = gzopen($fname,$mode); | ||
| 3476 | if ($gz) { $self->{fh} = $gz } | ||
| 3477 | else { | ||
| 3478 | die "gzopen error: $gzerrno"; # can't stash arbitrary text into $! | ||
| 3479 | $! = EIO; undef $gz; # not reached | ||
| 3480 | } | ||
| 3481 | $gz; | ||
| 3482 | } | ||
| 3483 | |||
| 3484 | sub seek { | ||
| 3485 | my($self,$pos,$whence) = @_; | ||
| 3486 | $whence==0 && $pos==0 | ||
| 3487 | or die "Seek to $whence,$pos on gzipped file not supported"; | ||
| 3488 | $self->{mode} eq 'rb' | ||
| 3489 | or die "Seek to $whence,$pos on gzipped file only supported for 'rb' mode"; | ||
| 3490 | if ($self->{pos}==0) { 1 } # already there | ||
| 3491 | else { $self->close; $self->open($self->{fname},$self->{mode}) } | ||
| 3492 | } | ||
| 3493 | |||
| 3494 | sub read { # SCALAR,LENGTH,OFFSET | ||
| 3495 | my($self) = shift; $self->{pos} = 1; | ||
| 3496 | !defined($_[2]) || $_[2]==0 | ||
| 3497 | or die "Reading gzipped file to an offset not supported"; | ||
| 3498 | my($nbytes) = $self->{fh}->gzread($_[0], defined $_[1] ? $_[1] : 4096); | ||
| 3499 | if ($nbytes < 0) { | ||
| 3500 | die "gzread error: $gzerrno"; # can't stash arbitrary text into $! | ||
| 3501 | $! = EIO; undef $nbytes; # not reached | ||
| 3502 | } | ||
| 3503 | $nbytes; # eof: 0; error: undef | ||
| 3504 | } | ||
| 3505 | |||
| 3506 | sub getline { | ||
| 3507 | my($self) = shift; $self->{pos} = 1; my($nbytes,$line); | ||
| 3508 | $nbytes = $self->{fh}->gzreadline($line); | ||
| 3509 | if ($nbytes <= 0) { # eof (0) or error (-1) | ||
| 3510 | $! = 0; undef $line; | ||
| 3511 | if ($nbytes < 0 && $gzerrno != Z_STREAM_END) { | ||
| 3512 | die "gzreadline error: $gzerrno"; # can't stash arbitrary text into $! | ||
| 3513 | $! = EIO; # not reached | ||
| 3514 | } | ||
| 3515 | } | ||
| 3516 | $line; # eof: undef, $! zero; error: undef, $! nonzero | ||
| 3517 | } | ||
| 3518 | |||
| 3519 | sub print { | ||
| 3520 | my($self) = shift; | ||
| 3521 | my($nbytes); my($len) = length($_[0]); | ||
| 3522 | if ($len <= 0) { $nbytes = "0 but true" } | ||
| 3523 | else { | ||
| 3524 | $self->{pos} = 1; $nbytes = $self->{fh}->gzwrite($_[0]); | ||
| 3525 | if ($nbytes <= 0) { | ||
| 3526 | die "gzwrite error: $gzerrno"; # can't stash arbitrary text into $! | ||
| 3527 | $! = EIO; undef $nbytes; # not reached | ||
| 3528 | } | ||
| 3529 | } | ||
| 3530 | $nbytes; | ||
| 3531 | } | ||
| 3532 | |||
| 3533 | sub printf { shift->print(sprintf(shift,@_)) } | ||
| 3534 | |||
| 3535 | 1; | ||
| 3536 | |||
| 3537 | # | ||
| 3538 | package Amavis::In::Connection; | ||
| 3539 | |||
| 3540 | # Keeps relevant information about how we received the message: | ||
| 3541 | # client connection information, SMTP envelope and SMTP parameters | ||
| 3542 | |||
| 3543 | use strict; | ||
| 3544 | use re 'taint'; | ||
| 3545 | |||
| 3546 | BEGIN { | ||
| 3547 | use Exporter (); | ||
| 3548 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 3549 | $VERSION = '2.043'; | ||
| 3550 | @ISA = qw(Exporter); | ||
| 3551 | } | ||
| 3552 | |||
| 3553 | sub new | ||
| 3554 | { my($class) = @_; bless {}, $class } | ||
| 3555 | sub client_ip # client IP address (immediate SMTP client, i.e. our MTA) | ||
| 3556 | { my($self)=shift; !@_ ? $self->{client_ip} : ($self->{client_ip}=shift) } | ||
| 3557 | sub socket_ip # IP address of our interface that received connection | ||
| 3558 | { my($self)=shift; !@_ ? $self->{socket_ip} : ($self->{socket_ip}=shift) } | ||
| 3559 | sub socket_port # TCP port of our interface that received connection | ||
| 3560 | { my($self)=shift; !@_ ? $self->{socket_port}:($self->{socket_port}=shift) } | ||
| 3561 | sub proto # TCP/UNIX | ||
| 3562 | { my($self)=shift; !@_ ? $self->{proto} : ($self->{proto}=shift) } | ||
| 3563 | sub smtp_proto # SMTP/ESMTP(A|S|SA)/LMTP(A|S|SA) # rfc3848, or QMQP/QMQPqq | ||
| 3564 | { my($self)=shift; !@_ ? $self->{smtp_proto}: ($self->{smtp_proto}=shift) } | ||
| 3565 | sub smtp_helo # (E)SMTP HELO/EHLO parameter | ||
| 3566 | { my($self)=shift; !@_ ? $self->{smtp_helo} : ($self->{smtp_helo}=shift) } | ||
| 3567 | |||
| 3568 | 1; | ||
| 3569 | |||
| 3570 | # | ||
| 3571 | package Amavis::In::Message::PerRecip; | ||
| 3572 | |||
| 3573 | use strict; | ||
| 3574 | use re 'taint'; | ||
| 3575 | |||
| 3576 | BEGIN { | ||
| 3577 | use Exporter (); | ||
| 3578 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 3579 | $VERSION = '2.043'; | ||
| 3580 | @ISA = qw(Exporter); | ||
| 3581 | } | ||
| 3582 | |||
| 3583 | # per-recipient data are kept in an array of n-tuples: | ||
| 3584 | # (recipient-address, destiny, done, smtp-response-text, remote-mta, ...) | ||
| 3585 | sub new # NOTE: this class is a list for historical reasons, not a hash | ||
| 3586 | { my($class) = @_; bless [(undef) x 15], $class } | ||
| 3587 | |||
| 3588 | # subs to set or access individual elements of a n-tuple by name | ||
| 3589 | sub recip_addr # raw (unquoted) recipient envelope e-mail address | ||
| 3590 | { my($self)=shift; !@_ ? $$self[0] : ($$self[0]=shift) } | ||
| 3591 | sub recip_addr_modified # recip. addr. with possible addr. extension inserted | ||
| 3592 | { my($self)=shift; !@_ ? $$self[1] : ($$self[1]=shift) } | ||
| 3593 | sub recip_destiny # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS | ||
| 3594 | { my($self)=shift; !@_ ? $$self[2] : ($$self[2]=shift) } | ||
| 3595 | sub recip_done # false: not done, true: done (1: faked, 2: truly sent) | ||
| 3596 | { my($self)=shift; !@_ ? $$self[3] : ($$self[3]=shift) } | ||
| 3597 | sub recip_smtp_response # rfc2821 response (3-digit + enhanced resp + text) | ||
| 3598 | { my($self)=shift; !@_ ? $$self[4] : ($$self[4]=shift) } | ||
| 3599 | sub recip_remote_mta_smtp_response # smtp response as issued by remote MTA | ||
| 3600 | { my($self)=shift; !@_ ? $$self[5] : ($$self[5]=shift) } | ||
| 3601 | sub recip_remote_mta # remote MTA that issued the smtp response | ||
| 3602 | { my($self)=shift; !@_ ? $$self[6] : ($$self[6]=shift) } | ||
| 3603 | sub recip_mbxname # mailbox name or file when known (local:, bsmtp: or sql:) | ||
| 3604 | { my($self)=shift; !@_ ? $$self[7] : ($$self[7]=shift) } | ||
| 3605 | sub recip_whitelisted_sender # recip considers this sender whitelisted (> 0) | ||
| 3606 | { my($self)=shift; !@_ ? $$self[8] : ($$self[8]=shift) } | ||
| 3607 | sub recip_blacklisted_sender # recip considers this sender blacklisted | ||
| 3608 | { my($self)=shift; !@_ ? $$self[9] : ($$self[9]=shift) } | ||
| 3609 | sub recip_score_boost # recip adds penalty spam points to the final score | ||
| 3610 | { my($self)=shift; !@_ ? $$self[10] : ($$self[10]=shift) } | ||
| 3611 | sub infected # contains a virus (1); check bypassed (undef); clean (0) | ||
| 3612 | { my($self)=shift; !@_ ? $$self[11] : ($$self[11]=shift) } | ||
| 3613 | sub banned_parts # banned part descriptions (ref to a list of banned parts) | ||
| 3614 | { my($self)=shift; !@_ ? $$self[12] : ($$self[12]=shift) } | ||
| 3615 | sub banned_keys # keys of matching banned rules (a ref to a list) | ||
| 3616 | { my($self)=shift; !@_ ? $$self[13] : ($$self[13]=shift) } | ||
| 3617 | sub banned_rhs # right-hand side of matching rules (a ref to a list) | ||
| 3618 | { my($self)=shift; !@_ ? $$self[14] : ($$self[14]=shift) } | ||
| 3619 | |||
| 3620 | sub recip_final_addr { # return recip_addr_modified if set, else recip_addr | ||
| 3621 | my($self)=shift; | ||
| 3622 | my($newaddr) = $self->recip_addr_modified; | ||
| 3623 | defined $newaddr ? $newaddr : $self->recip_addr; | ||
| 3624 | } | ||
| 3625 | |||
| 3626 | 1; | ||
| 3627 | |||
| 3628 | # | ||
| 3629 | package Amavis::In::Message; | ||
| 3630 | # this class contains information about the message being processed | ||
| 3631 | |||
| 3632 | use strict; | ||
| 3633 | use re 'taint'; | ||
| 3634 | |||
| 3635 | BEGIN { | ||
| 3636 | use Exporter (); | ||
| 3637 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 3638 | $VERSION = '2.043'; | ||
| 3639 | @ISA = qw(Exporter); | ||
| 3640 | } | ||
| 3641 | |||
| 3642 | BEGIN { | ||
| 3643 | import Amavis::Conf qw(:platform); | ||
| 3644 | import Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp); | ||
| 3645 | import Amavis::In::Message::PerRecip; | ||
| 3646 | } | ||
| 3647 | |||
| 3648 | sub new | ||
| 3649 | { my($class) = @_; bless {}, $class } | ||
| 3650 | sub rx_time # Unix time (s since epoch) of message reception by amavisd | ||
| 3651 | { my($self)=shift; !@_ ? $self->{rx_time} : ($self->{rx_time}=shift) } | ||
| 3652 | sub client_addr # original client IP addr, obtained from XFORWARD or milter | ||
| 3653 | { my($self)=shift; !@_ ? $self->{cli_ip} : ($self->{cli_ip}=shift) } | ||
| 3654 | sub client_name # orig. client DNS name, obtained from XFORWARD or milter | ||
| 3655 | { my($self)=shift; !@_ ? $self->{cli_name} : ($self->{cli_name}=shift) } | ||
| 3656 | sub client_proto # orig. client protocol, obtained from XFORWARD or milter | ||
| 3657 | { my($self)=shift; !@_ ? $self->{cli_proto} : ($self->{cli_proto}=shift) } | ||
| 3658 | sub client_helo # orig. client EHLO name, obtained from XFORWARD or milter | ||
| 3659 | { my($self)=shift; !@_ ? $self->{cli_helo} : ($self->{cli_helo}=shift) } | ||
| 3660 | sub queue_id # MTA queue ID of message if known (Courier, milter/AM.PDP) | ||
| 3661 | { my($self)=shift; !@_ ? $self->{queue_id} : ($self->{queue_id}=shift) } | ||
| 3662 | sub mail_id # some long-term unique id of the message on this system | ||
| 3663 | { my($self)=shift; !@_ ? $self->{mail_id} : ($self->{mail_id}=shift) } | ||
| 3664 | sub secret_id # secret string to grant access to message with mail_id | ||
| 3665 | { my($self)=shift; !@_ ? $self->{secret_id} : ($self->{secret_id}=shift) } | ||
| 3666 | sub msg_size # ESMTP SIZE value, later corrected by actual message size | ||
| 3667 | { my($self)=shift; !@_ ? $self->{msg_size} : ($self->{msg_size}=shift) } | ||
| 3668 | sub auth_user # ESMTP AUTH username | ||
| 3669 | { my($self)=shift; !@_ ? $self->{auth_user} : ($self->{auth_user}=shift) } | ||
| 3670 | sub auth_pass # ESMTP AUTH password | ||
| 3671 | { my($self)=shift; !@_ ? $self->{auth_pass} : ($self->{auth_pass}=shift) } | ||
| 3672 | sub auth_submitter # ESMTP MAIL command AUTH option value (addr-spec or "<>") | ||
| 3673 | { my($self)=shift; !@_ ? $self->{auth_subm} : ($self->{auth_subm}=shift) } | ||
| 3674 | sub requested_by # Resent-From addr who requested release from a quarantine | ||
| 3675 | { my($self)=shift; !@_ ? $self->{requested_by}:($self->{requested_by}=shift)} | ||
| 3676 | sub body_type # ESMTP BODY param (rfc1652: 7BIT, 8BITMIME) or BINARYMIME | ||
| 3677 | { my($self)=shift; !@_ ? $self->{body_type} : ($self->{body_type}=shift) } | ||
| 3678 | sub sender # envelope sender | ||
| 3679 | { my($self)=shift; !@_ ? $self->{sender} : ($self->{sender}=shift) } | ||
| 3680 | sub sender_contact # unmangled sender address or undef (e.g. believed faked) | ||
| 3681 | { my($self)=shift; !@_ ? $self->{sender_c} : ($self->{sender_c}=shift) } | ||
| 3682 | sub sender_source # unmangled sender address or info from the trace | ||
| 3683 | { my($self)=shift; !@_ ? $self->{sender_src} : ($self->{sender_src}=shift) } | ||
| 3684 | sub mime_entity # MIME::Parser entity holding the message | ||
| 3685 | { my($self)=shift; !@_ ? $self->{mime_entity}: ($self->{mime_entity}=shift)} | ||
| 3686 | sub parts_root # Amavis::Unpackers::Part root object | ||
| 3687 | { my($self)=shift; !@_ ? $self->{parts_root}: ($self->{parts_root}=shift)} | ||
| 3688 | sub mail_text # rfc2822 msg: (open) file handle, or MIME::Entity object | ||
| 3689 | { my($self)=shift; !@_ ? $self->{mail_text} : ($self->{mail_text}=shift) } | ||
| 3690 | sub mail_text_fn # orig. mail filename or undef, e.g. mail_tempdir/email.txt | ||
| 3691 | { my($self)=shift; !@_ ? $self->{mail_text_fn} : ($self->{mail_text_fn}=shift) } | ||
| 3692 | sub mail_tempdir # work directory, under $TEMPBASE or supplied by client | ||
| 3693 | { my($self)=shift; !@_ ? $self->{mail_tempdir} : ($self->{mail_tempdir}=shift) } | ||
| 3694 | sub header_edits # Amavis::Out::EditHeader object or undef | ||
| 3695 | { my($self)=shift; !@_ ? $self->{hdr_edits} : ($self->{hdr_edits}=shift) } | ||
| 3696 | sub orig_header # original header - an arrayref of lines, with trailing LF | ||
| 3697 | { my($self)=shift; !@_ ? $self->{orig_header}: ($self->{orig_header}=shift) } | ||
| 3698 | sub orig_header_size # size of original header | ||
| 3699 | { my($self)=shift; !@_ ? $self->{orig_hdr_s} : ($self->{orig_hdr_s}=shift) } | ||
| 3700 | sub orig_body_size # size of original body | ||
| 3701 | { my($self)=shift; !@_ ? $self->{orig_bdy_s} : ($self->{orig_bdy_s}=shift) } | ||
| 3702 | sub body_digest # message digest of a message body (e.g. MD5 or SHA1) | ||
| 3703 | { my($self)=shift; !@_ ? $self->{body_digest}: ($self->{body_digest}=shift) } | ||
| 3704 | sub quarantined_to # list of quarantine mailbox names or addresses if quarantined | ||
| 3705 | { my($self)=shift; !@_ ? $self->{quarantine} : ($self->{quarantine}=shift) } | ||
| 3706 | sub quar_type # quarantine type: F/Z/B/Q/M (file/zipfile/bsmtp/sql/mailbox) | ||
| 3707 | { my($self)=shift; !@_ ? $self->{quar_type} : ($self->{quar_type}=shift) } | ||
| 3708 | sub dsn_sent # delivery status notification was sent(1) or faked(2) | ||
| 3709 | { my($self)=shift; !@_ ? $self->{dsn_sent} : ($self->{dsn_sent}=shift) } | ||
| 3710 | sub delivery_method # delivery method, or empty for implicit delivery (milter) | ||
| 3711 | { my($self)=shift; !@_ ? $self->{delivery_method}:($self->{delivery_method}=shift)} | ||
| 3712 | sub client_delete # don't delete the tempdir, it is a client's reponsibility | ||
| 3713 | { my($self)=shift; !@_ ? $self->{client_delete}:($self->{client_delete}=shift)} | ||
| 3714 | # credativ -jw | ||
| 3715 | sub postfixid # the original postfix queue id | ||
| 3716 | { my($self)=shift; !@_ ? $self->{postfixid} : ($self->{postfixid}=shift) } | ||
| 3717 | # credativ end | ||
| 3718 | |||
| 3719 | # The order of entries in the list is the original order in which | ||
| 3720 | # recipient addresses (e.g. obtained via 'MAIL TO:') were received. | ||
| 3721 | # Only the entries that were accepted (via SMTP response code 2xx) | ||
| 3722 | # are placed in the list. The ORDER MUST BE PRESERVED and no recipients | ||
| 3723 | # may be added or removed from the list! This is vital to be able | ||
| 3724 | # to produce correct per-recipient responses to a LMTP client! | ||
| 3725 | # | ||
| 3726 | sub per_recip_data { # get or set a listref of envelope recipient n-tuples | ||
| 3727 | my($self) = shift; | ||
| 3728 | # store a given listref of n-tuples (originals, not copies!) | ||
| 3729 | if (@_) { @{$self->{recips}} = @{$_[0]} } | ||
| 3730 | # return a listref to the original n-tuples, | ||
| 3731 | # caller may modify the data if he knows what he is doing | ||
| 3732 | $self->{recips}; | ||
| 3733 | } | ||
| 3734 | |||
| 3735 | sub recips { # get or set a listref of envelope recipients | ||
| 3736 | my($self)=shift; | ||
| 3737 | if (@_) { # store a copy of a given listref of recipient addresses | ||
| 3738 | # wrap scalars (strings) into n-tuples | ||
| 3739 | $self->per_recip_data([ map { | ||
| 3740 | my($per_recip_obj) = Amavis::In::Message::PerRecip->new; | ||
| 3741 | $per_recip_obj->recip_addr($_); | ||
| 3742 | $per_recip_obj->recip_destiny(D_PASS); # default is Pass | ||
| 3743 | $per_recip_obj } @{$_[0]} ]); | ||
| 3744 | } | ||
| 3745 | return if !defined wantarray; # don't bother | ||
| 3746 | # return listref of recipient addresses | ||
| 3747 | [ map { $_->recip_addr } @{$self->per_recip_data} ]; | ||
| 3748 | } | ||
| 3749 | |||
| 3750 | 1; | ||
| 3751 | |||
| 3752 | # | ||
| 3753 | package Amavis::Out::EditHeader; | ||
| 3754 | |||
| 3755 | # Accumulates instructions on what lines need to be added to the message | ||
| 3756 | # header, deleted, or how to change existing lines, then via a call | ||
| 3757 | # to write_header() performs these edits on the fly. | ||
| 3758 | |||
| 3759 | use strict; | ||
| 3760 | use re 'taint'; | ||
| 3761 | |||
| 3762 | BEGIN { | ||
| 3763 | use Exporter (); | ||
| 3764 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 3765 | $VERSION = '2.043'; | ||
| 3766 | @ISA = qw(Exporter); | ||
| 3767 | @EXPORT_OK = qw(&hdr); | ||
| 3768 | } | ||
| 3769 | |||
| 3770 | BEGIN { | ||
| 3771 | import Amavis::Conf qw(:platform c cr ca); | ||
| 3772 | import Amavis::Timing qw(section_time); | ||
| 3773 | import Amavis::Util qw(ll do_log safe_encode q_encode); | ||
| 3774 | } | ||
| 3775 | use MIME::Words; | ||
| 3776 | |||
| 3777 | sub new { my($class) = @_; bless {}, $class } | ||
| 3778 | |||
| 3779 | sub prepend_header($$$;$) { | ||
| 3780 | my($self, $field_name, $field_body, $structured) = @_; | ||
| 3781 | unshift(@{$self->{prepend}}, hdr($field_name, $field_body, $structured)); | ||
| 3782 | } | ||
| 3783 | |||
| 3784 | sub append_header($$$;$) { | ||
| 3785 | my($self, $field_name, $field_body, $structured) = @_; | ||
| 3786 | push(@{$self->{append}}, hdr($field_name, $field_body, $structured)); | ||
| 3787 | } | ||
| 3788 | |||
| 3789 | sub delete_header($$) { | ||
| 3790 | my($self, $field_name) = @_; | ||
| 3791 | $self->{edit}{lc($field_name)} = undef; | ||
| 3792 | } | ||
| 3793 | |||
| 3794 | sub edit_header($$$;$) { | ||
| 3795 | my($self, $field_name, $field_edit_sub, $structured) = @_; | ||
| 3796 | # $field_edit_sub will be called with 2 args: field name and field body; | ||
| 3797 | # it should return the replacement field body (no field name and colon), | ||
| 3798 | # with or without the trailing NL | ||
| 3799 | !defined($field_edit_sub) || ref($field_edit_sub) eq 'CODE' | ||
| 3800 | or die "edit_header: arg#3 must be undef or a subroutine ref"; | ||
| 3801 | $self->{edit}{lc($field_name)} = $field_edit_sub; | ||
| 3802 | } | ||
| 3803 | |||
| 3804 | # copy all header edits from another header-edits object into this one | ||
| 3805 | sub inherit_header_edits($$) { | ||
| 3806 | my($self, $other_edits) = @_; | ||
| 3807 | if (defined $other_edits) { | ||
| 3808 | unshift(@{$self->{prepend}}, | ||
| 3809 | @{$other_edits->{prepend}}) if $other_edits->{prepend}; | ||
| 3810 | unshift(@{$self->{append}}, | ||
| 3811 | @{$other_edits->{append}}) if $other_edits->{append}; | ||
| 3812 | if ($other_edits->{edit}) { | ||
| 3813 | for (keys %{$other_edits->{edit}}) | ||
| 3814 | { $self->{edit}{$_} = $other_edits->{edit}{$_} } | ||
| 3815 | } | ||
| 3816 | } | ||
| 3817 | } | ||
| 3818 | |||
| 3819 | # Insert space after colon if not present, RFC2047-encode if field body | ||
| 3820 | # contains non-ASCII characters, fold long lines if needed, | ||
| 3821 | # prepend space before each NL if missing, append NL if missing; | ||
| 3822 | # Header fields with only spaces are not allowed. | ||
| 3823 | # (rfc2822: Each line of characters MUST be no more than 998 characters, | ||
| 3824 | # and SHOULD be no more than 78 characters, excluding the CRLF. | ||
| 3825 | # '$structured' indicates that folding is only allowed at positions | ||
| 3826 | # indicated by \n in the provided header body. | ||
| 3827 | # | ||
| 3828 | sub hdr($$;$) { | ||
| 3829 | my($field_name, $field_body, $structured) = @_; | ||
| 3830 | if ($field_name =~ /^(X-.*|Subject|Comments)\z/si && | ||
| 3831 | $field_body =~ /[^\011\012\040-\176]/ #any nonprintable except TAB and LF | ||
| 3832 | ) { # encode according to RFC 2047 | ||
| 3833 | $field_body =~ s/\n([ \t])/$1/g; # unfold | ||
| 3834 | chomp($field_body); | ||
| 3835 | my($field_body_octets) = safe_encode(c('hdr_encoding'), $field_body); | ||
| 3836 | my($qb) = c('hdr_encoding_qb'); | ||
| 3837 | if (uc($qb) eq 'Q') { | ||
| 3838 | $field_body = q_encode($field_body_octets, $qb, c('hdr_encoding')); | ||
| 3839 | } else { | ||
| 3840 | $field_body = MIME::Words::encode_mimeword($field_body_octets, | ||
| 3841 | $qb, c('hdr_encoding')); | ||
| 3842 | } | ||
| 3843 | } else { # supposed to be in plain ASCII, let's make sure it is | ||
| 3844 | $field_body = safe_encode('ascii', $field_body); | ||
| 3845 | } | ||
| 3846 | $field_name = safe_encode('ascii', $field_name); | ||
| 3847 | my($str) = $field_name . ':'; | ||
| 3848 | $str .= ' ' if $field_body !~ /^[ \t]/; | ||
| 3849 | $str .= $field_body; | ||
| 3850 | $str =~ s/\n([^ \t\n])/\n $1/g; # insert a space at line folds if missing | ||
| 3851 | $str =~ s/\n([ \t]*\n)+/\n/g; # remove empty lines | ||
| 3852 | chomp($str); # chop off trailing NL if present | ||
| 3853 | if ($structured) { | ||
| 3854 | $str =~ s/[ \t]+/ /g; # collapse spaces and tabs to a single space | ||
| 3855 | my(@sublines) = split(/\n/, $str, -1); | ||
| 3856 | $str = ''; my($s) = ''; my($s_l) = 0; my($s_il)=0; | ||
| 3857 | for (@sublines) { # join shorter field sections | ||
| 3858 | if ($s !~ /^\s*\z/ && $s_l + $s_il + length($_) > 78) { | ||
| 3859 | $s_il = 8; # length of the initial tab | ||
| 3860 | $str .= "\n\t" if $str ne ''; | ||
| 3861 | $s =~ s/^[ \t]+//g; # remove leading and trailing whitespace | ||
| 3862 | $s =~ s/[ \t]+$//g; | ||
| 3863 | $str .= $s; $s = ''; $s_l = 0; | ||
| 3864 | } | ||
| 3865 | $s .= $_; $s_l += length($_); | ||
| 3866 | } | ||
| 3867 | if ($s !~ /^\s*\z/) { | ||
| 3868 | $str .= "\n\t" if $str ne ''; | ||
| 3869 | $s =~ s/^[ \t]+//g; # remove leading and trailing whitespace | ||
| 3870 | $s =~ s/[ \t]+$//g; | ||
| 3871 | $str .= $s; | ||
| 3872 | } | ||
| 3873 | } elsif (length($str) > 998) { | ||
| 3874 | # truncate the damn thing (to be done better) | ||
| 3875 | $str = substr($str,0,998); | ||
| 3876 | } | ||
| 3877 | $str .= "\n"; # append final NL | ||
| 3878 | do_log(5, "header: $str"); | ||
| 3879 | $str; | ||
| 3880 | } | ||
| 3881 | |||
| 3882 | # Copy mail header to the supplied method (line by line) while adding, | ||
| 3883 | # removing, or changing certain header lines as required, and append | ||
| 3884 | # an empty line (end-of-header). Returns number of original 'Received:' | ||
| 3885 | # header fields to make simple loop detection possible (as required | ||
| 3886 | # by rfc2821 section 6.2). | ||
| 3887 | # | ||
| 3888 | # Assumes input file is properly positioned, leaves it positioned | ||
| 3889 | # at the beginning of the body. | ||
| 3890 | # | ||
| 3891 | sub write_header($$$) { | ||
| 3892 | my($self, $msg, $out_fh) = @_; | ||
| 3893 | my($is_mime) = ref($msg) && $msg->isa('MIME::Entity') ? 1 : 0; | ||
| 3894 | do_log(5,"write_header: $is_mime, $out_fh"); | ||
| 3895 | $out_fh = IO::Wrap::wraphandle($out_fh); # assure an IO::Handle-like obj | ||
| 3896 | my(@header); | ||
| 3897 | if ($is_mime) { | ||
| 3898 | @header = map { /^[ \t]*\n?\z/ ? () # remove empty lines, ensure NL | ||
| 3899 | : (/\n\z/ ? $_ : $_ . "\n") } @{$msg->header}; | ||
| 3900 | } | ||
| 3901 | my($received_cnt) = 0; my($str) = ''; | ||
| 3902 | for (@{$self->{prepend}}) { $str .= $_ } | ||
| 3903 | if ($str ne '') { $out_fh->print($str) or die "sending mail header1: $!" } | ||
| 3904 | if (!defined($msg)) { | ||
| 3905 | # existing header empty | ||
| 3906 | } else { | ||
| 3907 | push(@header, $eol) if $is_mime; # append empty line as end-of-header | ||
| 3908 | local($1,$2); my($curr_head,$next_head); my($illcnt) = 0; undef $!; | ||
| 3909 | while (defined($next_head = $is_mime ? shift @header : $msg->getline)) { | ||
| 3910 | if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head } # folded | ||
| 3911 | else { # new header | ||
| 3912 | if (!defined($curr_head)) { # no previous complete header field | ||
| 3913 | } elsif ($curr_head !~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s) { | ||
| 3914 | # invalid header, but we don't care | ||
| 3915 | $curr_head =~ s{\n [ \t]* (?= \n )}{}gsx and $illcnt++; | ||
| 3916 | $out_fh->print($curr_head) or die "sending mail header4: $!"; | ||
| 3917 | } else { # count, edit, or delete | ||
| 3918 | # obsolete rfc822 syntax allowed whitespace before colon | ||
| 3919 | my($field_name, $field_body) = ($1, $2); | ||
| 3920 | my($field_name_lc) = lc($field_name); | ||
| 3921 | $received_cnt++ if $field_name_lc eq 'received'; | ||
| 3922 | if (!exists($self->{edit}{$field_name_lc})) { # unchanged | ||
| 3923 | # unfold illegal all-whitespace continuation lines | ||
| 3924 | $curr_head =~ s{\n [ \t]* (?= \n )}{}gsx and $illcnt++; | ||
| 3925 | $out_fh->print($curr_head) or die "sending mail header5: $!"; | ||
| 3926 | } else { | ||
| 3927 | my($edit) = $self->{edit}{$field_name_lc}; | ||
| 3928 | if (defined($edit)) { # edit, not delete | ||
| 3929 | chomp($field_body); | ||
| 3930 | ### $field_body =~ s/\n([ \t])/$1/g; # unfold | ||
| 3931 | my($subst) = hdr($field_name, &$edit($field_name,$field_body)); | ||
| 3932 | $subst =~ s{\n [ \t]* (?= \n )}{}gsx and $illcnt++; | ||
| 3933 | $out_fh->print($subst) or die "sending mail header6: $!"; | ||
| 3934 | } | ||
| 3935 | } | ||
| 3936 | } | ||
| 3937 | last if $next_head eq $eol; # end-of-header reached | ||
| 3938 | $curr_head = $next_head; | ||
| 3939 | } | ||
| 3940 | undef $!; | ||
| 3941 | } | ||
| 3942 | defined $next_head || $is_mime || $!==0 | ||
| 3943 | or die "Error reading mail header: $!"; | ||
| 3944 | do_log(0, "INFO: unfolded $illcnt illegal all-whitespace ". | ||
| 3945 | "continuation lines") if $illcnt; | ||
| 3946 | } | ||
| 3947 | $str = ''; | ||
| 3948 | for (@{$self->{append}}) { $str .= $_ } | ||
| 3949 | $str .= $eol; # end of header - separator line | ||
| 3950 | $out_fh->print($str) or die "sending mail header7: $!"; | ||
| 3951 | section_time('write-header'); | ||
| 3952 | $received_cnt; | ||
| 3953 | } | ||
| 3954 | 1; | ||
| 3955 | |||
| 3956 | # | ||
| 3957 | package Amavis::Out::Local; | ||
| 3958 | use strict; | ||
| 3959 | use re 'taint'; | ||
| 3960 | |||
| 3961 | BEGIN { | ||
| 3962 | use Exporter (); | ||
| 3963 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); | ||
| 3964 | $VERSION = '2.043'; | ||
| 3965 | @ISA = qw(Exporter); | ||
| 3966 | @EXPORT_OK = qw(&mail_to_local_mailbox); | ||
| 3967 | } | ||
| 3968 | |||
| 3969 | use Errno qw(ENOENT EACCES); | ||
| 3970 | use IO::File qw(O_CREAT O_EXCL O_WRONLY); | ||
| 3971 | use IO::Wrap; | ||
| 3972 | |||
| 3973 | BEGIN { | ||
| 3974 | import Amavis::Conf qw(:platform $quarantine_subdir_levels c cr ca); | ||
| 3975 | import Amavis::Lock; | ||
| 3976 | import Amavis::Util qw(ll do_log am_id exit_status_str run_command_consumer); | ||
| 3977 | import Amavis::Timing qw(section_time); | ||
| 3978 | import Amavis::rfc2821_2822_Tools; | ||
| 3979 | import Amavis::Out::EditHeader; | ||
| 3980 | } | ||
| 3981 | |||
| 3982 | use subs @EXPORT_OK; | ||
| 3983 | |||
| 3984 | # Deliver to local mailboxes only, ignore the rest: either to directory | ||
| 3985 | # (maildir style), or file (Unix mbox). (normally used as a quarantine method) | ||
| 3986 | # | ||
| 3987 | sub mail_to_local_mailbox(@) { | ||
| 3988 | my($via, $msginfo, $initial_submission, $filter) = @_; | ||
| 3989 | $via =~ /^local:(.*)\z/si or die "Bad local method: $via"; | ||
| 3990 | my($via_arg) = $1; | ||
| 3991 | my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) } | ||
| 3992 | @{$msginfo->per_recip_data}; | ||
| 3993 | return 1 if !@per_recip_data; | ||
| 3994 | my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object | ||
| 3995 | if (defined($msg) && !$msg->isa('MIME::Entity')) { | ||
| 3996 | # at this point, we have no idea what the user gave us... | ||
| 3997 | # a globref? a FileHandle? | ||
| 3998 | $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj | ||
| 3999 | } | ||
| 4000 | my($sender) = $msginfo->sender; | ||
| 4001 | for my $r (@per_recip_data) { | ||
| 4002 | # each recipient gets its own copy; these are not the original recipients | ||
| 4003 | my($recip) = $r->recip_final_addr; | ||
| 4004 | next if $recip eq ''; | ||
| 4005 | my($localpart,$domain) = split_address($recip); | ||
| 4006 | my($smtp_response); | ||
| 4007 | |||
| 4008 | # %local_delivery_aliases emulates aliases map - this would otherwise | ||
| 4009 | # be done by MTA's local delivery agent if we gave the message to MTA. | ||
| 4010 | # This way we keep interface compatible with other mail delivery | ||
| 4011 | # methods. The hash value may be a ref to a pair of fixed strings, | ||
| 4012 | # or a subroutine ref (which must return such pair) to allow delayed | ||
| 4013 | # (lazy) evaluation when some part of the pair is not yet known | ||
| 4014 | # at initialization time. | ||
| 4015 | # If no matching entry is found, the key ($localpart) is treated as | ||
| 4016 | # a mailbox filename if nonempty, or else quarantining is skipped. | ||
| 4017 | |||
| 4018 | my($mbxname, $suggested_filename); | ||
| 4019 | { # a block is used as a 'switch' statement - 'last' will exit from it | ||
| 4020 | my($ldar) = cr('local_delivery_aliases'); # a ref to a hash | ||
| 4021 | my($alias) = $ldar->{$localpart}; | ||
| 4022 | if (ref($alias) eq 'ARRAY') { | ||
| 4023 | ($mbxname, $suggested_filename) = @$alias; | ||
| 4024 | } elsif (ref($alias) eq 'CODE') { # lazy (delayed) evaluation | ||
| 4025 | ($mbxname, $suggested_filename) = &$alias; | ||
| 4026 | } elsif ($alias ne '') { | ||
| 4027 | ($mbxname, $suggested_filename) = ($alias, undef); | ||
| 4028 | } elsif (!exists $ldar->{$localpart}) { | ||
| 4029 | do_log(0, "no key '$localpart' in \%local_delivery_aliases, skip local delivery"); | ||
| 4030 | } | ||
| 4031 | if ($mbxname eq '') { | ||
| 4032 | my($why) = !exists $ldar->{$localpart} ? 1 : $alias eq '' ? 2 : 3; | ||
| 4033 | do_log(2, "skip local delivery($why): <$sender> -> <$recip>"); | ||
| 4034 | $smtp_response = "250 2.6.0 Ok, skip local delivery($why)"; | ||
| 4035 | last; # exit block, not the loop | ||
| 4036 | } | ||
| 4037 | my($ux); # is it a UNIX-style mailbox? | ||
| 4038 | if (!-d $mbxname) { # assume a filename (need not exist yet) | ||
| 4039 | $ux = 1; # $mbxname is a UNIX-style mailbox (one file) | ||
| 4040 | } else { # a directory | ||
| 4041 | $ux = 0; # $mbxname is a directory (amavis/maildir style mailbox) | ||
| 4042 | my($explicitly_suggested_filename) = $suggested_filename ne ''; | ||
| 4043 | if ($suggested_filename eq '') | ||
| 4044 | { $suggested_filename = $via_arg ne '' ? $via_arg : '%m' } | ||
| 4045 | $suggested_filename =~ s{%(.)} | ||
| 4046 | { $1 eq 'b' ? $msginfo->body_digest | ||
| 4047 | : $1 eq 'm' ? $msginfo->mail_id | ||
| 4048 | : $1 eq 'i' ? iso8601_timestamp($msginfo->rx_time,1,'-') | ||
| 4049 | : $1 eq 'n' ? am_id() | ||
| 4050 | : $1 eq '%' ? '%' : '%'.$1 }egs; | ||
| 4051 | $mbxname = "$mbxname/$suggested_filename"; | ||
| 4052 | if ($quarantine_subdir_levels>=1 && !$explicitly_suggested_filename) { | ||
| 4053 | # using a subdirectory structure to disperse quarantine files | ||
| 4054 | local($1,$2); my($subdir) = substr($msginfo->mail_id, 0, 1); | ||
| 4055 | $subdir=~/^[A-Z0-9]\z/i or die "Unexpected first char: $subdir"; | ||
| 4056 | $mbxname =~ m{^ (.*/)? ([^/]+) \z}sx; my($path,$fname) = ($1,$2); | ||
| 4057 | $mbxname = "$path$subdir/$fname"; # resulting full filename | ||
| 4058 | my($errn) = stat("$path$subdir") ? 0 : 0+$!; | ||
| 4059 | if ($errn == ENOENT) { # check/prepare a set of subdirectories | ||
| 4060 | do_log(2, "checking/creating quarantine subdirs under $path"); | ||
| 4061 | for my $d ('A'..'Z','a'..'z','0'..'9') { | ||
| 4062 | $errn = stat("$path$d") ? 0 : 0+$!; | ||
| 4063 | if ($errn == ENOENT) { | ||
| 4064 | mkdir("$path$d", 0750) or die "Can't create dir $path$d: $!"; | ||
| 4065 | } | ||
| 4066 | } | ||
| 4067 | } | ||
| 4068 | } | ||
| 4069 | } | ||
| 4070 | do_log(1, "local delivery: <$sender> -> <$recip>, mbx=$mbxname"); | ||
| 4071 | my($mp,$pos,$pid); | ||
| 4072 | my($errn) = stat($mbxname) ? 0 : 0+$!; | ||
| 4073 | local $SIG{CHLD} = 'DEFAULT'; | ||
| 4074 | local $SIG{PIPE} = 'IGNORE'; # write to broken pipe would throw a signal | ||
| 4075 | eval { # try to open the mailbox file for writing | ||
| 4076 | if (!$ux) { # one mail per file, will create specified file | ||
| 4077 | if ($errn == ENOENT) {} # good, no file, as expected | ||
| 4078 | elsif (!$errn && -f _) | ||
| 4079 | { die "File $mbxname already exists, refuse to overwrite" } | ||
| 4080 | else | ||
| 4081 | { die "File $mbxname exists??? Refuse to overwrite it, $!" } | ||
| 4082 | if ($mbxname =~ /\.gz\z/) { | ||
| 4083 | $mp = Amavis::IO::Zlib->new; | ||
| 4084 | $mp->open($mbxname,'wb') | ||
| 4085 | or die "Can't create gzip file $mbxname: $!"; | ||
| 4086 | } else { | ||
| 4087 | $mp = IO::File->new; | ||
| 4088 | $mp->open($mbxname, O_CREAT|O_EXCL|O_WRONLY, 0640) | ||
| 4089 | or die "Can't create file $mbxname: $!"; | ||
| 4090 | binmode($mp, ":bytes") or die "Can't cancel :utf8 mode: $!" | ||
| 4091 | if $unicode_aware; | ||
| 4092 | } | ||
| 4093 | } else { # append to UNIX-style mailbox | ||
| 4094 | # deliver only to non-executable regular files | ||
| 4095 | if ($errn == ENOENT) { | ||
| 4096 | $mp = IO::File->new; | ||
| 4097 | $mp->open($mbxname, O_CREAT|O_EXCL|O_WRONLY, 0640) | ||
| 4098 | or die "Can't create file $mbxname: $!"; | ||
| 4099 | } elsif (!$errn && !-f _) { | ||
| 4100 | die "Mailbox $mbxname is not a regular file, refuse to deliver"; | ||
| 4101 | } elsif (-x _ || -X _) { | ||
| 4102 | die "Mailbox file $mbxname is executable, refuse to deliver"; | ||
| 4103 | } else { | ||
| 4104 | $mp = IO::File->new; | ||
| 4105 | $mp->open($mbxname,'>>',0640) | ||
| 4106 | or die "Can't append to $mbxname: $!"; | ||
| 4107 | } | ||
| 4108 | binmode($mp, ":bytes") or die "Can't cancel |