1 | #!/usr/bin/perl -T |
---|
2 | |
---|
3 | #------------------------------------------------------------------------------ |
---|
4 | # This is amavisd-new. |
---|
5 | # It is an interface between message transfer agent (MTA) and virus |
---|
6 | # scanners and/or spam scanners, functioning as a mail content filter. |
---|
7 | # |
---|
8 | # It is a performance-enhanced and feature-enriched version of amavisd |
---|
9 | # (which in turn is a daemonized version of AMaViS), initially based |
---|
10 | # on amavisd-snapshot-20020300). |
---|
11 | # |
---|
12 | # All work since amavisd-snapshot-20020300: |
---|
13 | # Copyright (C) 2002,2003,2004 Mark Martinec, All Rights Reserved. |
---|
14 | # with contributions from the amavis-* mailing lists and individuals, |
---|
15 | # as acknowledged in the release notes. |
---|
16 | # |
---|
17 | # This program is free software; you can redistribute it and/or modify |
---|
18 | # it under the terms of the GNU General Public License as published by |
---|
19 | # the Free Software Foundation; either version 2 of the License, or |
---|
20 | # (at your option) any later version. |
---|
21 | # |
---|
22 | # This program is distributed in the hope that it will be useful, |
---|
23 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
24 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
---|
25 | # GNU General Public License for details. |
---|
26 | # |
---|
27 | # You should have received a copy of the GNU General Public License |
---|
28 | # along with this program; if not, write to the Free Software |
---|
29 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
---|
30 | |
---|
31 | # Author: Mark Martinec <mark.martinec@ijs.si> |
---|
32 | # Patches and problem reports are welcome. |
---|
33 | # |
---|
34 | # The latest version of this program is available at: |
---|
35 | # http://www.ijs.si/software/amavisd/ |
---|
36 | #------------------------------------------------------------------------------ |
---|
37 | |
---|
38 | # Here is a boilerplate from the amavisd(-snapshot) version, |
---|
39 | # which is the version that served as a base code for the initial |
---|
40 | # version of amavisd-new. License terms were the same: |
---|
41 | # |
---|
42 | # Author: Chris Mason <cmason@unixzone.com> |
---|
43 | # Current maintainer: Lars Hecking <lhecking@users.sourceforge.net> |
---|
44 | # Based on work by: |
---|
45 | # Mogens Kjaer, Carlsberg Laboratory, <mk@crc.dk> |
---|
46 | # Juergen Quade, Softing GmbH, <quade@softing.com> |
---|
47 | # Christian Bricart <shiva@aachalon.de> |
---|
48 | # Rainer Link <link@foo.fh-furtwangen.de> |
---|
49 | # This script is part of the AMaViS package. For more information see: |
---|
50 | # http://amavis.org/ |
---|
51 | # Copyright (C) 2000 - 2002 the people mentioned above |
---|
52 | # This software is licensed under the GNU General Public License (GPL) |
---|
53 | # See: http://www.gnu.org/copyleft/gpl.html |
---|
54 | #------------------------------------------------------------------------------ |
---|
55 | |
---|
56 | #------------------------------------------------------------------------------ |
---|
57 | #Index of packages in this file |
---|
58 | # Amavis::Boot |
---|
59 | # Amavis::Conf |
---|
60 | # Amavis::Lock |
---|
61 | # Amavis::Log |
---|
62 | # Amavis::Timing |
---|
63 | # Amavis::Util |
---|
64 | # Amavis::rfc2821_2822_Tools |
---|
65 | # Amavis::Lookup::RE |
---|
66 | # Amavis::Lookup::Label |
---|
67 | # Amavis::Lookup |
---|
68 | # Amavis::Expand |
---|
69 | # Amavis::In::Connection |
---|
70 | # Amavis::In::Message::PerRecip |
---|
71 | # Amavis::In::Message |
---|
72 | # Amavis::Out::EditHeader |
---|
73 | # Amavis::Out::Local |
---|
74 | # Amavis::Out |
---|
75 | # Amavis::UnmangleSender |
---|
76 | # Amavis::Unpackers::NewFilename |
---|
77 | # Amavis::Unpackers::Part |
---|
78 | # Amavis::Unpackers::OurFiler |
---|
79 | # Amavis::Unpackers::Validity |
---|
80 | # Amavis::Unpackers::MIME |
---|
81 | # Amavis::Notify |
---|
82 | # Amavis::Cache |
---|
83 | # Amavis |
---|
84 | #optionally compiled-in packages: --------------------------------------------- |
---|
85 | # Amavis::DB::SNMP |
---|
86 | # Amavis::DB |
---|
87 | # Amavis::Cache |
---|
88 | # Amavis::Lookup::SQLfield |
---|
89 | # Amavis::Lookup::SQL |
---|
90 | # Amavis::Lookup::LDAP |
---|
91 | # Amavis::Lookup::LDAPattr |
---|
92 | # Amavis::In::AMCL |
---|
93 | # Amavis::In::SMTP |
---|
94 | # Amavis::In::QMQPqq |
---|
95 | # Amavis::AV |
---|
96 | # Amavis::SpamControl |
---|
97 | # Amavis::Unpackers |
---|
98 | #------------------------------------------------------------------------------ |
---|
99 | |
---|
100 | # |
---|
101 | package Amavis::Boot; |
---|
102 | use strict; |
---|
103 | use re 'taint'; |
---|
104 | |
---|
105 | # Fetch all required modules (or nicely report missing ones), and compile them |
---|
106 | # once-and-for-all at the parent process, so that forked children can inherit |
---|
107 | # and share already compiled code in memory. Children will still need to 'use' |
---|
108 | # modules if they want to inherit from their name space. |
---|
109 | # |
---|
110 | sub fetch_modules($$@) { |
---|
111 | my($reason, $required, @modules) = @_; |
---|
112 | my(@missing); |
---|
113 | for my $m (@modules) { |
---|
114 | local($_) = $m; |
---|
115 | $_ .= /^auto::/ ? '.al' : '.pm' if !/\.(pm|pl|al)\z/; |
---|
116 | s[::][/]g; |
---|
117 | eval { require $_ } or push(@missing, $m); |
---|
118 | } |
---|
119 | die "ERROR: MISSING $reason:\n" . join('', map { " $_\n" } @missing) |
---|
120 | if $required && @missing; |
---|
121 | \@missing; |
---|
122 | } |
---|
123 | |
---|
124 | BEGIN { |
---|
125 | fetch_modules('REQUIRED BASIC MODULES', 1, qw( |
---|
126 | Exporter POSIX Fcntl Socket Errno Carp Time::HiRes |
---|
127 | IO::Handle IO::File IO::Socket IO::Socket::UNIX IO::Socket::INET |
---|
128 | IO::Wrap IO::Stringy Digest::MD5 Unix::Syslog File::Basename File::Copy |
---|
129 | Mail::Field Mail::Address Mail::Header Mail::Internet |
---|
130 | MIME::Base64 MIME::QuotedPrint MIME::Words |
---|
131 | MIME::Head MIME::Body MIME::Entity MIME::Parser MIME::Decoder |
---|
132 | MIME::Decoder::Base64 MIME::Decoder::Binary MIME::Decoder::QuotedPrint |
---|
133 | MIME::Decoder::NBit MIME::Decoder::UU MIME::Decoder::Gzip64 |
---|
134 | Net::Cmd Net::SMTP Net::Server Net::Server::PreForkSimple |
---|
135 | )); |
---|
136 | # with earlier versions of Perl one may need to add additional modules |
---|
137 | # to the list, such as: auto::POSIX::setgid auto::POSIX::setuid ... |
---|
138 | fetch_modules('OPTIONAL BASIC MODULES', 0, qw( |
---|
139 | Carp::Heavy auto::POSIX::setgid auto::POSIX::setuid |
---|
140 | MIME::Decoder::BinHex |
---|
141 | )); |
---|
142 | } |
---|
143 | |
---|
144 | 1; |
---|
145 | |
---|
146 | # |
---|
147 | package Amavis::Conf; |
---|
148 | use strict; |
---|
149 | use re 'taint'; |
---|
150 | |
---|
151 | # prototypes |
---|
152 | sub D_REJECT(); |
---|
153 | sub D_BOUNCE(); |
---|
154 | sub D_DISCARD(); |
---|
155 | sub D_PASS(); |
---|
156 | |
---|
157 | BEGIN { |
---|
158 | use Exporter (); |
---|
159 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
160 | $VERSION = '2.034'; |
---|
161 | @ISA = qw(Exporter); |
---|
162 | @EXPORT = (); |
---|
163 | @EXPORT_OK = (); |
---|
164 | %EXPORT_TAGS = ( |
---|
165 | 'dynamic_confvars' => [qw( |
---|
166 | $policy_bank_name $protocol @inet_acl |
---|
167 | $log_level $log_templ $log_recip_templ $forward_method $notify_method |
---|
168 | |
---|
169 | $amavis_auth_user $amavis_auth_pass $auth_reauthenticate_forwarded |
---|
170 | $auth_required_out $auth_required_inp @auth_mech_avail |
---|
171 | $local_client_bind_address |
---|
172 | $localhost_name $smtpd_greeting_banner $smtpd_quit_banner |
---|
173 | $smtpd_message_size_limit |
---|
174 | |
---|
175 | $final_virus_destiny $final_spam_destiny |
---|
176 | $final_banned_destiny $final_bad_header_destiny |
---|
177 | $warnvirussender $warnspamsender $warnbannedsender $warnbadhsender |
---|
178 | $warn_offsite |
---|
179 | |
---|
180 | @av_scanners @av_scanners_backup $first_infected_stops_scan |
---|
181 | $bypass_decode_parts |
---|
182 | |
---|
183 | $defang_virus $defang_banned $defang_spam |
---|
184 | $defang_bad_header $defang_undecipherable $defang_all |
---|
185 | $undecipherable_subject_tag |
---|
186 | $sa_spam_report_header $sa_spam_level_char |
---|
187 | $sa_mail_body_size_limit |
---|
188 | |
---|
189 | $localpart_is_case_sensitive |
---|
190 | $recipient_delimiter $replace_existing_extension |
---|
191 | $hdr_encoding $bdy_encoding $hdr_encoding_qb |
---|
192 | $notify_xmailer_header $X_HEADER_TAG $X_HEADER_LINE |
---|
193 | $remove_existing_x_scanned_headers $remove_existing_spam_headers |
---|
194 | |
---|
195 | $hdrfrom_notify_sender $hdrfrom_notify_recip |
---|
196 | $hdrfrom_notify_admin $hdrfrom_notify_spamadmin |
---|
197 | $mailfrom_notify_sender $mailfrom_notify_recip |
---|
198 | $mailfrom_notify_admin $mailfrom_notify_spamadmin |
---|
199 | $mailfrom_to_quarantine |
---|
200 | $virus_quarantine_method $spam_quarantine_method |
---|
201 | $banned_files_quarantine_method $bad_header_quarantine_method |
---|
202 | %local_delivery_aliases |
---|
203 | |
---|
204 | $notify_sender_templ |
---|
205 | $notify_virus_sender_templ $notify_spam_sender_templ |
---|
206 | $notify_virus_admin_templ $notify_spam_admin_templ |
---|
207 | $notify_virus_recips_templ $notify_spam_recips_templ |
---|
208 | |
---|
209 | $banned_namepath_re |
---|
210 | $per_recip_whitelist_sender_lookup_tables |
---|
211 | $per_recip_blacklist_sender_lookup_tables |
---|
212 | |
---|
213 | @local_domains_maps @mynetworks_maps |
---|
214 | @bypass_virus_checks_maps @bypass_spam_checks_maps |
---|
215 | @bypass_banned_checks_maps @bypass_header_checks_maps |
---|
216 | @virus_lovers_maps @spam_lovers_maps |
---|
217 | @banned_files_lovers_maps @bad_header_lovers_maps |
---|
218 | @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps |
---|
219 | @newvirus_admin_maps @virus_admin_maps |
---|
220 | @banned_admin_maps @bad_header_admin_maps @spam_admin_maps |
---|
221 | @virus_quarantine_to_maps |
---|
222 | @banned_quarantine_to_maps @bad_header_quarantine_to_maps |
---|
223 | @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps |
---|
224 | @banned_filename_maps |
---|
225 | @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps |
---|
226 | @spam_dsn_cutoff_level_maps @spam_modifies_subj_maps |
---|
227 | @spam_subject_tag_maps @spam_subject_tag2_maps |
---|
228 | @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps |
---|
229 | @message_size_limit_maps |
---|
230 | @addr_extension_virus_maps @addr_extension_spam_maps |
---|
231 | @addr_extension_banned_maps @addr_extension_bad_header_maps |
---|
232 | @debug_sender_maps |
---|
233 | )], |
---|
234 | 'confvars' => [qw( |
---|
235 | $myproduct_name $myversion_id $myversion_id_numeric $myversion_date |
---|
236 | $myversion $myhostname |
---|
237 | $MYHOME $TEMPBASE $QUARANTINEDIR |
---|
238 | $daemonize $pid_file $lock_file $db_home |
---|
239 | $enable_db $enable_global_cache |
---|
240 | $daemon_user $daemon_group $daemon_chroot_dir $path |
---|
241 | $DEBUG $DO_SYSLOG $SYSLOG_LEVEL $LOGFILE |
---|
242 | $max_servers $max_requests $child_timeout |
---|
243 | %current_policy_bank %policy_bank %interface_policy |
---|
244 | $unix_socketname $inet_socket_port $inet_socket_bind |
---|
245 | $insert_received_line $relayhost_is_client $smtpd_recipient_limit |
---|
246 | $MAXLEVELS $MAXFILES |
---|
247 | $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR |
---|
248 | $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR |
---|
249 | @lookup_sql_dsn |
---|
250 | $sql_select_policy $sql_select_white_black_list |
---|
251 | $virus_check_negative_ttl $virus_check_positive_ttl |
---|
252 | $spam_check_negative_ttl $spam_check_positive_ttl |
---|
253 | $enable_ldap $default_ldap |
---|
254 | @keep_decoded_original_maps @map_full_type_to_short_type_maps |
---|
255 | @viruses_that_fake_sender_maps |
---|
256 | )], |
---|
257 | 'unpack' => [qw( |
---|
258 | $file $arc $gzip $bzip2 $lzop $lha $unarj $uncompress $unfreeze |
---|
259 | $unrar $zoo $pax $cpio $ar $rpm2cpio $cabextract $ripole |
---|
260 | )], |
---|
261 | 'sa' => [qw( |
---|
262 | $helpers_home $dspam |
---|
263 | $sa_local_tests_only $sa_auto_whitelist $sa_timeout $sa_debug |
---|
264 | )], |
---|
265 | 'platform' => [qw( |
---|
266 | $can_truncate $unicode_aware $eol |
---|
267 | &D_REJECT &D_BOUNCE &D_DISCARD &D_PASS |
---|
268 | )], |
---|
269 | |
---|
270 | # other variables settable by user in amavisd.conf, |
---|
271 | # but not directly accessible by the program |
---|
272 | 'hidden_confvars' => [qw( |
---|
273 | $mydomain |
---|
274 | )], |
---|
275 | |
---|
276 | # legacy variables, predeclared for compatibility of amavisd.conf |
---|
277 | # The rest of the program does not use them directly and they should not be |
---|
278 | # visible in other modules, but may be referenced throgh @*_maps variables. |
---|
279 | 'legacy_confvars' => [qw( |
---|
280 | %local_domains @local_domains_acl $local_domains_re @mynetworks |
---|
281 | %bypass_virus_checks @bypass_virus_checks_acl $bypass_virus_checks_re |
---|
282 | %bypass_spam_checks @bypass_spam_checks_acl $bypass_spam_checks_re |
---|
283 | %bypass_banned_checks @bypass_banned_checks_acl $bypass_banned_checks_re |
---|
284 | %bypass_header_checks @bypass_header_checks_acl $bypass_header_checks_re |
---|
285 | %virus_lovers @virus_lovers_acl $virus_lovers_re |
---|
286 | %spam_lovers @spam_lovers_acl $spam_lovers_re |
---|
287 | %banned_files_lovers @banned_files_lovers_acl $banned_files_lovers_re |
---|
288 | %bad_header_lovers @bad_header_lovers_acl $bad_header_lovers_re |
---|
289 | %virus_admin %spam_admin |
---|
290 | $newvirus_admin $virus_admin $banned_admin $bad_header_admin $spam_admin |
---|
291 | $warnvirusrecip $warnbannedrecip $warnbadhrecip |
---|
292 | $virus_quarantine_to $banned_quarantine_to $bad_header_quarantine_to |
---|
293 | $spam_quarantine_to $spam_quarantine_bysender_to |
---|
294 | $keep_decoded_original_re $map_full_type_to_short_type_re |
---|
295 | $banned_filename_re $viruses_that_fake_sender_re |
---|
296 | $sa_tag_level_deflt $sa_tag2_level_deflt $sa_kill_level_deflt |
---|
297 | $sa_dsn_cutoff_level $sa_spam_modifies_subj |
---|
298 | $sa_spam_subject_tag1 $sa_spam_subject_tag |
---|
299 | %whitelist_sender @whitelist_sender_acl $whitelist_sender_re |
---|
300 | %blacklist_sender @blacklist_sender_acl $blacklist_sender_re |
---|
301 | $addr_extension_virus $addr_extension_spam |
---|
302 | $addr_extension_banned $addr_extension_bad_header |
---|
303 | $gets_addr_in_quoted_form @debug_sender_acl |
---|
304 | )], |
---|
305 | ); |
---|
306 | Exporter::export_tags qw(dynamic_confvars confvars unpack sa platform |
---|
307 | hidden_confvars legacy_confvars); |
---|
308 | } # BEGIN |
---|
309 | |
---|
310 | use POSIX qw(uname); |
---|
311 | use Carp (); |
---|
312 | use Errno qw(ENOENT EACCES); |
---|
313 | |
---|
314 | use vars @EXPORT; |
---|
315 | |
---|
316 | sub c($); sub cr($); sub ca($); # prototypes |
---|
317 | use subs qw(c cr ca); # access subroutine to new-style config variables |
---|
318 | BEGIN { push(@EXPORT,qw(c cr ca)) } |
---|
319 | |
---|
320 | { # initialize new-style hash (policy bank) containing dynamic config settings |
---|
321 | for my $tag (@EXPORT_TAGS{'dynamic_confvars'}) { |
---|
322 | for my $v (@$tag) { |
---|
323 | if ($v !~ /^([%\$\@])(.*)\z/) { die "Unsupported variable type: $v" } |
---|
324 | else { |
---|
325 | no strict 'refs'; my($type,$name) = ($1,$2); |
---|
326 | $current_policy_bank{$name} = $type eq '$' ? \${"Amavis::Conf::$name"} |
---|
327 | : $type eq '@' ? \@{"Amavis::Conf::$name"} |
---|
328 | : $type eq '%' ? \%{"Amavis::Conf::$name"} |
---|
329 | : undef; |
---|
330 | } |
---|
331 | } |
---|
332 | } |
---|
333 | $current_policy_bank{'policy_bank_name'} = ''; # builtin policy |
---|
334 | $current_policy_bank{'policy_bank_path'} = ''; |
---|
335 | $policy_bank{''} = { %current_policy_bank }; # copy |
---|
336 | } |
---|
337 | |
---|
338 | # new-style access to dynamic config variables |
---|
339 | # return a config variable value - usually a scalar; |
---|
340 | # one level of indirection for scalars is allowed |
---|
341 | sub c($) { |
---|
342 | my($name) = @_; |
---|
343 | if (!exists $current_policy_bank{$name}) { |
---|
344 | Carp::croak(sprintf('No entry "%s" in policy bank "%s"', |
---|
345 | $name, $current_policy_bank{'policy_bank_name'})); |
---|
346 | } |
---|
347 | my($var) = $current_policy_bank{$name}; my($r) = ref($var); |
---|
348 | !$r ? $var : $r eq 'SCALAR' ? $$var |
---|
349 | : $r eq 'ARRAY' ? @$var : $r eq 'HASH' ? %$var : $var; |
---|
350 | } |
---|
351 | |
---|
352 | # return a ref to a config variable value, or undef if var is undefined |
---|
353 | sub cr($) { |
---|
354 | my($name) = @_; |
---|
355 | if (!exists $current_policy_bank{$name}) { |
---|
356 | Carp::croak(sprintf('No entry "%s" in policy bank "%s"', |
---|
357 | $name, $current_policy_bank{'policy_bank_name'})); |
---|
358 | } |
---|
359 | my($var) = $current_policy_bank{$name}; |
---|
360 | !defined($var) ? undef : !ref($var) ? \$var : $var; |
---|
361 | } |
---|
362 | |
---|
363 | # return a ref to a config variable value (which is supposed to be an array), |
---|
364 | # converting undef to an empty array, and a scalar to a one-element array |
---|
365 | # if necessary |
---|
366 | sub ca($) { |
---|
367 | my($name) = @_; |
---|
368 | if (!exists $current_policy_bank{$name}) { |
---|
369 | Carp::croak(sprintf('No entry "%s" in policy bank "%s"', |
---|
370 | $name, $current_policy_bank{'policy_bank_name'})); |
---|
371 | } |
---|
372 | my($var) = $current_policy_bank{$name}; |
---|
373 | !defined($var) ? [] : !ref($var) ? [$var] : $var; |
---|
374 | } |
---|
375 | |
---|
376 | $myproduct_name = 'amavisd-new'; |
---|
377 | $myversion_id = '2.2.1'; $myversion_date = '20041222'; |
---|
378 | |
---|
379 | $myversion = "$myproduct_name-$myversion_id ($myversion_date)"; |
---|
380 | $myversion_id_numeric = # x.yyyzzz, allows numerical comparision, like Perl $] |
---|
381 | sprintf("%8.6f", $1 + ($2 + $3/1000)/1000) |
---|
382 | if $myversion_id =~ /^(\d+)(?:\.(\d*)(?:\.(\d*))?)?(.*)$/; |
---|
383 | |
---|
384 | $eol = "\n"; # native record separator in files: LF or CRLF or even CR |
---|
385 | $unicode_aware = $]>=5.008 && length("\x{263a}")==1 && eval { require Encode }; |
---|
386 | |
---|
387 | # serves only as a quick default for other configuration settings |
---|
388 | $MYHOME = '/var/amavis'; |
---|
389 | $mydomain = '!change-mydomain-variable!.example.com';#intentionally bad default |
---|
390 | |
---|
391 | # Create debugging output - true: log to stderr; false: log to syslog/file |
---|
392 | $DEBUG = 0; |
---|
393 | |
---|
394 | # Cause Net::Server parameters 'background' and 'setsid' to be set, |
---|
395 | # resulting in the program to detach itself from the terminal |
---|
396 | $daemonize = 1; |
---|
397 | |
---|
398 | # Net::Server pre-forking settings - defaults, overruled by amavisd.conf |
---|
399 | $max_servers = 2; # number of pre-forked children |
---|
400 | $max_requests = 10; # retire a child after that many accepts |
---|
401 | |
---|
402 | $child_timeout = 8*60; # abort child if it does not complete each task in n sec |
---|
403 | |
---|
404 | # Can file be truncated? |
---|
405 | # Set to 1 if 'truncate' works (it is XPG4-UNIX standard feature, |
---|
406 | # not required by Posix). |
---|
407 | # Things will go faster with SMTP-in, otherwise (e.g. with milter) |
---|
408 | # it makes no difference as file truncation will not be used. |
---|
409 | $can_truncate = 1; |
---|
410 | |
---|
411 | # expiration time of cached results: time to live in seconds |
---|
412 | # (how long the result of a virus/spam test remains valid) |
---|
413 | $virus_check_negative_ttl= 3*60; # time to remember that mail was not infected |
---|
414 | $virus_check_positive_ttl= 30*60; # time to remember that mail was infected |
---|
415 | $spam_check_negative_ttl = 30*60; # time to remember that mail was not spam |
---|
416 | $spam_check_positive_ttl = 30*60; # time to remember that mail was spam |
---|
417 | # |
---|
418 | # NOTE: |
---|
419 | # Cache size will be determined by the largest of the $*_ttl values. |
---|
420 | # Depending on the mail rate, the cache database may grow quite large. |
---|
421 | # Reasonable compromise for the max value is 15 minutes to 2 hours. |
---|
422 | |
---|
423 | # Customizable notification messages, logging |
---|
424 | |
---|
425 | $SYSLOG_LEVEL = 'mail.debug'; |
---|
426 | |
---|
427 | $enable_db = 0; # load optional modules Amavis::DB & Amavis::DB::SNMP |
---|
428 | $enable_global_cache = 0; # enable use of bdb-based Amavis::Cache |
---|
429 | |
---|
430 | # Where to find SQL server(s) and database to support SQL lookups? |
---|
431 | # A list of triples: (dsn,user,passw). Specify more than one |
---|
432 | # for multiple (backup) SQL servers. |
---|
433 | # |
---|
434 | #@lookup_sql_dsn = |
---|
435 | # ( ['DBI:mysql:mail:host1', 'some-username1', 'some-password1'], |
---|
436 | # ['DBI:mysql:mail:host2', 'some-username2', 'some-password2'] ); |
---|
437 | |
---|
438 | # The SQL select clause to fetch per-recipient policy settings |
---|
439 | # The %k will be replaced by a comma-separated list of query addresses |
---|
440 | # (e.g. full address, domain only, catchall). Use ORDER, if there |
---|
441 | # is a chance that multiple records will match - the first match wins |
---|
442 | # If field names are not unique (e.g. 'id'), the later field overwrites the |
---|
443 | # earlier in a hash returned by lookup, which is why we use '*,users.id'. |
---|
444 | $sql_select_policy = |
---|
445 | 'SELECT *,users.id FROM users,policy' |
---|
446 | . ' WHERE (users.policy_id=policy.id) AND (users.email IN (%k))' |
---|
447 | . ' ORDER BY users.priority DESC'; |
---|
448 | |
---|
449 | # The SQL select clause to check sender in per-recipient whitelist/blacklist |
---|
450 | # The first SELECT argument '?' will be users.id from recipient SQL lookup, |
---|
451 | # the %k will be sender addresses (e.g. full address, domain only, catchall). |
---|
452 | # Only the first occurrence of '?' will be replaced by users.id, subsequent |
---|
453 | # occurrences of '?' will see empty string as an argument. There can be zero |
---|
454 | # or more occurrences of %k, lookup keys will be multiplied accordingly. |
---|
455 | # Up until version 2.2.0 the '?' had to be placed before the '%k'; |
---|
456 | # starting with 2.2.1 this restriction is lifted. |
---|
457 | $sql_select_white_black_list = |
---|
458 | 'SELECT wb FROM wblist,mailaddr' |
---|
459 | . ' WHERE (wblist.rid=?) AND (wblist.sid=mailaddr.id)' |
---|
460 | . ' AND (mailaddr.email IN (%k))' |
---|
461 | . ' ORDER BY mailaddr.priority DESC'; |
---|
462 | |
---|
463 | # |
---|
464 | # Receiving mail related |
---|
465 | |
---|
466 | # $unix_socketname = '/var/amavis/amavisd.sock'; # traditional amavis client protocol |
---|
467 | # $inet_socket_port = 10024; # accept SMTP on this TCP port |
---|
468 | # $inet_socket_port = [10024,10026,10027]; # ...possibly on more than one |
---|
469 | $inet_socket_bind = '127.0.0.1'; # limit socket bind to loopback interface |
---|
470 | |
---|
471 | @inet_acl = qw( 127.0.0.1 ::1 ); # allow SMTP access only from localhost |
---|
472 | @mynetworks = qw( 127.0.0.0/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 ); |
---|
473 | |
---|
474 | $notify_method = 'smtp:[127.0.0.1]:10025'; |
---|
475 | $forward_method = 'smtp:[127.0.0.1]:10025'; |
---|
476 | |
---|
477 | $virus_quarantine_method = 'local:virus-%i-%n'; |
---|
478 | $spam_quarantine_method = 'local:spam-%b-%i-%n'; |
---|
479 | $banned_files_quarantine_method = 'local:banned-%i-%n'; |
---|
480 | $bad_header_quarantine_method = 'local:badh-%i-%n'; |
---|
481 | |
---|
482 | $insert_received_line = 1; # insert 'Received:' header field? (not with milter) |
---|
483 | $remove_existing_x_scanned_headers = 0; |
---|
484 | $remove_existing_spam_headers = 1; |
---|
485 | |
---|
486 | # encoding (charset in MIME terminology) |
---|
487 | # to be used in RFC 2047-encoded ... |
---|
488 | $hdr_encoding = 'iso-8859-1'; # ... header field bodies |
---|
489 | $bdy_encoding = 'iso-8859-1'; # ... notification body text |
---|
490 | |
---|
491 | # encoding (encoding in MIME terminology) |
---|
492 | $hdr_encoding_qb = 'Q'; # quoted-printable (default) |
---|
493 | #$hdr_encoding_qb = 'B'; # base64 (usual for far east charsets) |
---|
494 | |
---|
495 | $smtpd_recipient_limit = 1100; # max recipients (RCPT TO) - sanity limit |
---|
496 | |
---|
497 | # $myhostname is used by SMTP server module in the initial SMTP welcome line, |
---|
498 | # in inserted 'Received:' lines, Message-ID in notifications, log entries, ... |
---|
499 | $myhostname = (uname)[1]; # should be a FQDN ! |
---|
500 | |
---|
501 | $smtpd_greeting_banner = '${helo-name} ${protocol} ${product} service ready'; |
---|
502 | $smtpd_quit_banner = '${helo-name} ${product} closing transmission channel'; |
---|
503 | |
---|
504 | # $localhost_name is the name of THIS host running amavisd |
---|
505 | # (typically 'localhost'). It is used in HELO SMTP command |
---|
506 | # when reinjecting mail back to MTA via SMTP for final delivery. |
---|
507 | $localhost_name = 'localhost'; |
---|
508 | |
---|
509 | # @auth_mech_avail = ('PLAIN','LOGIN'); # empty list disables incoming AUTH |
---|
510 | #$auth_required_inp = 1; # incoming SMTP authentication required by amavisd? |
---|
511 | #$auth_required_out = 1; # SMTP authentication required by MTA |
---|
512 | |
---|
513 | # SMTP AUTH username and password for notification submissions |
---|
514 | # (and reauthentication of forwarded mail if requested) |
---|
515 | #$amavis_auth_user = undef; # perhaps: 'amavisd' |
---|
516 | #$amavis_auth_pass = undef; |
---|
517 | #$auth_reauthenticate_forwarded = undef; # supply our own credentials also |
---|
518 | # for forwarded (passed) mail |
---|
519 | |
---|
520 | # whom quarantined messages appear to be sent from (envelope sender) |
---|
521 | # $mailfrom_to_quarantine = undef; # original sender if undef, or set explicitly |
---|
522 | |
---|
523 | # where to send quarantined malware |
---|
524 | # Specify undef to disable, or e-mail address containing '@', |
---|
525 | # or just a local part, which will be mapped by %local_delivery_aliases |
---|
526 | # into local mailbox name or directory. The lookup key is a recipient address |
---|
527 | $virus_quarantine_to = 'virus-quarantine'; # %local_delivery_aliases mapped |
---|
528 | $banned_quarantine_to = 'banned-quarantine'; # %local_delivery_aliases mapped |
---|
529 | $bad_header_quarantine_to = 'bad-header-quarantine'; # %local_delivery_aliases |
---|
530 | $spam_quarantine_to = 'spam-quarantine'; # %local_delivery_aliases mapped |
---|
531 | |
---|
532 | $banned_admin = \@virus_admin_maps; # compatibility |
---|
533 | $bad_header_admin = \@virus_admin_maps; # compatibility |
---|
534 | |
---|
535 | # similar to $spam_quarantine_to, but the lookup key is the sender address |
---|
536 | $spam_quarantine_bysender_to = undef; # dflt: no by-sender spam quarantine |
---|
537 | |
---|
538 | # quarantine directory or mailbox file or empty |
---|
539 | # (only used if $virus_quarantine_to specifies direct local delivery) |
---|
540 | $QUARANTINEDIR = undef; # no quarantine unless overridden by config |
---|
541 | |
---|
542 | $undecipherable_subject_tag = '***UNCHECKED*** '; |
---|
543 | |
---|
544 | # string to prepend to Subject header field when message qualifies as spam |
---|
545 | # $sa_spam_subject_tag1 = undef; # example: '***possible SPAM*** ' |
---|
546 | # $sa_spam_subject_tag = undef; # example: '***SPAM*** ' |
---|
547 | $sa_spam_modifies_subj = 1; # true for compatibility; can be a |
---|
548 | # lookup table indicating per-recip settings |
---|
549 | $sa_spam_level_char = '*'; # character to be used in X-Spam-Level bar; |
---|
550 | # empty or undef disables adding this header field |
---|
551 | # $sa_spam_report_header = undef; # insert X-Spam-Report header field? |
---|
552 | $sa_local_tests_only = 0; |
---|
553 | $sa_debug = 0; |
---|
554 | $sa_timeout = 30; # timeout in seconds for a call to SpamAssassin |
---|
555 | |
---|
556 | # MIME defanging is only activated when enabled and malware is allowed to pass |
---|
557 | # $defang_virus = undef; |
---|
558 | # $defang_banned = undef; |
---|
559 | # $defang_spam = undef; |
---|
560 | # $defang_bad_header = undef; |
---|
561 | # $defang_undecipherable = undef; |
---|
562 | # $defang_all = undef; |
---|
563 | |
---|
564 | $MIN_EXPANSION_FACTOR = 5; # times original mail size |
---|
565 | $MAX_EXPANSION_FACTOR = 500; # times original mail size |
---|
566 | |
---|
567 | # See amavisd.conf and README.lookups for details. |
---|
568 | |
---|
569 | # What to do with the message (this is independent of quarantining): |
---|
570 | # Reject: tell MTA to generate a non-delivery notification, MTA gets 5xx |
---|
571 | # Bounce: generate a non-delivery notification by ourselves, MTA gets 250 |
---|
572 | # Discard: drop the message and pretend it was delivered, MTA gets 250 |
---|
573 | # Pass: deliver/accept the message |
---|
574 | # |
---|
575 | # Bounce and Reject are similar: in both cases sender gets a non-delivery |
---|
576 | # notification, either generated by amavisd-new, or by MTA. The notification |
---|
577 | # issued by amavisd-new may be more informative, while on the other hand |
---|
578 | # MTA may be able to do a true reject on the original SMTP session |
---|
579 | # (e.g. with sendmail milter), or else it just generates normal non-delivery |
---|
580 | # notification / bounce (e.g. with Postfix, Exim). As a consequence, |
---|
581 | # with Postfix and Exim and dual-sendmail setup the Bounce is more informative |
---|
582 | # than Reject, but sendmail-milter users may prefer Reject. |
---|
583 | # |
---|
584 | # Bounce and Discard are similar: in both cases amavisd-new confirms |
---|
585 | # to MTA the message reception with success code 250. The difference is |
---|
586 | # in sender notification: Bounce sends a non-delivery notification to sender, |
---|
587 | # Discard does not, the message is silently dropped. Quarantine and |
---|
588 | # admin notifications are not affected by any of these settings. |
---|
589 | # |
---|
590 | # COMPATIBITITY NOTE: the separation of *_destiny values into |
---|
591 | # D_BOUNCE, D_REJECT, D_DISCARD and D_PASS made settings $warnvirussender |
---|
592 | # and $warnspamsender only still useful with D_PASS. The combination of |
---|
593 | # D_DISCARD + $warn*sender=1 is mapped into D_BOUNCE for compatibility. |
---|
594 | |
---|
595 | # intentionally leave value -1 unassigned for compatibility |
---|
596 | sub D_REJECT () { -3 } |
---|
597 | sub D_BOUNCE () { -2 } |
---|
598 | sub D_DISCARD() { 0 } |
---|
599 | sub D_PASS () { 1 } |
---|
600 | |
---|
601 | # The following symbolic constants can be used in *destiny settings: |
---|
602 | # |
---|
603 | # D_PASS mail will pass to recipients, regardless of contents; |
---|
604 | # |
---|
605 | # D_DISCARD mail will not be delivered to its recipients, sender will NOT be |
---|
606 | # notified. Effectively we lose mail (but it will be quarantined |
---|
607 | # unless disabled). Not a decent thing to do for a mailer. |
---|
608 | # |
---|
609 | # D_BOUNCE mail will not be delivered to its recipients, a non-delivery |
---|
610 | # notification (bounce) will be sent to the sender by amavisd-new; |
---|
611 | # Exception: bounce (DSN) will not be sent if a virus name matches |
---|
612 | # $viruses_that_fake_sender_maps, or to messages from mailing lists |
---|
613 | # (Precedence: bulk|list|junk), or for spam exceeding |
---|
614 | # spam_dsn_cutoff_level |
---|
615 | # |
---|
616 | # D_REJECT mail will not be delivered to its recipients, sender should |
---|
617 | # preferably get a reject, e.g. SMTP permanent reject response |
---|
618 | # (e.g. with milter), or non-delivery notification from MTA |
---|
619 | # (e.g. Postfix). If this is not possible (e.g. different recipients |
---|
620 | # have different tolerances to bad mail contents and not using LMTP) |
---|
621 | # amavisd-new sends a bounce by itself (same as D_BOUNCE). |
---|
622 | # |
---|
623 | # Notes: |
---|
624 | # D_REJECT and D_BOUNCE are similar, the difference is in who is responsible |
---|
625 | # for informing the sender about non-delivery, and how informative |
---|
626 | # the notification can be (amavisd-new knows more than MTA); |
---|
627 | # With D_REJECT, MTA may reject original SMTP, or send DSN (delivery status |
---|
628 | # notification, colloquially called 'bounce') - depending on MTA; |
---|
629 | # Best suited for sendmail milter, especially for spam. |
---|
630 | # With D_BOUNCE, amavisd-new (not MTA) sends DSN (can better explain the |
---|
631 | # reason for mail non-delivery but unable to reject the original |
---|
632 | # SMTP session, is in position to suppress DSN if considered |
---|
633 | # unsuitable). Best suited for Postfix and other dual-MTA setups. |
---|
634 | |
---|
635 | $final_virus_destiny = D_DISCARD; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS |
---|
636 | $final_banned_destiny = D_BOUNCE; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS |
---|
637 | $final_spam_destiny = D_BOUNCE; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS |
---|
638 | $final_bad_header_destiny = D_PASS; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS |
---|
639 | |
---|
640 | # If you decide to pass viruses (or spam) to certain users using |
---|
641 | # %virus_lovers/@virus_lovers_acl/$virus_lovers_re, (or *spam_lovers*), |
---|
642 | # %bypass_virus_checks/@bypass_virus_checks_acl, or $final_virus_destiny=D_PASS |
---|
643 | # ($final_spam_destiny=D_PASS), you can set the variable $addr_extension_virus |
---|
644 | # ($addr_extension_spam) to some string, and the recipient address will have |
---|
645 | # this string appended as an address extension to the local-part of the |
---|
646 | # address. This extension can be used by final local delivery agent to place |
---|
647 | # such mail in different folders. Leave these variables undefined or empty |
---|
648 | # strings to prevent appending address extensions. Setting has no effect |
---|
649 | # on users which will not be receiving viruses (spam). Recipients which |
---|
650 | # do not match access lists in @local_domains_maps are not affected (i.e. |
---|
651 | # non-local recipients). |
---|
652 | # |
---|
653 | # LDAs usually default to stripping away address extension if no special |
---|
654 | # handling for it is specified, so having this option enabled normally |
---|
655 | # does no harm, provided the $recipients_delimiter character matches |
---|
656 | # the setting at the final MTA's local delivery agent (LDA). |
---|
657 | # |
---|
658 | # $addr_extension_virus = 'virus'; # for example |
---|
659 | # $addr_extension_spam = 'spam'; |
---|
660 | # $addr_extension_banned = 'banned'; |
---|
661 | # $addr_extension_bad_header = 'badh'; |
---|
662 | |
---|
663 | # Delimiter between local part of the recipient address and address extension |
---|
664 | # (which can optionally be added, see variables $addr_extension_virus and |
---|
665 | # $addr_extension_spam). E.g. recipient address <user@domain.example> gets |
---|
666 | # changed to <user+virus@domain.example>. |
---|
667 | # |
---|
668 | # Delimiter should match equivalent (final) MTA delimiter setting. |
---|
669 | # (e.g. for Postfix add 'recipient_delimiter = +' to main.cf). |
---|
670 | # Setting it to an empty string or to undef disables this feature |
---|
671 | # regardless of $addr_extension_virus and $addr_extension_spam settings. |
---|
672 | |
---|
673 | # $recipient_delimiter = '+'; |
---|
674 | $replace_existing_extension = 1; # true: replace ext; false: append ext |
---|
675 | |
---|
676 | # Affects matching of localpart of e-mail addresses (left of '@') |
---|
677 | # in lookups: true = case sensitive, false = case insensitive |
---|
678 | $localpart_is_case_sensitive = 0; |
---|
679 | |
---|
680 | # first match wins, more specific entries should precede general ones! |
---|
681 | # the result may be a string or a ref to a list of strings; |
---|
682 | # see also sub decompose_part() |
---|
683 | $map_full_type_to_short_type_re = Amavis::Lookup::RE->new( |
---|
684 | [qr/^empty\z/ => 'empty'], |
---|
685 | [qr/^directory\z/ => 'dir'], |
---|
686 | [qr/^can't (stat|read)\b/ => 'dat'], # file(1) diagnostics |
---|
687 | [qr/^cannot open\b/ => 'dat'], # file(1) diagnostics |
---|
688 | [qr/^ERROR: Corrupted\b/ => 'dat'], # file(1) diagnostics |
---|
689 | [qr/can't read magic file|couldn't find any magic files/ => 'dat'], |
---|
690 | [qr/^data\z/ => 'dat'], |
---|
691 | |
---|
692 | [qr/^ISO-8859.*\btext\b/ => 'txt'], |
---|
693 | [qr/^Non-ISO.*ASCII\b.*\btext\b/ => 'txt'], |
---|
694 | [qr/^Unicode\b.*\btext\b/i => 'txt'], |
---|
695 | [qr/^'diff' output text\b/ => 'txt'], |
---|
696 | [qr/^GNU message catalog\b/ => 'mo'], |
---|
697 | [qr/^PGP encrypted data\b/ => 'pgp'], |
---|
698 | [qr/^PGP armored data( signed)? message\b/ => ['pgp','pgp.asc'] ], |
---|
699 | [qr/^PGP armored\b/ => ['pgp','pgp.asc'] ], |
---|
700 | |
---|
701 | ### 'file' is a bit too trigger happy to claim something is 'mail text' |
---|
702 | # [qr/^RFC 822 mail text\b/ => 'mail'], |
---|
703 | [qr/^(ASCII|smtp|RFC 822) mail text\b/ => 'txt'], |
---|
704 | |
---|
705 | [qr/^JPEG image data\b/ =>['image','jpg'] ], |
---|
706 | [qr/^GIF image data\b/ =>['image','gif'] ], |
---|
707 | [qr/^PNG image data\b/ =>['image','png'] ], |
---|
708 | [qr/^TIFF image data\b/ =>['image','tif'] ], |
---|
709 | [qr/^PCX\b.*\bimage data\b/ =>['image','pcx'] ], |
---|
710 | [qr/^PC bitmap data\b/ =>['image','bmp'] ], |
---|
711 | |
---|
712 | [qr/^MP2\b/ =>['audio','mpa','mp2'] ], |
---|
713 | [qr/^MP3\b/ =>['audio','mpa','mp3'] ], |
---|
714 | [qr/^MPEG video stream data\b/ =>['movie','mpv'] ], |
---|
715 | [qr/^MPEG system stream data\b/ =>['movie','mpg'] ], |
---|
716 | [qr/^MPEG\b/ =>['movie','mpg'] ], |
---|
717 | [qr/^Microsoft ASF\b/ =>['movie','wmv'] ], |
---|
718 | [qr/^RIFF\b.*\bAVI\b/ =>['movie','avi'] ], |
---|
719 | [qr/^RIFF\b.*\bWAVE audio\b/ =>['audio','wav'] ], |
---|
720 | |
---|
721 | [qr/^Macromedia Flash data\b/ => 'swf'], |
---|
722 | [qr/^HTML document text\b/ => 'html'], |
---|
723 | [qr/^XML document text\b/ => 'xml'], |
---|
724 | [qr/^exported SGML document text\b/ => 'sgml'], |
---|
725 | [qr/^PostScript document text\b/ => 'ps'], |
---|
726 | [qr/^PDF document\b/ => 'pdf'], |
---|
727 | [qr/^Rich Text Format data\b/ => 'rtf'], |
---|
728 | [qr/^Microsoft Office Document\b/i => 'doc'], # OLE2: doc, ppt, xls, ... |
---|
729 | [qr/^LaTeX\b.*\bdocument text\b/ => 'lat'], |
---|
730 | [qr/^TeX DVI file\b/ => 'dvi'], |
---|
731 | [qr/\bdocument text\b/ => 'txt'], |
---|
732 | [qr/^compiled Java class data\b/ => 'java'], |
---|
733 | [qr/^MS Windows 95 Internet shortcut text\b/ => 'url'], |
---|
734 | |
---|
735 | [qr/^frozen\b/ => 'F'], |
---|
736 | [qr/^gzip compressed\b/ => 'gz'], |
---|
737 | [qr/^bzip compressed\b/ => 'bz'], |
---|
738 | [qr/^bzip2 compressed\b/ => 'bz2'], |
---|
739 | [qr/^lzop compressed\b/ => 'lzo'], |
---|
740 | [qr/^compress'd/ => 'Z'], |
---|
741 | [qr/^Zip archive\b/i => 'zip'], |
---|
742 | [qr/^RAR archive\b/i => 'rar'], |
---|
743 | [qr/^LHa.*\barchive\b/i => 'lha'], # or .lzh |
---|
744 | [qr/^ARC archive\b/i => 'arc'], |
---|
745 | [qr/^ARJ archive\b/i => 'arj'], |
---|
746 | [qr/^Zoo archive\b/i => 'zoo'], |
---|
747 | [qr/^(\S+\s+)?tar archive\b/i => 'tar'], |
---|
748 | [qr/^(\S+\s+)?cpio archive\b/i => 'cpio'], |
---|
749 | [qr/^Debian binary package\b/i => 'deb'], # standard Unix archive (ar) |
---|
750 | [qr/^current ar archive\b/i => 'a'], # standard Unix archive (ar) |
---|
751 | [qr/^RPM\b/ => 'rpm'], |
---|
752 | [qr/^(Transport Neutral Encapsulation Format|TNEF)\b/i => 'tnef'], |
---|
753 | [qr/^Microsoft cabinet file\b/ => 'cab'], |
---|
754 | |
---|
755 | [qr/^(uuencoded|xxencoded)\b/i => 'uue'], |
---|
756 | [qr/^binhex\b/i => 'hqx'], |
---|
757 | [qr/^(ASCII|text)\b/i => 'asc'], |
---|
758 | [qr/^Emacs.*byte-compiled Lisp data/i => 'asc'], # BinHex with an empty line |
---|
759 | [qr/\bscript text executable\b/ => 'txt'], |
---|
760 | |
---|
761 | [qr/^MS-DOS\b.*\bexecutable\b/ => ['exe','exe-ms'] ], |
---|
762 | [qr/^MS Windows\b.*\bexecutable\b/ => ['exe','exe-ms'] ], |
---|
763 | [qr/^PA-RISC.*\bexecutable\b/ => ['exe','exe-unix'] ], |
---|
764 | [qr/^ELF .*\bexecutable\b/ => ['exe','exe-unix'] ], |
---|
765 | [qr/^COFF format .*\bexecutable\b/ => ['exe','exe-unix'] ], |
---|
766 | [qr/^executable \(RISC System\b/ => ['exe','exe-unix'] ], |
---|
767 | [qr/^VMS\b.*\bexecutable\b/ => ['exe','exe-vms'] ], |
---|
768 | |
---|
769 | [qr/\bexecutable\b/i => 'exe'], |
---|
770 | [qr/^MS Windows\b.*\bDLL\b/ => 'dll'], |
---|
771 | [qr/\bshared object, \b/i => 'so'], |
---|
772 | [qr/\brelocatable, \b/i => 'o'], |
---|
773 | [qr/\btext\b/i => 'asc'], |
---|
774 | [qr/.*/ => 'dat'], # catchall |
---|
775 | |
---|
776 | ); |
---|
777 | |
---|
778 | # MS Windows PE 32-bit Intel 80386 GUI executable not relocatable |
---|
779 | # MS-DOS executable (EXE), OS/2 or MS Windows |
---|
780 | # PA-RISC1.1 executable dynamically linked |
---|
781 | # PA-RISC1.1 shared executable dynamically linked |
---|
782 | # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (FreeBSD), for FreeBSD 5.0.1, dynamically linked (uses shared libs), stripped |
---|
783 | # ELF 64-bit LSB executable, Alpha (unofficial), version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), stripped |
---|
784 | # ELF 64-bit MSB executable, SPARC V9, version 1 (FreeBSD), for FreeBSD 5.0, dynamically linked (uses shared libs), stripped |
---|
785 | # ELF 64-bit MSB shared object, SPARC V9, version 1 (FreeBSD), stripped |
---|
786 | # ELF 32-bit LSB executable, Intel 80386, version 1, dynamically` |
---|
787 | # ELF 32-bit MSB executable, SPARC, version 1, dynamically linke` |
---|
788 | # COFF format alpha executable paged stripped - version 3.11-10 |
---|
789 | # COFF format alpha executable paged dynamically linked stripped` |
---|
790 | # COFF format alpha demand paged executable or object module stripped - version 3.11-10 |
---|
791 | # COFF format alpha paged dynamically linked not stripped shared` |
---|
792 | # executable (RISC System/6000 V3.1) or obj module |
---|
793 | # VMS VAX executable |
---|
794 | |
---|
795 | # Define aliase names in this module to make it simpler to call |
---|
796 | # these routines from amavisd.conf |
---|
797 | *read_text = \&Amavis::Util::read_text; |
---|
798 | *read_l10n_templates = \&Amavis::Util::read_l10n_templates; |
---|
799 | *read_hash = \&Amavis::Util::read_hash; |
---|
800 | *ask_daemon = \&Amavis::AV::ask_daemon; |
---|
801 | *sophos_savi = \&Amavis::AV::ask_sophos_savi; |
---|
802 | *ask_clamav = \&Amavis::AV::ask_clamav; |
---|
803 | sub new_RE { Amavis::Lookup::RE->new(@_) } |
---|
804 | |
---|
805 | sub build_default_maps() { |
---|
806 | @local_domains_maps = ( |
---|
807 | \%local_domains, \@local_domains_acl, \$local_domains_re); |
---|
808 | @mynetworks_maps = (\@mynetworks); |
---|
809 | @bypass_virus_checks_maps = ( |
---|
810 | \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re); |
---|
811 | @bypass_spam_checks_maps = ( |
---|
812 | \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re); |
---|
813 | @bypass_banned_checks_maps = ( |
---|
814 | \%bypass_banned_checks, \@bypass_banned_checks_acl, \$bypass_banned_checks_re); |
---|
815 | @bypass_header_checks_maps = ( |
---|
816 | \%bypass_header_checks, \@bypass_header_checks_acl, \$bypass_header_checks_re); |
---|
817 | @virus_lovers_maps = ( |
---|
818 | \%virus_lovers, \@virus_lovers_acl, \$virus_lovers_re); |
---|
819 | @spam_lovers_maps = ( |
---|
820 | \%spam_lovers, \@spam_lovers_acl, \$spam_lovers_re); |
---|
821 | @banned_files_lovers_maps = ( |
---|
822 | \%banned_files_lovers, \@banned_files_lovers_acl, \$banned_files_lovers_re); |
---|
823 | @bad_header_lovers_maps = ( |
---|
824 | \%bad_header_lovers, \@bad_header_lovers_acl, \$bad_header_lovers_re); |
---|
825 | @warnvirusrecip_maps = (\$warnvirusrecip); |
---|
826 | @warnbannedrecip_maps = (\$warnbannedrecip); |
---|
827 | @warnbadhrecip_maps = (\$warnbadhrecip); |
---|
828 | @newvirus_admin_maps = (\$newvirus_admin); |
---|
829 | @virus_admin_maps = (\%virus_admin, \$virus_admin); |
---|
830 | @banned_admin_maps = (\$banned_admin); |
---|
831 | @bad_header_admin_maps= (\$bad_header_admin); |
---|
832 | @spam_admin_maps = (\%spam_admin, \$spam_admin); |
---|
833 | @virus_quarantine_to_maps = (\$virus_quarantine_to); |
---|
834 | @banned_quarantine_to_maps = (\$banned_quarantine_to); |
---|
835 | @bad_header_quarantine_to_maps = (\$bad_header_quarantine_to); |
---|
836 | @spam_quarantine_to_maps = (\$spam_quarantine_to); |
---|
837 | @spam_quarantine_bysender_to_maps = (\$spam_quarantine_bysender_to); |
---|
838 | @keep_decoded_original_maps = (\$keep_decoded_original_re); |
---|
839 | @map_full_type_to_short_type_maps = (\$map_full_type_to_short_type_re); |
---|
840 | @banned_filename_maps = (\$banned_filename_re); |
---|
841 | @viruses_that_fake_sender_maps = (\$viruses_that_fake_sender_re, 1); |
---|
842 | @spam_tag_level_maps = (\$sa_tag_level_deflt); |
---|
843 | @spam_tag2_level_maps = (\$sa_tag2_level_deflt); |
---|
844 | @spam_kill_level_maps = (\$sa_kill_level_deflt); |
---|
845 | @spam_dsn_cutoff_level_maps = (\$sa_dsn_cutoff_level); |
---|
846 | @spam_modifies_subj_maps = (\$sa_spam_modifies_subj); |
---|
847 | @spam_subject_tag_maps = (\$sa_spam_subject_tag1); # note: inconsistent |
---|
848 | @spam_subject_tag2_maps = (\$sa_spam_subject_tag); # note: inconsistent |
---|
849 | @whitelist_sender_maps = ( |
---|
850 | \%whitelist_sender, \@whitelist_sender_acl, \$whitelist_sender_re); |
---|
851 | @blacklist_sender_maps = ( |
---|
852 | \%blacklist_sender, \@blacklist_sender_acl, \$blacklist_sender_re); |
---|
853 | @score_sender_maps = (); # new variable, no backwards compatibility needed |
---|
854 | @message_size_limit_maps = (); # new variable |
---|
855 | @addr_extension_virus_maps = (\$addr_extension_virus); |
---|
856 | @addr_extension_spam_maps = (\$addr_extension_spam); |
---|
857 | @addr_extension_banned_maps = (\$addr_extension_banned); |
---|
858 | @addr_extension_bad_header_maps = (\$addr_extension_bad_header); |
---|
859 | @debug_sender_maps = (\@debug_sender_acl); |
---|
860 | } |
---|
861 | |
---|
862 | # prepend a lookup table label object for logging purposes |
---|
863 | sub label_default_maps() { |
---|
864 | for my $varname (qw( |
---|
865 | @local_domains_maps @mynetworks_maps |
---|
866 | @bypass_virus_checks_maps @bypass_spam_checks_maps |
---|
867 | @bypass_banned_checks_maps @bypass_header_checks_maps |
---|
868 | @virus_lovers_maps @spam_lovers_maps |
---|
869 | @banned_files_lovers_maps @bad_header_lovers_maps |
---|
870 | @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps |
---|
871 | @newvirus_admin_maps @virus_admin_maps |
---|
872 | @banned_admin_maps @bad_header_admin_maps @spam_admin_maps |
---|
873 | @virus_quarantine_to_maps |
---|
874 | @banned_quarantine_to_maps @bad_header_quarantine_to_maps |
---|
875 | @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps |
---|
876 | @keep_decoded_original_maps @map_full_type_to_short_type_maps |
---|
877 | @banned_filename_maps @viruses_that_fake_sender_maps |
---|
878 | @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps |
---|
879 | @spam_dsn_cutoff_level_maps @spam_modifies_subj_maps |
---|
880 | @spam_subject_tag_maps @spam_subject_tag2_maps |
---|
881 | @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps |
---|
882 | @message_size_limit_maps |
---|
883 | @addr_extension_virus_maps @addr_extension_spam_maps |
---|
884 | @addr_extension_banned_maps @addr_extension_bad_header_maps |
---|
885 | @debug_sender_maps )) |
---|
886 | { |
---|
887 | my($g) = $varname; $g =~ s{\@}{Amavis::Conf::}; # qualified variable name |
---|
888 | my($label) = $varname; $label=~s/^\@//; $label=~s/_maps$//; |
---|
889 | { no strict 'refs'; |
---|
890 | unshift(@$g, # NOTE: a symbolic reference |
---|
891 | Amavis::Lookup::Label->new($label)) if @$g; # no label if empty |
---|
892 | } |
---|
893 | } |
---|
894 | } |
---|
895 | |
---|
896 | # read and evaluate configuration files (one or more) |
---|
897 | sub read_config(@) { |
---|
898 | my(@config_files) = @_; |
---|
899 | for my $config_file (@config_files) { |
---|
900 | my($msg); |
---|
901 | my($errn) = stat($config_file) ? 0 : 0+$!; |
---|
902 | if ($errn == ENOENT) { $msg = "does not exist" } |
---|
903 | elsif ($errn) { $msg = "is inaccessible: $!" } |
---|
904 | elsif (-d _) { $msg = "is a directory" } |
---|
905 | elsif (!-f _) { $msg = "is not a regular file" } |
---|
906 | elsif ($> && -o _) { $msg = "is owned by EUID $>, should be owned by root"} |
---|
907 | elsif ($> && -w _) { $msg = "is writable by EUID $>, EGID $)" } |
---|
908 | if (defined $msg) { die "Config file \"$config_file\" $msg," } |
---|
909 | $! = undef; my($rv) = do $config_file; |
---|
910 | if (!defined($rv)) { |
---|
911 | if ($@ ne '') { die "Error in config file \"$config_file\": $@" } |
---|
912 | else { die "Error reading config file \"$config_file\": $!" } |
---|
913 | } |
---|
914 | } |
---|
915 | $daemon_chroot_dir = '' if !defined $daemon_chroot_dir; # avoids warnings |
---|
916 | # some sensible defaults for essential settings |
---|
917 | $TEMPBASE = $MYHOME if !defined $TEMPBASE; |
---|
918 | $helpers_home = $MYHOME if !defined $helpers_home; |
---|
919 | $db_home = "$MYHOME/db" if !defined $db_home; |
---|
920 | $lock_file = "$MYHOME/amavisd.lock" if !defined $lock_file; |
---|
921 | $pid_file = "$MYHOME/amavisd.pid" if !defined $pid_file; |
---|
922 | |
---|
923 | $X_HEADER_TAG = 'X-Virus-Scanned' if !defined $X_HEADER_TAG; |
---|
924 | $X_HEADER_LINE= "$myproduct_name at $mydomain" if !defined $X_HEADER_LINE; |
---|
925 | #my($pname)= "$pname" if !defined $pname; |
---|
926 | # $pname = "Content filter" if !defined $pname; |
---|
927 | #my($pname) = "\"Content-filter at $myhostname\"" if !defined $CONTENT; |
---|
928 | $hdrfrom_notify_sender = "<postmaster\@$myhostname>" |
---|
929 | if !defined $hdrfrom_notify_sender; |
---|
930 | $hdrfrom_notify_recip = $mailfrom_notify_recip ne '' |
---|
931 | ? "<$mailfrom_notify_recip>" |
---|
932 | : $hdrfrom_notify_sender if !defined $hdrfrom_notify_recip; |
---|
933 | $hdrfrom_notify_admin = $mailfrom_notify_admin ne '' |
---|
934 | ? "<$mailfrom_notify_admin>" |
---|
935 | : $hdrfrom_notify_sender if !defined $hdrfrom_notify_admin; |
---|
936 | $hdrfrom_notify_spamadmin = $mailfrom_notify_spamadmin ne '' |
---|
937 | ? "<$mailfrom_notify_spamadmin>" |
---|
938 | : $hdrfrom_notify_sender if !defined $hdrfrom_notify_spamadmin; |
---|
939 | |
---|
940 | # compatibility with deprecated $warn*sender and old *_destiny values |
---|
941 | # map old values <0, =0, >0 into D_REJECT/D_BOUNCE, D_DISCARD, D_PASS |
---|
942 | for ($final_virus_destiny, $final_banned_destiny, $final_spam_destiny) { |
---|
943 | if ($_ > 0) { $_ = D_PASS } |
---|
944 | elsif ($_ < 0 && $_ != D_BOUNCE && $_ != D_REJECT) { # compatibility |
---|
945 | # favour Reject with sendmail milter, Bounce with others |
---|
946 | $_ = c('forward_method') eq '' ? D_REJECT : D_BOUNCE; |
---|
947 | } |
---|
948 | } |
---|
949 | if ($final_virus_destiny == D_DISCARD && c('warnvirussender') ) |
---|
950 | { $final_virus_destiny = D_BOUNCE } |
---|
951 | if ($final_spam_destiny == D_DISCARD && c('warnspamsender') ) |
---|
952 | { $final_spam_destiny = D_BOUNCE } |
---|
953 | if ($final_banned_destiny == D_DISCARD && c('warnbannedsender') ) |
---|
954 | { $final_banned_destiny = D_BOUNCE } |
---|
955 | if ($final_bad_header_destiny == D_DISCARD && c('warnbadhsender') ) |
---|
956 | { $final_bad_header_destiny = D_BOUNCE } |
---|
957 | } |
---|
958 | |
---|
959 | 1; |
---|
960 | |
---|
961 | # |
---|
962 | package Amavis::Lock; |
---|
963 | use strict; |
---|
964 | use re 'taint'; |
---|
965 | |
---|
966 | BEGIN { |
---|
967 | use Exporter (); |
---|
968 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
969 | $VERSION = '2.034'; |
---|
970 | @ISA = qw(Exporter); |
---|
971 | @EXPORT = qw(&lock &unlock); |
---|
972 | } |
---|
973 | use Fcntl qw(LOCK_SH LOCK_EX LOCK_UN); |
---|
974 | |
---|
975 | use subs @EXPORT; |
---|
976 | |
---|
977 | sub lock($) { |
---|
978 | my($file_handle) = @_; |
---|
979 | flock($file_handle, LOCK_EX) or die "Can't lock $file_handle: $!"; |
---|
980 | # NOTE: a lock is on a file, not on a file handle |
---|
981 | } |
---|
982 | |
---|
983 | sub unlock($) { |
---|
984 | my($file_handle) = @_; |
---|
985 | flock($file_handle, LOCK_UN) or die "Can't unlock $file_handle: $!"; |
---|
986 | } |
---|
987 | |
---|
988 | 1; |
---|
989 | |
---|
990 | # |
---|
991 | package Amavis::Log; |
---|
992 | use strict; |
---|
993 | use re 'taint'; |
---|
994 | |
---|
995 | BEGIN { |
---|
996 | use Exporter (); |
---|
997 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
998 | $VERSION = '2.034'; |
---|
999 | @ISA = qw(Exporter); |
---|
1000 | %EXPORT_TAGS = (); |
---|
1001 | @EXPORT = (); |
---|
1002 | @EXPORT_OK = qw(&init &write_log); |
---|
1003 | } |
---|
1004 | use subs @EXPORT_OK; |
---|
1005 | |
---|
1006 | use POSIX qw(locale_h strftime); |
---|
1007 | use Unix::Syslog qw(:macros :subs); |
---|
1008 | use IO::File (); |
---|
1009 | use File::Basename; |
---|
1010 | |
---|
1011 | BEGIN { |
---|
1012 | import Amavis::Conf qw(:platform $myversion $myhostname $daemon_user); |
---|
1013 | import Amavis::Lock; |
---|
1014 | } |
---|
1015 | |
---|
1016 | use vars qw($loghandle); # log file handle |
---|
1017 | use vars qw($myname); |
---|
1018 | use vars qw($syslog_facility $syslog_priority %syslog_priority); |
---|
1019 | use vars qw($log_to_stderr $do_syslog $logfile); |
---|
1020 | |
---|
1021 | sub init($$$$$) { |
---|
1022 | my($ident, $syslog_level); |
---|
1023 | ($ident, $log_to_stderr, $do_syslog, $syslog_level, $logfile) = @_; |
---|
1024 | |
---|
1025 | # Avoid taint bug in some versions of Perl (likely in 5.004, 5.005). |
---|
1026 | # The 5.6.1 is fine. To test, run this one-liner: |
---|
1027 | # perl -Te '"$0 $$"; $r=$$; print eval{kill(0,$$);1}?"OK\n":"BUG\n"' |
---|
1028 | $myname = $0; |
---|
1029 | # $myname = $1 if basename($0) =~ /^(.*)\z/; |
---|
1030 | |
---|
1031 | if ($syslog_level =~ /^\s*([a-z0-9]+)\.([a-z0-9]+)\s*\z/i) { |
---|
1032 | $syslog_facility = eval("LOG_\U$1"); |
---|
1033 | $syslog_priority = eval("LOG_\U$2"); |
---|
1034 | } |
---|
1035 | $syslog_facility = LOG_DAEMON if $syslog_facility !~ /^\d+\z/; |
---|
1036 | $syslog_priority = LOG_WARNING if $syslog_priority !~ /^\d+\z/; |
---|
1037 | if ($do_syslog) { |
---|
1038 | openlog($ident, LOG_PID, $syslog_facility); |
---|
1039 | } elsif ($logfile eq '') { |
---|
1040 | die 'No $LOGFILE is specified (and not logging via syslog)'; |
---|
1041 | } else { |
---|
1042 | $loghandle = IO::File->new($logfile,'>>') |
---|
1043 | or die "Failed to open log file $logfile: $!"; |
---|
1044 | $loghandle->autoflush(1); |
---|
1045 | my($uid) = $daemon_user=~/^(\d+)$/ ? $1 : (getpwnam($daemon_user))[2]; |
---|
1046 | if ($> == 0 && $uid) { |
---|
1047 | chown($uid,-1,$logfile) |
---|
1048 | or die "Can't chown logfile $logfile to $uid: $!"; |
---|
1049 | } |
---|
1050 | } |
---|
1051 | my($msg) = "starting. $myname at $myhostname $myversion"; |
---|
1052 | $msg .= ", eol=\"$eol\"" if $eol ne "\n"; |
---|
1053 | $msg .= ", Unicode aware" if $unicode_aware; |
---|
1054 | $msg .= ", LC_ALL=$ENV{LC_ALL}" if $ENV{LC_ALL} ne ''; |
---|
1055 | $msg .= ", LC_TYPE=$ENV{LC_TYPE}" if $ENV{LC_TYPE} ne ''; |
---|
1056 | $msg .= ", LC_CTYPE=$ENV{LC_CTYPE}" if $ENV{LC_CTYPE} ne ''; |
---|
1057 | $msg .= ", LANG=$ENV{LANG}" if $ENV{LANG} ne ''; |
---|
1058 | write_log(0, $msg, undef); |
---|
1059 | } |
---|
1060 | |
---|
1061 | # Log either to syslog or a file |
---|
1062 | sub write_log($$$) { |
---|
1063 | my($level,$errmsg,$am_id) = @_; |
---|
1064 | |
---|
1065 | my($old_locale) = setlocale(LC_TIME,"C"); # English dates required! |
---|
1066 | my($really_log_to_stderr) = $log_to_stderr || (!$do_syslog && !$loghandle); |
---|
1067 | my($prefix) = ''; |
---|
1068 | if ($really_log_to_stderr || !$do_syslog) { # create syslog-like prefix |
---|
1069 | $prefix = sprintf("%s %s %s[%s]: ", |
---|
1070 | strftime("%b %e %H:%M:%S", localtime), $myhostname, $myname, $$); |
---|
1071 | } |
---|
1072 | $am_id = !defined $am_id ? '' : "($am_id) "; |
---|
1073 | $errmsg = Amavis::Util::sanitize_str($errmsg); |
---|
1074 | # if (length($errmsg) > 2000) { # crop at some arbitrary limit (< LINE_MAX) |
---|
1075 | # $errmsg = substr($errmsg,0,2000) . "..."; |
---|
1076 | # } |
---|
1077 | if ($really_log_to_stderr) { |
---|
1078 | print STDERR $prefix, $am_id, $errmsg, $eol; |
---|
1079 | } elsif ($do_syslog) { |
---|
1080 | my($prio) = $syslog_priority; # never go below this priority level |
---|
1081 | # syslog priorities: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT, EMERG |
---|
1082 | if ($level <= -3) { $prio = LOG_CRIT if $prio > LOG_CRIT } |
---|
1083 | elsif ($level <= -2) { $prio = LOG_ERR if $prio > LOG_ERR } |
---|
1084 | elsif ($level <= -1) { $prio = LOG_WARNING if $prio > LOG_WARNING } |
---|
1085 | elsif ($level <= 0) { $prio = LOG_NOTICE if $prio > LOG_NOTICE } |
---|
1086 | elsif ($level <= 2) { $prio = LOG_INFO if $prio > LOG_INFO } |
---|
1087 | else { $prio = LOG_DEBUG if $prio > LOG_DEBUG } |
---|
1088 | my($pre) = ''; |
---|
1089 | my($logline_size) = 980; # less than (1023 - prefix) |
---|
1090 | while (length($am_id . $pre . $errmsg) > $logline_size) { |
---|
1091 | my($avail) = $logline_size - length($am_id . $pre . "..."); |
---|
1092 | syslog($prio, "%s", $am_id . $pre . substr($errmsg,0,$avail) . "..."); |
---|
1093 | $pre = "..."; |
---|
1094 | $errmsg = substr($errmsg, $avail); |
---|
1095 | } |
---|
1096 | syslog($prio, "%s", $am_id . $pre . $errmsg); |
---|
1097 | } else { |
---|
1098 | lock($loghandle); |
---|
1099 | seek($loghandle,0,2) or die "Can't position log file to its tail: $!"; |
---|
1100 | print $loghandle $prefix, $am_id, $errmsg, $eol; |
---|
1101 | unlock($loghandle); |
---|
1102 | } |
---|
1103 | setlocale(LC_TIME, $old_locale); |
---|
1104 | } |
---|
1105 | |
---|
1106 | 1; |
---|
1107 | |
---|
1108 | # |
---|
1109 | package Amavis::Timing; |
---|
1110 | use strict; |
---|
1111 | use re 'taint'; |
---|
1112 | |
---|
1113 | BEGIN { |
---|
1114 | use Exporter (); |
---|
1115 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
1116 | $VERSION = '2.034'; |
---|
1117 | @ISA = qw(Exporter); |
---|
1118 | %EXPORT_TAGS = (); |
---|
1119 | @EXPORT = (); |
---|
1120 | @EXPORT_OK = qw(&init §ion_time &report &get_time_so_far); |
---|
1121 | } |
---|
1122 | use subs @EXPORT_OK; |
---|
1123 | |
---|
1124 | use Time::HiRes (); |
---|
1125 | |
---|
1126 | use vars qw(@timing); |
---|
1127 | |
---|
1128 | # clear array @timing and enter start time |
---|
1129 | sub init() { |
---|
1130 | @timing = (); section_time('init'); |
---|
1131 | } |
---|
1132 | |
---|
1133 | # enter current time reading into array @timing |
---|
1134 | sub section_time($) { |
---|
1135 | push(@timing,shift,Time::HiRes::time); |
---|
1136 | } |
---|
1137 | |
---|
1138 | # returns a string - a report of elapsed time by section |
---|
1139 | sub report() { |
---|
1140 | section_time('rundown'); |
---|
1141 | my($notneeded, $t0) = (shift(@timing), shift(@timing)); |
---|
1142 | my($total) = $t0 <= 0 ? 0 : $timing[$#timing] - $t0; |
---|
1143 | if ($total < 0.0000001) { $total = 0.0000001 } |
---|
1144 | my(@sections); |
---|
1145 | while (@timing) { |
---|
1146 | my($section, $t) = (shift(@timing), shift(@timing)); |
---|
1147 | my($dt) = $t-$t0; |
---|
1148 | $dt = 0 if $dt < 0; # just in case (clock jumps) |
---|
1149 | my($dtp) = $dt > $total ? 100 : $dt*100.0/$total; |
---|
1150 | push(@sections, sprintf("%s: %.0f (%.0f%%)", $section, $dt*1000, $dtp)); |
---|
1151 | $t0 = $t; |
---|
1152 | } |
---|
1153 | sprintf("TIMING [total %.0f ms] - %s", $total * 1000, join(", ",@sections)); |
---|
1154 | } |
---|
1155 | |
---|
1156 | # returns value in seconds of elapsed time for processing of this mail so far |
---|
1157 | sub get_time_so_far() { |
---|
1158 | my($notneeded, $t0) = @timing; |
---|
1159 | my($total) = $t0 <= 0 ? 0 : Time::HiRes::time - $t0; |
---|
1160 | $total < 0 ? 0 : $total; |
---|
1161 | } |
---|
1162 | |
---|
1163 | use vars qw($t_was_busy $t_busy_cum $t_idle_cum $t0); |
---|
1164 | |
---|
1165 | sub idle_proc(@) { |
---|
1166 | my($t1) = Time::HiRes::time; |
---|
1167 | if (defined $t0) { |
---|
1168 | ($t_was_busy ? $t_busy_cum : $t_idle_cum) += $t1 - $t0; |
---|
1169 | Amavis::Util::ll(5) && Amavis::Util::do_log(5, |
---|
1170 | sprintf("idle_proc, @_: was %s, %.1f ms, total idle %.3f s, busy %.3f s", |
---|
1171 | $t_was_busy ? "busy" : "idle", 1000 * ($t1 - $t0), |
---|
1172 | $t_idle_cum, $t_busy_cum)); |
---|
1173 | } |
---|
1174 | $t0 = $t1; |
---|
1175 | } |
---|
1176 | |
---|
1177 | sub go_idle(@) { |
---|
1178 | if ($t_was_busy) { idle_proc(@_); $t_was_busy = 0 } |
---|
1179 | } |
---|
1180 | |
---|
1181 | sub go_busy(@) { |
---|
1182 | if (!$t_was_busy) { idle_proc(@_); $t_was_busy = 1 } |
---|
1183 | } |
---|
1184 | |
---|
1185 | sub report_load() { |
---|
1186 | return if $t_busy_cum + $t_idle_cum <= 0; |
---|
1187 | Amavis::Util::do_log(3, sprintf( |
---|
1188 | "load: %.0f %%, total idle %.3f s, busy %.3f s", |
---|
1189 | 100*$t_busy_cum / ($t_busy_cum + $t_idle_cum), $t_idle_cum, $t_busy_cum)); |
---|
1190 | } |
---|
1191 | |
---|
1192 | 1; |
---|
1193 | |
---|
1194 | # |
---|
1195 | package Amavis::Util; |
---|
1196 | use strict; |
---|
1197 | use re 'taint'; |
---|
1198 | |
---|
1199 | BEGIN { |
---|
1200 | use Exporter (); |
---|
1201 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
1202 | $VERSION = '2.034'; |
---|
1203 | @ISA = qw(Exporter); |
---|
1204 | %EXPORT_TAGS = (); |
---|
1205 | @EXPORT = (); |
---|
1206 | @EXPORT_OK = qw(&untaint &min &max &safe_encode &q_encode |
---|
1207 | &snmp_count &snmp_counters_init &snmp_counters_get |
---|
1208 | &am_id &new_am_id &ll &do_log &debug_oneshot |
---|
1209 | &retcode &exit_status_str &prolong_timer &sanitize_str |
---|
1210 | &strip_tempdir &rmdir_recursively |
---|
1211 | &read_text &read_l10n_templates &read_hash |
---|
1212 | &run_command &run_command_consumer); |
---|
1213 | } |
---|
1214 | use subs @EXPORT_OK; |
---|
1215 | use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED |
---|
1216 | WEXITSTATUS WTERMSIG WSTOPSIG); |
---|
1217 | use Errno qw(ENOENT EACCES); |
---|
1218 | use Digest::MD5; |
---|
1219 | # use Encode; # Perl 5.8 UTF-8 support |
---|
1220 | # use Encode::CN; # example: explicitly load Chinese module |
---|
1221 | |
---|
1222 | BEGIN { |
---|
1223 | import Amavis::Conf qw(:platform $DEBUG c cr ca); |
---|
1224 | import Amavis::Log qw(write_log); |
---|
1225 | import Amavis::Timing qw(section_time); |
---|
1226 | } |
---|
1227 | |
---|
1228 | # Return untainted copy of a string (argument can be a string or a string ref) |
---|
1229 | sub untaint($) { |
---|
1230 | no re 'taint'; |
---|
1231 | my($str); |
---|
1232 | local($1); # avoid Perl taint bug: tainted global $1 propagates taintedness |
---|
1233 | $str = $1 if (ref($_[0]) ? ${$_[0]} : $_[0]) =~ /^(.*)\z/s; |
---|
1234 | $str; |
---|
1235 | } |
---|
1236 | |
---|
1237 | # Returns the smallest number from the list, or undef |
---|
1238 | sub min(@) { |
---|
1239 | my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref |
---|
1240 | my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ < $m) } |
---|
1241 | $m; |
---|
1242 | } |
---|
1243 | |
---|
1244 | # Returns the largest number from the list, or undef |
---|
1245 | sub max(@) { |
---|
1246 | my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_; # accept list, or a list ref |
---|
1247 | my($m); for (@$r) { $m = $_ if defined $_ && (!defined $m || $_ > $m) } |
---|
1248 | $m; |
---|
1249 | } |
---|
1250 | |
---|
1251 | # A wrapper for Encode::encode, avoiding a bug in Perl 5.8.0 which causes |
---|
1252 | # Encode::encode to loop and fill memory when given a tainted string |
---|
1253 | sub safe_encode($$;$) { |
---|
1254 | if (!$unicode_aware) { $_[1] } # just return the second argument |
---|
1255 | else { |
---|
1256 | my($encoding,$str,$check) = @_; |
---|
1257 | $check = 0 if !defined($check); |
---|
1258 | # taintedness of the string, with UTF-8 flag unconditionally off |
---|
1259 | my($taint) = Encode::encode('ascii',substr($str,0,0)); |
---|
1260 | $taint . Encode::encode($encoding,untaint($str),$check); # preserve taint |
---|
1261 | } |
---|
1262 | } |
---|
1263 | |
---|
1264 | # Do the Q-encoding manually, the MIME::Words::encode_mimeword does not |
---|
1265 | # encode spaces and does not limit to 75 ch, which violates the RFC 2047 |
---|
1266 | sub q_encode($$$) { |
---|
1267 | my($octets,$encoding,$charset) = @_; |
---|
1268 | my($prefix) = '=?' . $charset . '?' . $encoding . '?'; |
---|
1269 | my($suffix) = '?='; local($1,$2,$3); |
---|
1270 | # FWS | utext (= NO-WS-CTL|rest of US-ASCII) |
---|
1271 | $octets =~ /^ ( [\001-\011\013\014\016-\177]* [ \t] )? (.*?) |
---|
1272 | ( [ \t] [\001-\011\013\014\016-\177]* )? \z/sx; |
---|
1273 | my($head,$rest,$tail) = ($1,$2,$3); |
---|
1274 | # Q-encode $rest according to RFC 2047 |
---|
1275 | # more restricted than =?_ so that it may be used in 'phrase' |
---|
1276 | $rest =~ s{([^ 0-9a-zA-Z!*/+-])}{sprintf('=%02X',ord($1))}egs; |
---|
1277 | $rest =~ tr/ /_/; # turn spaces into _ (rfc2047 allows it) |
---|
1278 | my($s) = $head; my($len) = 75 - (length($prefix)+length($suffix)) - 2; |
---|
1279 | while ($rest ne '') { |
---|
1280 | $s .= ' ' if $s !~ /[ \t]\z/; # encoded words must be separated by FWS |
---|
1281 | $rest =~ /^ ( .{0,$len} [^=] (?: [^=] | \z ) ) (.*) \z/sx; |
---|
1282 | $s .= $prefix.$1.$suffix; $rest = $2; |
---|
1283 | } |
---|
1284 | $s.$tail; |
---|
1285 | } |
---|
1286 | |
---|
1287 | # Set or get Amavis internal message id. |
---|
1288 | # This message id performs a similar function to queue-id in MTA responses. |
---|
1289 | # It may only be used in generating text part of SMTP responses, |
---|
1290 | # or in generating log entries. |
---|
1291 | use vars qw($amavis_task_id); # internal message id (accessible via &am_id) |
---|
1292 | |
---|
1293 | sub am_id(;$) { |
---|
1294 | if (@_) { # set, if argument present |
---|
1295 | $amavis_task_id = shift; |
---|
1296 | $0 = "amavisd ($amavis_task_id)"; |
---|
1297 | } |
---|
1298 | $amavis_task_id; # return current value |
---|
1299 | } |
---|
1300 | |
---|
1301 | sub new_am_id($;$$) { |
---|
1302 | my($str, $cnt, $seq) = @_; |
---|
1303 | my($id); |
---|
1304 | # my($ctx) = Digest::MD5->new; # 128 bits (32 hex digits) |
---|
1305 | # $ctx->add($str.$$); |
---|
1306 | # $id = substr($ctx->hexdigest, 0, 6); |
---|
1307 | $id = defined $str ? $str : sprintf("%05d", $$); |
---|
1308 | $id .= sprintf("-%02d", $cnt) if defined $cnt; |
---|
1309 | $id .= "-$seq" if $seq > 1; |
---|
1310 | am_id($id); |
---|
1311 | } |
---|
1312 | |
---|
1313 | use vars qw(@counter_names); |
---|
1314 | # elements may be counter names (increment is 1), or pairs: [name,increment] |
---|
1315 | sub snmp_counters_init() { @counter_names = () } |
---|
1316 | sub snmp_count(@) { push(@counter_names, @_) } |
---|
1317 | sub snmp_counters_get() { \@counter_names } |
---|
1318 | |
---|
1319 | use vars qw($debug_oneshot); |
---|
1320 | sub debug_oneshot(;$$) { |
---|
1321 | if (@_) { |
---|
1322 | my($new_debug_oneshot) = shift; |
---|
1323 | if (($new_debug_oneshot ? 1 : 0) != ($debug_oneshot ? 1 : 0)) { |
---|
1324 | do_log(0, "DEBUG_ONESHOT: TURNED ".($new_debug_oneshot ? "ON" : "OFF")); |
---|
1325 | do_log(0, shift) if @_; # caller-provided extra log entry, usually |
---|
1326 | # the one that caused debug_oneshot call |
---|
1327 | } |
---|
1328 | $debug_oneshot = $new_debug_oneshot; |
---|
1329 | } |
---|
1330 | $debug_oneshot; |
---|
1331 | } |
---|
1332 | |
---|
1333 | # is a message log level below the current log level? |
---|
1334 | sub ll($) { |
---|
1335 | my($level) = @_; |
---|
1336 | $level = 0 if $level > 0 && ($DEBUG || $debug_oneshot); |
---|
1337 | $level <= c('log_level'); |
---|
1338 | } |
---|
1339 | |
---|
1340 | # write log entry |
---|
1341 | sub do_log($$) { |
---|
1342 | my($level, $errmsg) = @_; |
---|
1343 | if (ll($level)) { |
---|
1344 | $level = 0 if $level > 0 && ($DEBUG || $debug_oneshot); |
---|
1345 | write_log($level, $errmsg, am_id()); |
---|
1346 | } |
---|
1347 | } |
---|
1348 | |
---|
1349 | sub retcode($) { # (this subroutine is being phased out) |
---|
1350 | my $code = shift; |
---|
1351 | return WEXITSTATUS($code) if WIFEXITED($code); |
---|
1352 | return 128 + WTERMSIG($code) if WIFSIGNALED($code); |
---|
1353 | return 255; |
---|
1354 | } |
---|
1355 | |
---|
1356 | sub exit_status_str($;$) { |
---|
1357 | my($stat,$err) = @_; my($str); |
---|
1358 | if (WIFEXITED($stat)) { |
---|
1359 | $str = sprintf("exit %d", WEXITSTATUS($stat)); |
---|
1360 | } elsif (WIFSTOPPED($stat)) { |
---|
1361 | $str = sprintf("stopped, signal %d", WSTOPSIG($stat)); |
---|
1362 | } else { |
---|
1363 | $str = sprintf("DIED on signal %d (%04x)", WTERMSIG($stat),$stat); |
---|
1364 | } |
---|
1365 | $str .= ', '.$err if $err ne ''; |
---|
1366 | $str; |
---|
1367 | } |
---|
1368 | |
---|
1369 | sub prolong_timer($;$) { |
---|
1370 | my($which_section, $child_remaining_time) = @_; |
---|
1371 | if (!defined($child_remaining_time)) { |
---|
1372 | $child_remaining_time = alarm(0); # check how much time is left |
---|
1373 | } |
---|
1374 | do_log(4, "prolong_timer after $which_section: " |
---|
1375 | . "remaining time = $child_remaining_time s"); |
---|
1376 | $child_remaining_time = 60 if $child_remaining_time < 60; |
---|
1377 | alarm($child_remaining_time); # restart/prolong the timer |
---|
1378 | } |
---|
1379 | |
---|
1380 | # Mostly for debugging and reporting purposes: |
---|
1381 | # Convert nonprintable characters in the argument |
---|
1382 | # to \[rnftbe], or \octal code, and '\' to '\\', |
---|
1383 | # and Unicode characters to \x{xxxx}, returning the sanitized string. |
---|
1384 | sub sanitize_str { |
---|
1385 | my($str, $keep_eol) = @_; |
---|
1386 | my(%map) = ("\r" => '\\r', "\n" => '\\n', "\f" => '\\f', "\t" => '\\t', |
---|
1387 | "\b" => '\\b', "\e" => '\\e', "\\" => '\\\\'); |
---|
1388 | if ($keep_eol) { |
---|
1389 | $str =~ s/([^\012\040-\133\135-\176])/ # and \240-\376 ? |
---|
1390 | exists($map{$1}) ? $map{$1} : |
---|
1391 | sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg; |
---|
1392 | } else { |
---|
1393 | $str =~ s/([^\040-\133\135-\176])/ # and \240-\376 ? |
---|
1394 | exists($map{$1}) ? $map{$1} : |
---|
1395 | sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg; |
---|
1396 | } |
---|
1397 | $str; |
---|
1398 | } |
---|
1399 | |
---|
1400 | # Checks tempdir after being cleaned. |
---|
1401 | # It may only contain subdirectory 'parts' and file email.txt, nothing else. |
---|
1402 | # |
---|
1403 | sub check_tempdir($) { |
---|
1404 | my($dir) = shift; |
---|
1405 | local(*DIR); my($f); |
---|
1406 | opendir(DIR,$dir) or die "Can't open directory $dir: $!"; |
---|
1407 | while (defined($f = readdir(DIR))) { |
---|
1408 | if (!-d ("$dir/$f")) { |
---|
1409 | die "Unexpected file $dir/$f" if $f ne 'email.txt'; |
---|
1410 | } elsif ($f eq '.' || $f eq '..' || $f eq 'parts') { |
---|
1411 | } else { |
---|
1412 | die "Unexpected subdirectory $dir/$f"; |
---|
1413 | } |
---|
1414 | } |
---|
1415 | closedir(DIR) or die "Can't close directory $dir: $!"; |
---|
1416 | 1; |
---|
1417 | } |
---|
1418 | |
---|
1419 | # Remove all files and subdirectories from the temporary directory, leaving |
---|
1420 | # only the directory itself, file email.txt, and empty subdirectory ./parts . |
---|
1421 | # Leaving directories for reuse represents an important saving in time, |
---|
1422 | # as directory creation + deletion is quite an expensive operation, |
---|
1423 | # requiring atomic file system operation, including flushing buffers to disk. |
---|
1424 | # |
---|
1425 | sub strip_tempdir($) { |
---|
1426 | my($dir) = shift; |
---|
1427 | do_log(4, "strip_tempdir: $dir"); |
---|
1428 | my($errn) = lstat("$dir/parts") ? 0 : 0+$!; |
---|
1429 | if ($errn != ENOENT) { |
---|
1430 | if ( -l _) { die "strip_tempdir: $dir/parts is a symbolic link" } |
---|
1431 | elsif (!-d _) { die "strip_tempdir: $dir/parts is not a directory" } |
---|
1432 | rmdir_recursively("$dir/parts", 1); |
---|
1433 | } |
---|
1434 | # All done. Check for any remains in the top directory just in case |
---|
1435 | check_tempdir($dir); |
---|
1436 | 1; |
---|
1437 | } |
---|
1438 | |
---|
1439 | # |
---|
1440 | # Removes a directory, along with its contents |
---|
1441 | sub rmdir_recursively($;$); # prototype |
---|
1442 | sub rmdir_recursively($;$) { |
---|
1443 | my($dir, $exclude_itself) = @_; |
---|
1444 | local(*DIR); my($errn); my($cnt) = 0; my($dir_is_open) = 0; |
---|
1445 | do_log(4,"rmdir_recursively: $dir, excl=$exclude_itself"); |
---|
1446 | eval { |
---|
1447 | $errn = opendir(DIR,$dir) ? 0 : 0+$!; |
---|
1448 | if ($errn == ENOENT) { die "Directory $dir does not exist," } |
---|
1449 | elsif ($errn == EACCES) { # relax protection on directory, then try again |
---|
1450 | do_log(3,"rmdir_recursively: enabling read access to directory $dir"); |
---|
1451 | chmod(0750,$dir) or die "Can't change protection-1 on dir $dir: $!"; |
---|
1452 | $errn = opendir(DIR,$dir) ? 0 : 0+$!; # try again |
---|
1453 | } |
---|
1454 | if ($errn) { die "Can't open directory $dir: $!" } |
---|
1455 | $dir_is_open = 1; my($f); |
---|
1456 | while (defined($f = readdir(DIR))) { |
---|
1457 | my($fname) = "$dir/$f"; |
---|
1458 | $errn = lstat($fname) ? 0 : 0+$!; |
---|
1459 | if ($errn == ENOENT) { die "File \"$fname\" does not exist," } |
---|
1460 | elsif ($errn == EACCES) { # relax protection on the directory and retry |
---|
1461 | do_log(3,"rmdir_recursively: enabling access to files in dir $dir"); |
---|
1462 | chmod(0750,$dir) or die "Can't change protection-2 on dir $dir: $!"; |
---|
1463 | $errn = lstat($fname) ? 0 : 0+$!; # try again |
---|
1464 | } |
---|
1465 | if ($errn) { die "File \"$fname\" inaccessible: $!" } |
---|
1466 | next if ($f eq '.' || $f eq '..') && -d _; |
---|
1467 | if (-d _) { rmdir_recursively(untaint($fname), 0) } |
---|
1468 | else { |
---|
1469 | $cnt++; |
---|
1470 | if (unlink(untaint($fname))) { # ok |
---|
1471 | } else { # relax protection on the directory, then try again |
---|
1472 | do_log(3,"rmdir_recursively: enabling write access to dir $dir"); |
---|
1473 | my($what) = -l _ ? 'symlink' :-d _ ? 'directory' :'non-regular file'; |
---|
1474 | chmod(0750,$dir) or die "Can't change protection-3 on dir $dir: $!"; |
---|
1475 | unlink(untaint($fname)) or die "Can't remove $what $fname: $!"; |
---|
1476 | } |
---|
1477 | } |
---|
1478 | } |
---|
1479 | }; |
---|
1480 | if ($@ eq '') { |
---|
1481 | closedir(DIR) or die "Can't close directory $dir: $!" if $dir_is_open; |
---|
1482 | } else { |
---|
1483 | closedir(DIR) if $dir_is_open; # ignoring status |
---|
1484 | die "rmdir_recursively: $@\n"; |
---|
1485 | } |
---|
1486 | section_time("unlink-$cnt-files"); |
---|
1487 | if (!$exclude_itself) { |
---|
1488 | rmdir($dir) or die "rmdir_recursively: Can't remove directory $dir: $!"; |
---|
1489 | section_time('rmdir'); |
---|
1490 | } |
---|
1491 | 1; |
---|
1492 | } |
---|
1493 | |
---|
1494 | # read a multiline string from file - may be called from amavisd.conf |
---|
1495 | sub read_text($;$) { |
---|
1496 | my($filename, $encoding) = @_; |
---|
1497 | my($inp) = IO::File->new; |
---|
1498 | $inp->open($filename,'<') or die "Can't open file $filename for reading: $!"; |
---|
1499 | if ($unicode_aware && $encoding ne '') { |
---|
1500 | binmode($inp, ":encoding($encoding)") |
---|
1501 | or die "Can't set :encoding($encoding) on file $filename: $!"; |
---|
1502 | } |
---|
1503 | my($str) = ''; # must not be undef, work around a Perl UTF8 bug |
---|
1504 | while (<$inp>) { $str .= $_ } |
---|
1505 | $inp->close or die "Can't close $filename: $!"; |
---|
1506 | $str; |
---|
1507 | } |
---|
1508 | |
---|
1509 | # attempt to read all user-visible replies from a l10n dir |
---|
1510 | # This function auto-fills $notify_sender_templ, $notify_virus_sender_templ, |
---|
1511 | # $notify_virus_admin_templ, $notify_virus_recips_templ, |
---|
1512 | # $notify_spam_sender_templ and $notify_spam_admin_templ from files named |
---|
1513 | # template-dsn.txt, template-virus-sender.txt, template-virus-admin.txt, |
---|
1514 | # template-virus-recipient.txt, template-spam-sender.txt, |
---|
1515 | # template-spam-admin.txt. If this is available, it uses the charset |
---|
1516 | # file to do automatic charset conversion. Used by the Debian distribution. |
---|
1517 | sub read_l10n_templates($;$) { |
---|
1518 | my($dir) = @_; |
---|
1519 | if (@_ > 1) # compatibility with Debian |
---|
1520 | { my($l10nlang, $l10nbase) = @_; $dir = "$l10nbase/$l10nlang" } |
---|
1521 | my($file_chset) = Amavis::Util::read_text("$dir/charset"); |
---|
1522 | if ($file_chset =~ m{^(?:#[^\n]*\n)*([^./\n\s]+)(\s*[#\n].*)?$}s) { |
---|
1523 | $file_chset = untaint($1); |
---|
1524 | } else { |
---|
1525 | die "Invalid charset $file_chset\n"; |
---|
1526 | } |
---|
1527 | $Amavis::Conf::notify_sender_templ = |
---|
1528 | Amavis::Util::read_text("$dir/template-dsn.txt", $file_chset); |
---|
1529 | $Amavis::Conf::notify_virus_sender_templ = |
---|
1530 | Amavis::Util::read_text("$dir/template-virus-sender.txt", $file_chset); |
---|
1531 | $Amavis::Conf::notify_virus_admin_templ = |
---|
1532 | Amavis::Util::read_text("$dir/template-virus-admin.txt", $file_chset); |
---|
1533 | $Amavis::Conf::notify_virus_recips_templ = |
---|
1534 | Amavis::Util::read_text("$dir/template-virus-recipient.txt", $file_chset); |
---|
1535 | $Amavis::Conf::notify_spam_sender_templ = |
---|
1536 | Amavis::Util::read_text("$dir/template-spam-sender.txt", $file_chset); |
---|
1537 | $Amavis::Conf::notify_spam_admin_templ = |
---|
1538 | Amavis::Util::read_text("$dir/template-spam-admin.txt", $file_chset); |
---|
1539 | } |
---|
1540 | |
---|
1541 | #use CDB_File; |
---|
1542 | #sub tie_hash($$) { |
---|
1543 | # my($hashref, $filename) = @_; |
---|
1544 | # CDB_File::create(%$hashref, $filename, "$filename.tmp$$") |
---|
1545 | # or die "Can't create cdb $filename: $!"; |
---|
1546 | # my($cdb) = tie(%$hashref,'CDB_File',$filename) |
---|
1547 | # or die "Tie to $filename failed: $!"; |
---|
1548 | # $hashref; |
---|
1549 | #} |
---|
1550 | |
---|
1551 | # read a lookup hash from file - may be called from amavisd.conf . |
---|
1552 | # |
---|
1553 | # Format: one key per line, anything from '#' to the end of line |
---|
1554 | # is considered a comment, but '#' within correctly quoted rfc2821 |
---|
1555 | # addresses is not treated as a comment (e.g. a hash sign within |
---|
1556 | # "strange # \"foo\" address"@example.com is part of the string). |
---|
1557 | # Lines may contain a pair: key value, separated by whitespace, or key only, |
---|
1558 | # in which case a value 1 is implied. Trailing whitespace is discarded, |
---|
1559 | # empty lines (containing only whitespace and comment) are ignored. |
---|
1560 | # Addresses (lefthand-side) are converted from rfc2821-quoted form |
---|
1561 | # into internal (raw) form and inserted as keys into a given hash. |
---|
1562 | # NOTE: the format is partly compatible with Postfix maps (not aliases): |
---|
1563 | # no continuation lines are honoured, Postfix maps do not allow |
---|
1564 | # rfc2821-quoted addresses containing whitespace, Postfix only allow |
---|
1565 | # comments starting at the beginning of the line. |
---|
1566 | # |
---|
1567 | # The $hashref argument is returned for convenience, so that one can do |
---|
1568 | # for example: |
---|
1569 | # $per_recip_whitelist_sender_lookup_tables = { |
---|
1570 | # '.my1.example.com' => read_hash({},'/var/amavis/my1-example-com.wl'), |
---|
1571 | # '.my2.example.com' => read_hash({},'/var/amavis/my2-example-com.wl') } |
---|
1572 | # or even simpler: |
---|
1573 | # $per_recip_whitelist_sender_lookup_tables = { |
---|
1574 | # '.my1.example.com' => read_hash('/var/amavis/my1-example-com.wl'), |
---|
1575 | # '.my2.example.com' => read_hash('/var/amavis/my2-example-com.wl') } |
---|
1576 | # |
---|
1577 | sub read_hash(@) { |
---|
1578 | unshift(@_,{}) if !ref $_[0]; # first argument is optional, defaults to {} |
---|
1579 | my($hashref, $filename, $keep_case) = @_; |
---|
1580 | my($lpcs) = c('localpart_is_case_sensitive'); |
---|
1581 | my($inp) = IO::File->new; |
---|
1582 | $inp->open($filename,'<') or die "Can't open file $filename for reading: $!"; |
---|
1583 | while (<$inp>) { # carefully handle comments, '#' within "" does not count |
---|
1584 | chomp; |
---|
1585 | my($lhs) = ''; my($rhs) = ''; my($at_rhs) = 0; |
---|
1586 | for my $t ( /\G ( " (?: \\. | [^"\\] )* " | |
---|
1587 | [^#" \t]+ | [ \t]+ | . )/gcsx) { |
---|
1588 | last if $t eq '#'; |
---|
1589 | if (!$at_rhs && $t =~ /^[ \t]+\z/) { $at_rhs = 1 } |
---|
1590 | else { ($at_rhs ? $rhs : $lhs) .= $t } |
---|
1591 | } |
---|
1592 | $rhs =~ s/[ \t]+\z//; # trim trailing whitespace |
---|
1593 | next if $lhs eq '' && $rhs eq ''; |
---|
1594 | my($addr) = Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($lhs); |
---|
1595 | my($localpart,$domain) = Amavis::rfc2821_2822_Tools::split_address($addr); |
---|
1596 | $localpart = lc($localpart) if !$lpcs; |
---|
1597 | $addr = $localpart . lc($domain); |
---|
1598 | $hashref->{$addr} = $rhs eq '' ? 1 : $rhs; |
---|
1599 | # do_log(5, "read_hash: address: <$addr>: ".$hashref->{$addr}); |
---|
1600 | } |
---|
1601 | $inp->close or die "Can't close $filename: $!"; |
---|
1602 | $hashref; |
---|
1603 | } |
---|
1604 | |
---|
1605 | # Run specified command as a subprocess (like qx operator, but more careful |
---|
1606 | # with error reporting and cancels :utf8 mode). Return a file handle open |
---|
1607 | # for reading from the subprocess. Use IO::Handle to ensure the subprocess |
---|
1608 | # will be automatically reclaimed in case of failure. |
---|
1609 | # |
---|
1610 | sub run_command($$@) { |
---|
1611 | my($stdin_from, $stderr_to, $cmd, @args) = @_; |
---|
1612 | my($cmd_text) = join(' ', $cmd, @args); |
---|
1613 | $stdin_from = '/dev/null' if $stdin_from eq ''; |
---|
1614 | my($msg) = join(' ', $cmd, @args, "<$stdin_from"); |
---|
1615 | $msg .= " 2>$stderr_to" if $stderr_to ne ''; |
---|
1616 | my($pid); my($proc_fh) = IO::File->new; |
---|
1617 | eval { $pid = $proc_fh->open('-|') }; # fork, catching errors |
---|
1618 | if ($@ ne '') { chomp($@); die "run_command (open pipe): $@" } |
---|
1619 | defined($pid) or die "run_command: can't fork: $!"; |
---|
1620 | if (!$pid) { # child |
---|
1621 | eval { # must not use die in forked process, or we end up with |
---|
1622 | # two running daemons! Close unneeded files. |
---|
1623 | # use Devel::Symdump (); |
---|
1624 | # my($dumpobj) = Devel::Symdump->rnew; |
---|
1625 | # for my $k ($dumpobj->ios) { |
---|
1626 | # no strict 'refs'; |
---|
1627 | # my($fn) = fileno($k); |
---|
1628 | # if ($fn == 1 || $fn == 2) { |
---|
1629 | # do_log(2,sprintf("KEEPING %s, fileno=%s", $k, $fn)); |
---|
1630 | # } else { |
---|
1631 | # $! = undef; close(*{$k}{IO}) and do_log(0, "DID CLOSE $k (fileno=$fn)"); |
---|
1632 | # } |
---|
1633 | # } |
---|
1634 | close(STDIN) or die "Can't close STDIN: $!"; |
---|
1635 | close(main::stdin) or die "Can't close main::stdin: $!"; |
---|
1636 | open(STDIN, "<$stdin_from") |
---|
1637 | or die "Can't reopen STDIN on $stdin_from: $!"; |
---|
1638 | fileno(STDIN) == 0 or die ("run_command: STDIN not fd0: ".fileno(STDIN)); |
---|
1639 | if ($stderr_to ne '') { |
---|
1640 | close(STDERR) or die "Can't close STDERR: $!"; |
---|
1641 | open(STDERR, ">$stderr_to") |
---|
1642 | or die "Can't open STDERR to $stderr_to: $!"; |
---|
1643 | fileno(STDERR) == 2 |
---|
1644 | or die ("run_command: STDERR not fd2: ".fileno(STDERR)); |
---|
1645 | } |
---|
1646 | # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC |
---|
1647 | { no warnings; |
---|
1648 | exec {$cmd} ($cmd,@args) or die "Failed to exec $cmd_text: $!"; |
---|
1649 | } |
---|
1650 | }; |
---|
1651 | chomp($@); |
---|
1652 | do_log(-2,"run_command: child process [$$]: $@\n"); |
---|
1653 | { no warnings; |
---|
1654 | kill('KILL',$$) # must not exit, we have to avoid DESTROY handlers |
---|
1655 | or do_log(-3,"run_command: TROUBLE - Panic1, can't die: $!"); |
---|
1656 | # still kicking? die! |
---|
1657 | exec('/usr/bin/false'); exec('/bin/false'); exec('false'); exec('true'); |
---|
1658 | do_log(-3,"run_command: TROUBLE - Panic2, can't die"); |
---|
1659 | exit 1; # better safe than sorry |
---|
1660 | # NOTREACHED |
---|
1661 | } |
---|
1662 | } |
---|
1663 | # parent |
---|
1664 | do_log(5,"run_command: [$pid] $msg"); |
---|
1665 | binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1 |
---|
1666 | ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID |
---|
1667 | } |
---|
1668 | |
---|
1669 | # Run specified command as a subprocess. Return a file handle open for |
---|
1670 | # WRITING to the subprocess. Use IO::Handle to ensure the subprocess |
---|
1671 | # will be automatically reclaimed in case of failure. |
---|
1672 | # |
---|
1673 | sub run_command_consumer($$@) { |
---|
1674 | my($stdout_to, $stderr_to, $cmd, @args) = @_; |
---|
1675 | my($cmd_text) = join(' ', $cmd, @args); |
---|
1676 | $stdout_to = '/dev/null' if $stdout_to eq ''; |
---|
1677 | my($msg) = join(' ', $cmd, @args, ">$stdout_to"); |
---|
1678 | $msg .= " 2>$stderr_to" if $stderr_to ne ''; |
---|
1679 | my($pid); my($proc_fh) = IO::File->new; |
---|
1680 | eval { $pid = $proc_fh->open('|-') }; # fork, catching errors |
---|
1681 | if ($@ ne '') { chomp($@); die "run_command_consumer (open pipe): $@" } |
---|
1682 | defined($pid) or die "run_command_consumer: can't fork: $!"; |
---|
1683 | if (!$pid) { # child |
---|
1684 | eval { # must not use die in forked process, or we end up with |
---|
1685 | # two running daemons! Close unneeded files. |
---|
1686 | close(main::stderr) or die "Can't close main::stderr: $!"; |
---|
1687 | close(main::stdout) or die "Can't close main::stdout: $!"; |
---|
1688 | close(main::STDOUT) or die "Can't close main::STDOUT: $!"; |
---|
1689 | open(STDOUT, ">$stdout_to") |
---|
1690 | or die "Can't reopen STDOUT on $stdout_to: $!"; |
---|
1691 | fileno(STDOUT) == 1 |
---|
1692 | or die ("run_command_consumer: STDOUT not fd1: ".fileno(STDOUT)); |
---|
1693 | if ($stderr_to ne '') { |
---|
1694 | close(STDERR) or die "Can't close STDERR: $!"; |
---|
1695 | open(STDERR, ">$stderr_to") |
---|
1696 | or die "Can't open STDERR to $stderr_to: $!"; |
---|
1697 | fileno(STDERR) == 2 |
---|
1698 | or die ("run_command_consumer: STDERR not fd2: ".fileno(STDERR)); |
---|
1699 | } |
---|
1700 | # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC |
---|
1701 | { no warnings; |
---|
1702 | exec {$cmd} ($cmd,@args) or die "Failed to exec $cmd_text: $!"; |
---|
1703 | } |
---|
1704 | }; |
---|
1705 | chomp($@); |
---|
1706 | do_log(-2,"run_command_consumer: child process [$$]: $@\n"); |
---|
1707 | { no warnings; |
---|
1708 | kill('KILL',$$) # must not exit, we have to avoid DESTROY handlers |
---|
1709 | or do_log(-3,"run_command_consumer: TROUBLE - Panic1, can't die: $!"); |
---|
1710 | # still kicking? die! |
---|
1711 | exec('/usr/bin/false'); exec('/bin/false'); exec('false'); exec('true'); |
---|
1712 | do_log(-3,"run_command_consumer: TROUBLE - Panic2, can't die"); |
---|
1713 | exit 1; # better safe than sorry |
---|
1714 | # NOTREACHED |
---|
1715 | } |
---|
1716 | } |
---|
1717 | # parent |
---|
1718 | do_log(5,"run_command_consumer: [$pid] $msg"); |
---|
1719 | binmode($proc_fh) or die "Can't set pipe to binmode: $!"; # dflt Perl 5.8.1 |
---|
1720 | ($proc_fh, $pid); # return pipe file handle to the subprocess and its PID |
---|
1721 | } |
---|
1722 | |
---|
1723 | 1; |
---|
1724 | |
---|
1725 | # |
---|
1726 | package Amavis::rfc2821_2822_Tools; |
---|
1727 | use strict; |
---|
1728 | use re 'taint'; |
---|
1729 | |
---|
1730 | BEGIN { |
---|
1731 | use Exporter (); |
---|
1732 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
1733 | $VERSION = '2.034'; |
---|
1734 | @ISA = qw(Exporter); |
---|
1735 | %EXPORT_TAGS = (); |
---|
1736 | @EXPORT = qw( |
---|
1737 | &iso8601_timestamp &iso8601_utc_timestamp &rfc2822_timestamp |
---|
1738 | &received_line &parse_received |
---|
1739 | &fish_out_ip_from_received &split_address &split_localpart &make_query_keys |
---|
1740 | "e_rfc2821_local &qquote_rfc2821_local &unquote_rfc2821_local |
---|
1741 | &one_response_for_all |
---|
1742 | &EX_OK &EX_NOUSER &EX_UNAVAILABLE &EX_TEMPFAIL &EX_NOPERM); |
---|
1743 | } |
---|
1744 | |
---|
1745 | use subs @EXPORT; |
---|
1746 | |
---|
1747 | use POSIX qw(locale_h strftime); |
---|
1748 | |
---|
1749 | BEGIN { |
---|
1750 | eval { require 'sysexits.ph' }; # try to use the installed version |
---|
1751 | # define the most important constants if undefined |
---|
1752 | do { sub EX_OK() {0} } unless defined(&EX_OK); |
---|
1753 | do { sub EX_NOUSER() {67} } unless defined(&EX_NOUSER); |
---|
1754 | do { sub EX_UNAVAILABLE() {69} } unless defined(&EX_UNAVAILABLE); |
---|
1755 | do { sub EX_TEMPFAIL() {75} } unless defined(&EX_TEMPFAIL); |
---|
1756 | do { sub EX_NOPERM() {77} } unless defined(&EX_NOPERM); |
---|
1757 | } |
---|
1758 | |
---|
1759 | BEGIN { |
---|
1760 | import Amavis::Conf qw(:platform $myhostname c cr ca); |
---|
1761 | import Amavis::Util qw(ll do_log); |
---|
1762 | } |
---|
1763 | |
---|
1764 | # Given a Unix time, return the local time zone offset at that time |
---|
1765 | # as a string +HHMM or -HHMM, appropriate for the RFC2822 date format. |
---|
1766 | # Works also for non-full-hour zone offsets, and on systems where strftime |
---|
1767 | # can not return TZ offset as a number; (c) Mark Martinec, GPL |
---|
1768 | # |
---|
1769 | sub get_zone_offset($) { |
---|
1770 | my($t) = @_; |
---|
1771 | my($d) = 0; # local zone offset in seconds |
---|
1772 | for (1..3) { # match the date (with a safety loop limit just in case) |
---|
1773 | my($r) = sprintf("%04d%02d%02d", (localtime($t))[5, 4, 3]) cmp |
---|
1774 | sprintf("%04d%02d%02d", (gmtime($t + $d))[5, 4, 3]); |
---|
1775 | if ($r == 0) { last } else { $d += $r * 24 * 3600 } |
---|
1776 | } |
---|
1777 | my($sl,$su) = (0,0); |
---|
1778 | for ((localtime($t))[2,1,0]) { $sl = $sl * 60 + $_ } |
---|
1779 | for ((gmtime($t + $d))[2,1,0]) { $su = $su * 60 + $_ } |
---|
1780 | $d += $sl - $su; # add HMS difference (in seconds) |
---|
1781 | my($sign) = $d >= 0 ? '+' : '-'; |
---|
1782 | $d = -$d if $d < 0; |
---|
1783 | $d = int(($d + 30) / 60.0); # give minutes, rounded |
---|
1784 | sprintf("%s%02d%02d", $sign, int($d / 60), $d % 60); |
---|
1785 | } |
---|
1786 | |
---|
1787 | # Given a Unix time (seconds since 1970-01-01T00:00Z), |
---|
1788 | # provide date-time timestamp (local time) as specified in ISO 8601 (EN 28601) |
---|
1789 | sub iso8601_timestamp($;$) { |
---|
1790 | my($t,$suppress_zone) = @_; |
---|
1791 | # can't use %z because some systems do not support it (is treated as %Z) |
---|
1792 | my($s) = strftime("%Y%m%dT%H%M%S", localtime($t)); |
---|
1793 | $s .= get_zone_offset($t) unless $suppress_zone; |
---|
1794 | $s; |
---|
1795 | } |
---|
1796 | |
---|
1797 | # Given a Unix time (seconds since 1970-01-01T00:00Z), |
---|
1798 | # provide date-time timestamp (UTC) as specified in ISO 8601 (EN 28601) |
---|
1799 | sub iso8601_utc_timestamp($;$) { |
---|
1800 | my($t,$suppress_zone) = @_; |
---|
1801 | my($s) = strftime("%Y%m%dT%H%M%S", gmtime($t)); |
---|
1802 | $s .= 'Z' unless $suppress_zone; |
---|
1803 | $s; |
---|
1804 | } |
---|
1805 | |
---|
1806 | # Given a Unix time, provide date-time timestamp as specified in RFC 2822 |
---|
1807 | # (local time), to be used in headers such as 'Date:' and 'Received:' |
---|
1808 | # |
---|
1809 | sub rfc2822_timestamp($) { |
---|
1810 | my($t) = @_; |
---|
1811 | my(@lt) = localtime($t); |
---|
1812 | # can't use %z because some systems do not support it (is treated as %Z) |
---|
1813 | my($old_locale) = setlocale(LC_TIME,"C"); # English dates required! |
---|
1814 | my($zone_name) = strftime("%Z",@lt); |
---|
1815 | my($s) = strftime("%a, %e %b %Y %H:%M:%S ", @lt); |
---|
1816 | $s .= get_zone_offset($t); |
---|
1817 | $s .= " (" . $zone_name . ")" if $zone_name !~ /^\s*\z/; |
---|
1818 | setlocale(LC_TIME, $old_locale); # restore the locale |
---|
1819 | $s; |
---|
1820 | } |
---|
1821 | |
---|
1822 | sub received_line($$$$) { |
---|
1823 | my($conn, $msginfo, $id, $folded) = @_; |
---|
1824 | my($smtp_proto, $recips) = ($conn->smtp_proto, $msginfo->recips); |
---|
1825 | my($client_ip) = $conn->client_ip; |
---|
1826 | if ($client_ip =~ /:/ && $client_ip !~ /^IPv6:/i) { |
---|
1827 | $client_ip = 'IPv6:' . $client_ip; |
---|
1828 | } |
---|
1829 | my($s) = sprintf("from %s%s\n by %s%s (amavisd-new, %s)", |
---|
1830 | ($conn->smtp_helo eq '' ? 'unknown' : $conn->smtp_helo), |
---|
1831 | ($client_ip eq '' ? '' : " ([$client_ip])"), |
---|
1832 | c('localhost_name'), |
---|
1833 | ($conn->socket_ip eq '' ? '' |
---|
1834 | : sprintf(" (%s [%s])", $myhostname, $conn->socket_ip) ), |
---|
1835 | ($conn->socket_port eq '' ? 'unix socket' : "port ".$conn->socket_port) ); |
---|
1836 | # must not use proto name QMQPqq in 'with' |
---|
1837 | $s .= "\n with $smtp_proto" if $smtp_proto=~/^(ES|S|L)MTPS?A?\z/i; # rfc3848 |
---|
1838 | $s .= "\n id $id" if $id ne ''; |
---|
1839 | # do not disclose recipients if more than one |
---|
1840 | $s .= "\n for " . qquote_rfc2821_local(@$recips) if @$recips == 1; |
---|
1841 | $s .= ";\n " . rfc2822_timestamp($msginfo->rx_time); |
---|
1842 | $s =~ s/\n//g if !$folded; |
---|
1843 | $s; |
---|
1844 | } |
---|
1845 | |
---|
1846 | sub parse_received($) { |
---|
1847 | my($received) = @_; |
---|
1848 | local($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11); |
---|
1849 | $received =~ s/\n([ \t])/$1/g; # unfold |
---|
1850 | $received =~ s/[\n\r]//g; # delete remaining newlines if any |
---|
1851 | my(%fields); |
---|
1852 | while ($received =~ m{\G\s* |
---|
1853 | ( \b(from|by) \s+ ( (?: \[ (?: \\. | [^\]\\] )* \] | [^;\s\[] )+ ) |
---|
1854 | (?: \s* \( (?: ( [^\s\[]+ ) \s+ )? |
---|
1855 | \[ ( (?: \\. | [^\]\\] )* ) \] \s* |
---|
1856 | \) )? |
---|
1857 | (?: .*? ) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) # junk |
---|
1858 | | \b(via|with|id|for) \s+ |
---|
1859 | ( (?: " (?: \\. | [^"\\] )* " |
---|
1860 | | \[ (?: \\. | [^\]\\] )* \] |
---|
1861 | | \\. | . |
---|
1862 | )+? (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) ) |
---|
1863 | | (;) \s* ( .*? ) \s* \z # time |
---|
1864 | | (.*?) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) # junk |
---|
1865 | ) ( (?: \s+ | (?: \( (?: \\. | [^)\\] )* \) ) )* ) }xgcsi) |
---|
1866 | { |
---|
1867 | my($v1, $v2, $v3, $comment) = ('') x 4; |
---|
1868 | my($item, $field) = ($1, lc($2 || $6 || $8)); |
---|
1869 | if ($field eq 'from' || $field eq 'by') { |
---|
1870 | ($v1, $v2, $v3, $comment) = ($3, $4, $5, $11); |
---|
1871 | } elsif ($field eq ';') { # time |
---|
1872 | ($v1, $comment) = ($9, $11); |
---|
1873 | } elsif ($10 eq '') { # via|with|id|for |
---|
1874 | ($v1, $comment) = ($7, $11); |
---|
1875 | } else { # junk |
---|
1876 | ($v1, $comment) = ($10, $11); |
---|
1877 | } |
---|
1878 | $comment =~ s/^\s+//; |
---|
1879 | $comment =~ s/\s+\z//; |
---|
1880 | $item =~ s/^\Q$field\E\s*//i; |
---|
1881 | if (!exists $fields{$field}) { |
---|
1882 | $fields{$field} = [$item, $v1, $v2, $v3, $comment]; |
---|
1883 | do_log(5, "parse_received: $field = $item/$v1/$v2/$v3") if $field ne ''; |
---|
1884 | } |
---|
1885 | } |
---|
1886 | \%fields; |
---|
1887 | } |
---|
1888 | |
---|
1889 | sub fish_out_ip_from_received($) { |
---|
1890 | my($received) = @_; |
---|
1891 | my($ip); |
---|
1892 | my($fields_ref) = parse_received($received); |
---|
1893 | if (defined $fields_ref && exists $fields_ref->{'from'}) { |
---|
1894 | my($item, $v1, $v2, $v3, $comment) = @{$fields_ref->{'from'}}; |
---|
1895 | for ($v3, $v2, $v1, $comment, $item) { |
---|
1896 | if (/ \[ (\d{1,3} (?: \. \d{1,3}){3}) \] /x) { |
---|
1897 | $ip = $1; last; |
---|
1898 | } elsif (/ (\d{1,3} (?: \. \d{1,3}){3}) (?!\d) /x) { |
---|
1899 | $ip = $1; last; |
---|
1900 | } elsif (/ \[ (IPv6:)? ( ([0-9a-zA-Z]* : ){2,} [0-9a-zA-Z:.]* ) \] /xi) { |
---|
1901 | $ip = $2; last; |
---|
1902 | } |
---|
1903 | } |
---|
1904 | do_log(5, "fish_out_ip_from_received: $ip, $item"); |
---|
1905 | } |
---|
1906 | !defined($ip) ? undef : $ip; # undef need not be tainted |
---|
1907 | } |
---|
1908 | |
---|
1909 | # Splits unquoted fully qualified e-mail address, or an address |
---|
1910 | # with missing domain part. Returns a pair: (localpart, domain). |
---|
1911 | # The domain part (if nonempty) includes the '@' as the first character. |
---|
1912 | # If the syntax is badly broken, everything ends up as the localpart. |
---|
1913 | # The domain part can be an address literal, as specified by rfc2822. |
---|
1914 | # Does not handle explicit route paths. |
---|
1915 | # |
---|
1916 | sub split_address($) { |
---|
1917 | my($mailbox) = @_; |
---|
1918 | $mailbox =~ /^ (.*?) ( \@ (?: \[ (?: \\. | [^\]\\] )* \] |
---|
1919 | | [^@"<>\[\]\\\s] )* |
---|
1920 | ) \z/xs ? ($1, $2) : ($mailbox, ''); |
---|
1921 | } |
---|
1922 | |
---|
1923 | # split_localpart() splits localpart of an e-mail address at the first |
---|
1924 | # occurrence of the address extension delimiter character. (based on |
---|
1925 | # equivalent routine in Postfix) |
---|
1926 | # |
---|
1927 | # Reserved addresses are not split: postmaster, mailer-daemon, |
---|
1928 | # double-bounce. Addresses that begin with owner-, or addresses |
---|
1929 | # that end in -request are not split when the owner_request_special |
---|
1930 | # parameter is set. |
---|
1931 | |
---|
1932 | sub split_localpart($$) { |
---|
1933 | my($localpart, $delimiter) = @_; |
---|
1934 | my($owner_request_special) = 1; # configurable ??? |
---|
1935 | my($extension); |
---|
1936 | if ($localpart =~ /^(postmaster|mailer-daemon|double-bounce)\z/i) { |
---|
1937 | # do not split these, regardless of what the delimiter is |
---|
1938 | } elsif ($delimiter eq '-' && $owner_request_special && |
---|
1939 | $localpart =~ /^owner-|-request\z/i) { |
---|
1940 | # don't split owner-foo or foo-request |
---|
1941 | } elsif ($localpart =~ /^(.+?)\Q$delimiter\E(.*)\z/s) { |
---|
1942 | ($localpart, $extension) = ($1, $2); |
---|
1943 | # do not split the address if the result would have a null localpart |
---|
1944 | } |
---|
1945 | ($localpart, $extension); |
---|
1946 | } |
---|
1947 | |
---|
1948 | # For a given email address (e.g. for User+Foo@sub.exAMPLE.CoM) |
---|
1949 | # prepare and return a list of lookup keys in the following order: |
---|
1950 | # User+Foo@sub.exAMPLE.COM (as-is, no lowercasing) |
---|
1951 | # user+foo@sub.example.com |
---|
1952 | # user@sub.example.com (only if $recipient_delimiter nonempty) |
---|
1953 | # user+foo(@) (only if $include_bare_user) |
---|
1954 | # user(@) (only if $include_bare_user and $recipient_delimiter nonempty) |
---|
1955 | # (@)sub.example.com |
---|
1956 | # (@).sub.example.com |
---|
1957 | # (@).example.com |
---|
1958 | # (@).com |
---|
1959 | # (@). |
---|
1960 | # Note about (@): if $at_with_user is true the user-only keys (without domain) |
---|
1961 | # get an '@' character appended (e.g. 'user+foo@'). Usual for lookup_hash. |
---|
1962 | # If $at_with_user is false the domain-only (no user localpart) keys |
---|
1963 | # get a '@' prepended (e.g. '@.example.com'). Usual for lookup_sql. |
---|
1964 | # |
---|
1965 | # The domain part is lowercased in all but the first resulting item; |
---|
1966 | # the localpart is lowercased iff $localpart_is_case_sensitive is true. |
---|
1967 | # |
---|
1968 | sub make_query_keys($$$) { |
---|
1969 | my($addr,$at_with_user,$include_bare_user) = @_; |
---|
1970 | my($localpart,$domain) = split_address($addr); $domain = lc($domain); |
---|
1971 | my($saved_full_localpart) = $localpart; |
---|
1972 | $localpart = lc($localpart) if !c('localpart_is_case_sensitive'); |
---|
1973 | # chop off leading @, and trailing dots |
---|
1974 | $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s; |
---|
1975 | my($extension); my($delim) = c('recipient_delimiter'); |
---|
1976 | if ($delim ne '') { |
---|
1977 | ($localpart,$extension) = split_localpart($localpart,$delim); |
---|
1978 | } |
---|
1979 | my($append_to_user,$prepend_to_domain) = $at_with_user ? ('@','') : ('','@'); |
---|
1980 | my(@keys); # a list of query keys |
---|
1981 | push(@keys, $addr); # as is |
---|
1982 | push(@keys, $localpart.$delim.$extension.'@'.$domain) |
---|
1983 | if $extension ne ''; # user+foo@example.com |
---|
1984 | push(@keys, $localpart.'@'.$domain); # user@example.com |
---|
1985 | if ($include_bare_user) { # typically enabled for local users only |
---|
1986 | push(@keys, $localpart.$delim.$extension.$append_to_user) |
---|
1987 | if $extension ne ''; # user+foo(@) |
---|
1988 | push(@keys, $localpart.$append_to_user); # user(@) |
---|
1989 | } |
---|
1990 | push(@keys, $prepend_to_domain.$domain); # (@)sub.example.com |
---|
1991 | if ($domain =~ /\[/) { # don't split address literals |
---|
1992 | push(@keys, $prepend_to_domain.'.'); # (@). |
---|
1993 | } else { |
---|
1994 | my(@dkeys); my($d) = $domain; |
---|
1995 | for (;;) { # (@).sub.example.com (@).example.com (@).com (@). |
---|
1996 | push(@dkeys, $prepend_to_domain.'.'.$d); |
---|
1997 | last if $d eq ''; |
---|
1998 | $d = ($d =~ /^([^.]*)\.(.*)\z/s) ? $2 : ''; |
---|
1999 | } |
---|
2000 | if (@dkeys > 10) { @dkeys = @dkeys[$#dkeys-9 .. $#dkeys] } # sanity limit |
---|
2001 | push(@keys,@dkeys); |
---|
2002 | } |
---|
2003 | my($keys_ref) = []; # remove duplicates |
---|
2004 | for my $k (@keys) { push(@$keys_ref,$k) if !grep {$k eq $_} @$keys_ref } |
---|
2005 | ll(5) && do_log(5,"query_keys: ".join(', ',@$keys_ref)); |
---|
2006 | # the rhs replacement strings are similar to what would be obtained |
---|
2007 | # by lookup_re() given the following regular expression: |
---|
2008 | # /^( ( ( [^@]*? ) ( \Q$delim\E [^@]* )? ) (?: \@ (.*) ) )$/xs |
---|
2009 | my($rhs) = [ # a list of right-hand side replacement strings |
---|
2010 | $addr, # $1 = User+Foo@Sub.Example.COM |
---|
2011 | $saved_full_localpart, # $2 = User+Foo |
---|
2012 | $localpart, # $3 = user |
---|
2013 | $delim.$extension, # $4 = +foo |
---|
2014 | $domain, # $5 = sub.example.com |
---|
2015 | ]; |
---|
2016 | ($keys_ref, $rhs); |
---|
2017 | } |
---|
2018 | |
---|
2019 | # quote_rfc2821_local() quotes the local part of a mailbox address |
---|
2020 | # (given in internal (unquoted) form), and returns external (quoted) |
---|
2021 | # mailbox address, as per rfc2821. |
---|
2022 | # |
---|
2023 | # Internal (unquoted) form is used internally by amavisd-new and other mail sw, |
---|
2024 | # external (quoted) form is used in SMTP commands and message headers. |
---|
2025 | # |
---|
2026 | # The quote_rfc2821_local() conversion is necessary because addresses |
---|
2027 | # we get from certain MTAs are raw, with stripped-off quoting. |
---|
2028 | # To re-insert message back via SMTP, the local-part of the address needs |
---|
2029 | # to be quoted again if it contains reserved characters or otherwise |
---|
2030 | # does not obey the dot-atom syntax, as specified in rfc2821. |
---|
2031 | # Failing to do that gets us into trouble: amavis accepts message from MTA, |
---|
2032 | # but is unable to hand it back to MTA after checking, receiving |
---|
2033 | # '501 Bad address syntax' with every attempt. |
---|
2034 | # |
---|
2035 | sub quote_rfc2821_local($) { |
---|
2036 | my($mailbox) = @_; |
---|
2037 | # atext: any character except controls, SP, and specials (rfc2821/rfc2822) |
---|
2038 | my($atext) = "a-zA-Z0-9!#\$%&'*/=?^_`{|}~+-"; |
---|
2039 | # my($specials) = '()<>\[\]\\\\@:;,."'; |
---|
2040 | my($localpart,$domain) = split_address($mailbox); |
---|
2041 | if ($localpart !~ /^[$atext]+(\.[$atext]+)*\z/so) { # not dot-atom |
---|
2042 | $localpart =~ s/(["\\])/\\$1/g; # quoted-pair |
---|
2043 | $localpart = '"' . $localpart . '"'; # make a qcontent out of it |
---|
2044 | } |
---|
2045 | $domain = '' if $domain eq '@'; # strip off empty domain entirely |
---|
2046 | $localpart . $domain; |
---|
2047 | } |
---|
2048 | |
---|
2049 | # wraps the result of quote_rfc2821_local into angle brackets <...> ; |
---|
2050 | # If given a list, it returns a list (possibly converted to |
---|
2051 | # comma-separated scalar), quoting each element; |
---|
2052 | # |
---|
2053 | sub qquote_rfc2821_local(@) { |
---|
2054 | my(@r) = map { $_ eq '' ? '<>' : ('<' . quote_rfc2821_local($_) . '>') } @_; |
---|
2055 | wantarray ? @r : join(', ', @r); |
---|
2056 | } |
---|
2057 | |
---|
2058 | # unquote_rfc2821_local() strips away the quoting from the local part |
---|
2059 | # of an external (quoted) mailbox address, and returns internal (unquoted) |
---|
2060 | # mailbox address, as per rfc2821. |
---|
2061 | # |
---|
2062 | # Internal (unquoted) form is used internally by amavisd-new and other mail sw, |
---|
2063 | # external (quoted) form is used in SMTP commands and message headers. |
---|
2064 | # |
---|
2065 | sub unquote_rfc2821_local($) { |
---|
2066 | my($mailbox) = @_; |
---|
2067 | # the angle-bracket stripping is not really a duty of this subroutine, |
---|
2068 | # as it should have been already done elsewhere, but for the time being |
---|
2069 | # we do it here: |
---|
2070 | $mailbox = $1 if $mailbox =~ /^ \s* < ( .* ) > \s* \z/xs; |
---|
2071 | my($localpart,$domain) = split_address($mailbox); |
---|
2072 | $localpart =~ s/ " | \\ (.) | \\ \z /$1/xsg; # unquote quoted-pairs |
---|
2073 | $localpart . $domain; |
---|
2074 | } |
---|
2075 | |
---|
2076 | # Prepare a single SMTP response and an exit status as per sysexits.h |
---|
2077 | # from individual per-recipient response codes, taking into account |
---|
2078 | # sendmail milter specifics. Returns a triple: (smtp response, exit status, |
---|
2079 | # an indication whether DSN is needed). |
---|
2080 | # |
---|
2081 | sub one_response_for_all($$$) { |
---|
2082 | my($msginfo, $dsn_per_recip_capable, $am_id) = @_; |
---|
2083 | my($smtp_resp, $exit_code, $dsn_needed); |
---|
2084 | |
---|
2085 | my($delivery_method) = $msginfo->delivery_method; |
---|
2086 | my($sender) = $msginfo->sender; |
---|
2087 | my($per_recip_data) = $msginfo->per_recip_data; |
---|
2088 | my($any_not_done) = scalar(grep { !$_->recip_done } @$per_recip_data); |
---|
2089 | if ($delivery_method ne '' && $any_not_done) |
---|
2090 | { die "Explicit forwarding, but not all recips done" } |
---|
2091 | if (!@$per_recip_data) { # no recipients, nothing to do |
---|
2092 | $smtp_resp = "250 2.5.0 Ok, id=$am_id"; $exit_code = EX_OK; |
---|
2093 | do_log(5, "one_response_for_all <$sender>: no recipients, '$smtp_resp'"); |
---|
2094 | } |
---|
2095 | if (!defined $smtp_resp) { |
---|
2096 | for my $r (@$per_recip_data) { # any 4xx code ? |
---|
2097 | if ($r->recip_smtp_response =~ /^4/) # pick the first 4xx code |
---|
2098 | { $smtp_resp = $r->recip_smtp_response; last } |
---|
2099 | } |
---|
2100 | if (!defined $smtp_resp) { |
---|
2101 | for my $r (@$per_recip_data) { # any invalid code ? |
---|
2102 | if ($r->recip_done && $r->recip_smtp_response !~ /^[245]/) { |
---|
2103 | $smtp_resp = '451 4.5.0 Bad SMTP response code??? "' |
---|
2104 | . $r->recip_smtp_response . '"'; |
---|
2105 | last; # pick the first |
---|
2106 | } |
---|
2107 | } |
---|
2108 | } |
---|
2109 | if (defined $smtp_resp) { |
---|
2110 | $exit_code = EX_TEMPFAIL; |
---|
2111 | do_log(5, "one_response_for_all <$sender>: 4xx found, '$smtp_resp'"); |
---|
2112 | } |
---|
2113 | } |
---|
2114 | # NOTE: a 2xx SMTP response code is set both by internal Discard |
---|
2115 | # and by a genuine successful delivery. To distinguish between the two |
---|
2116 | # we need to check $r->recip_destiny as well. |
---|
2117 | # |
---|
2118 | if (!defined $smtp_resp) { |
---|
2119 | # if destiny for _all_ recipients is D_DISCARD => Discard |
---|
2120 | my($notall); |
---|
2121 | for my $r (@$per_recip_data) { |
---|
2122 | if ($r->recip_destiny == D_DISCARD) # pick the first DISCARD code |
---|
2123 | { $smtp_resp = $r->recip_smtp_response if !defined $smtp_resp } |
---|
2124 | else { $notall++; last } # one is not a discard, nogood |
---|
2125 | } |
---|
2126 | if ($notall) { $smtp_resp = undef } |
---|
2127 | if (defined $smtp_resp) { |
---|
2128 | # helper program will interpret 99 as discard |
---|
2129 | $exit_code = $delivery_method eq '' ? 99 : EX_OK; |
---|
2130 | do_log(5, "one_response_for_all <$sender>: all DISCARD, '$smtp_resp'"); |
---|
2131 | } |
---|
2132 | } |
---|
2133 | if (!defined $smtp_resp) { |
---|
2134 | # destiny for _all_ recipients is Discard or Reject => 5xx |
---|
2135 | # (and there is at least one Reject) |
---|
2136 | my($notall, $done_level); |
---|
2137 | my($bounce_cnt) = 0; |
---|
2138 | for my $r (@$per_recip_data) { |
---|
2139 | my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response); |
---|
2140 | if ($dest == D_DISCARD) { |
---|
2141 | # ok, this one is discard, let's see the rest |
---|
2142 | } elsif ($resp =~ /^5/ && $dest != D_BOUNCE) { |
---|
2143 | # prefer to report SMTP response code of genuine rejects |
---|
2144 | # from MTA, over internal rejects by content filters |
---|
2145 | if (!defined $smtp_resp || $r->recip_done > $done_level) |
---|
2146 | { $smtp_resp = $resp; $done_level = $r->recip_done } |
---|
2147 | } else { $notall++; last } # one is Pass or Bounce, nogood |
---|
2148 | } |
---|
2149 | if ($notall) { $smtp_resp = undef } |
---|
2150 | if (defined $smtp_resp) { |
---|
2151 | $exit_code = EX_UNAVAILABLE; |
---|
2152 | do_log(5, "one_response_for_all <$sender>: REJECTs, '$smtp_resp'"); |
---|
2153 | } |
---|
2154 | } |
---|
2155 | if (!defined $smtp_resp) { |
---|
2156 | # mixed destiny => 2xx, but generate dsn for bounces and rejects |
---|
2157 | my($rej_cnt) = 0; my($bounce_cnt) = 0; my($drop_cnt) = 0; |
---|
2158 | for my $r (@$per_recip_data) { |
---|
2159 | my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response); |
---|
2160 | if ($resp =~ /^2/ && $dest == D_PASS) # genuine successful delivery |
---|
2161 | { $smtp_resp = $resp if !defined $smtp_resp } |
---|
2162 | $drop_cnt++ if $dest == D_DISCARD; |
---|
2163 | if ($resp =~ /^5/) |
---|
2164 | { if ($dest == D_BOUNCE) { $bounce_cnt++ } else { $rej_cnt++ } } |
---|
2165 | } |
---|
2166 | $exit_code = EX_OK; |
---|
2167 | if (!defined $smtp_resp) { # no genuine Pass/2xx |
---|
2168 | # declare success, we'll handle bounce |
---|
2169 | $smtp_resp = "250 2.5.0 Ok, id=$am_id"; |
---|
2170 | if ($any_not_done) { $smtp_resp .= ", continue delivery" } |
---|
2171 | elsif ($delivery_method eq '') { $exit_code = 99 } # milter DISCARD |
---|
2172 | } |
---|
2173 | if ($rej_cnt + $bounce_cnt + $drop_cnt > 0) { |
---|
2174 | $smtp_resp .= ", "; |
---|
2175 | $smtp_resp .= "but " if $rej_cnt+$bounce_cnt+$drop_cnt<@$per_recip_data; |
---|
2176 | $smtp_resp .= join ", and ", |
---|
2177 | map { my($cnt, $nm) = @$_; |
---|
2178 | !$cnt ? () : $cnt == @$per_recip_data ? $nm : "$cnt $nm" |
---|
2179 | } ([$rej_cnt,'REJECT'], [$bounce_cnt,'BOUNCE'], [$drop_cnt,'DISCARD']); |
---|
2180 | } |
---|
2181 | $dsn_needed = |
---|
2182 | ($bounce_cnt > 0 || ($rej_cnt > 0 && !$dsn_per_recip_capable)) ? 1 : 0; |
---|
2183 | ll(5) && do_log(5,"one_response_for_all <$sender>: " |
---|
2184 | . ($rej_cnt + $bounce_cnt + $drop_cnt > 0 ? 'mixed' : 'success') |
---|
2185 | . ", dsn_needed=$dsn_needed, '$smtp_resp'"); |
---|
2186 | } |
---|
2187 | ($smtp_resp, $exit_code, $dsn_needed); |
---|
2188 | } |
---|
2189 | |
---|
2190 | 1; |
---|
2191 | |
---|
2192 | # |
---|
2193 | package Amavis::Lookup::RE; |
---|
2194 | use strict; |
---|
2195 | use re 'taint'; |
---|
2196 | |
---|
2197 | BEGIN { |
---|
2198 | use Exporter (); |
---|
2199 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
2200 | $VERSION = '2.034'; |
---|
2201 | @ISA = qw(Exporter); |
---|
2202 | } |
---|
2203 | BEGIN { import Amavis::Util qw(ll do_log) } |
---|
2204 | |
---|
2205 | # Make an object out of the supplied lookup list |
---|
2206 | # to make it distinguishable from simple ACL array |
---|
2207 | sub new($$) { my($class) = shift; bless [@_], $class } |
---|
2208 | |
---|
2209 | # lookup_re() performs a lookup for an e-mail address against |
---|
2210 | # a list made up of regular expressions. |
---|
2211 | # |
---|
2212 | # The full unmodified e-mail address is always used, so splitting to localpart |
---|
2213 | # and domain or lowercasing is NOT performed. The regexp is powerful enough |
---|
2214 | # that this can be accomplished by its mechanisms. The routine is useful for |
---|
2215 | # other RE tests besides the usual e-mail addresses, such as looking for |
---|
2216 | # banned file names. |
---|
2217 | # |
---|
2218 | # Each element of the list can be ref to a pair, or directly a regexp |
---|
2219 | # ('Regexp' object created by qr operator, or just a (less efficient) |
---|
2220 | # string containing a regular expression). If it is a pair, the first |
---|
2221 | # element is treated as a regexp, and the second provides a value in case |
---|
2222 | # the regexp matches. If not a pair, the implied result of a match is 1. |
---|
2223 | # |
---|
2224 | # The regular expression is taken as-is, no implicit anchoring or setting |
---|
2225 | # case insensitivity is done, so use qr'(?i)^user@example\.com$', |
---|
2226 | # and not a sloppy qr'user@example.com', which can easily backfire. |
---|
2227 | # Also, if qr is used with a delimiter other than ', make sure to quote |
---|
2228 | # the @ and $ . |
---|
2229 | # |
---|
2230 | # The pattern allows for capturing of parenthesized substrings, which can |
---|
2231 | # then be referenced from the result string using the $1, $2, ... notation, |
---|
2232 | # as with the Perl m// operator. The number after the $ may be a multi-digit |
---|
2233 | # decimal number. To avoid possible ambiguity the ${n} or $(n) form may be used |
---|
2234 | # Substring numbering starts with 1. Nonexistent references evaluate to empty |
---|
2235 | # strings. If any substitution is done, the result inherits the taintedness |
---|
2236 | # of $addr. Keep in mind that $ and @ characters needs to be backslash-quoted |
---|
2237 | # in qq() strings. Example: |
---|
2238 | # $virus_quarantine_to = new_RE( |
---|
2239 | # [ qr'^(.*)@example\.com$'i => 'virus-${1}@example.com' ], |
---|
2240 | # [ qr'^(.*)(@[^@]*)?$'i => 'virus-${1}${2}' ] ); |
---|
2241 | # |
---|
2242 | # Example (equivalent to the example in lookup_acl): |
---|
2243 | # $acl_re = Amavis::Lookup::RE->new( |
---|
2244 | # qr'@me\.ac\.uk$'i, [qr'[@.]ac\.uk$'i=>0], qr'\.uk$'i ); |
---|
2245 | # ($r,$k) = $acl_re->lookup_re('user@me.ac.uk'); |
---|
2246 | # or $r = lookup(0, 'user@me.ac.uk', $acl_re); |
---|
2247 | # |
---|
2248 | # 'user@me.ac.uk' matches me.ac.uk, returns true and search stops |
---|
2249 | # 'user@you.ac.uk' matches .ac.uk, returns false (because of =>0) and search stops |
---|
2250 | # 'user@them.co.uk' matches .uk, returns true and search stops |
---|
2251 | # 'user@some.com' does not match anything, falls through and returns false (undef) |
---|
2252 | |
---|
2253 | sub lookup_re($$;$) { |
---|
2254 | my($self, $addr,$get_all) = @_; |
---|
2255 | local($1,$2,$3,$4); my(@matchingkey,@result); |
---|
2256 | for my $e (@$self) { |
---|
2257 | my($key,$r); |
---|
2258 | if (ref($e) eq 'ARRAY') { # a pair: (regexp,result) |
---|
2259 | ($key,$r) = ($e->[0], @$e < 2 ? 1 : $e->[1]); |
---|
2260 | } else { # a single regexp (not a pair), implies result 1 |
---|
2261 | ($key,$r) = ($e, 1); |
---|
2262 | } |
---|
2263 | # do_log(5, "lookup_re: key=\"$addr\", matching against RE $key"); |
---|
2264 | ""=~/x{0}/; # braindead Perl: serves as explicit deflt for an empty regexp |
---|
2265 | my(@rhs) = $addr =~ /$key/; # match, capturing parenthesized subpatterns |
---|
2266 | if (@rhs) { # regexp matches |
---|
2267 | # do the righthand side replacements if any $n, ${n} or $(n) is specified |
---|
2268 | if (!ref($r) && $r=~/\$/) { |
---|
2269 | my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) } |
---|
2270 | { my($j)=$2+$3+$4; $j<1 ? '' : $rhs[$j-1] }gxse; |
---|
2271 | # bring taintedness of input to the result |
---|
2272 | $r .= substr($addr,0,0) if $any; |
---|
2273 | } |
---|
2274 | push(@result,$r); push(@matchingkey,$key); |
---|
2275 | last if !$get_all; |
---|
2276 | } |
---|
2277 | } |
---|
2278 | if (!ll(5)) { |
---|
2279 | # don't bother preparing log report which will not be printed |
---|
2280 | } elsif (!@result) { |
---|
2281 | do_log(5,"lookup_re($addr), no matches"); |
---|
2282 | } else { # pretty logging |
---|
2283 | my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b", |
---|
2284 | e => "\e", a => "\a", t => "\t"); |
---|
2285 | my(@mk) = @matchingkey; |
---|
2286 | for my $mk (@mk) # undo the \-quoting, will be redone by logging routines |
---|
2287 | { $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : $1 }egsx } |
---|
2288 | if (!$get_all) { # first match wins |
---|
2289 | do_log(5,sprintf('lookup_re(%s) matches key "%s", result=%s', |
---|
2290 | $addr,$mk[0],$result[0])); |
---|
2291 | } else { # want all matches |
---|
2292 | do_log(5,"lookup_re($addr) matches keys: ". |
---|
2293 | join(', ', map {sprintf('"%s"=>%s',$mk[$_],$result[$_])} |
---|
2294 | (0..$#result))); |
---|
2295 | } |
---|
2296 | } |
---|
2297 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
2298 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
2299 | } |
---|
2300 | |
---|
2301 | 1; |
---|
2302 | |
---|
2303 | # |
---|
2304 | package Amavis::Lookup::Label; |
---|
2305 | use strict; |
---|
2306 | use re 'taint'; |
---|
2307 | |
---|
2308 | # Make an object out of the supplied string, to serve as label |
---|
2309 | # in log messages generated by sub lookup |
---|
2310 | sub new($$) { my($class) = shift; my($str) = shift; bless \$str, $class } |
---|
2311 | sub display($) { my($self) = shift; $$self } |
---|
2312 | |
---|
2313 | 1; |
---|
2314 | |
---|
2315 | # |
---|
2316 | package Amavis::Lookup; |
---|
2317 | use strict; |
---|
2318 | use re 'taint'; |
---|
2319 | |
---|
2320 | BEGIN { |
---|
2321 | use Exporter (); |
---|
2322 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
2323 | $VERSION = '2.034'; |
---|
2324 | @ISA = qw(Exporter); |
---|
2325 | %EXPORT_TAGS = (); |
---|
2326 | @EXPORT = (); |
---|
2327 | @EXPORT_OK = qw(&lookup &lookup_ip_acl); |
---|
2328 | } |
---|
2329 | use subs @EXPORT_OK; |
---|
2330 | |
---|
2331 | BEGIN { |
---|
2332 | import Amavis::Util qw(ll do_log); |
---|
2333 | import Amavis::Conf qw(:platform c cr ca); |
---|
2334 | import Amavis::Timing qw(section_time); |
---|
2335 | import Amavis::rfc2821_2822_Tools qw(split_address make_query_keys); |
---|
2336 | } |
---|
2337 | |
---|
2338 | # lookup_hash() performs a lookup for an e-mail address against a hash map. |
---|
2339 | # If a match is found (a hash key exists in the Perl hash) the function returns |
---|
2340 | # whatever the map returns, otherwise undef is returned. First match wins, |
---|
2341 | # aborting further search sequence. |
---|
2342 | # |
---|
2343 | sub lookup_hash($$;$) { |
---|
2344 | my($addr, $hash_ref,$get_all) = @_; |
---|
2345 | (ref($hash_ref) eq 'HASH') |
---|
2346 | or die "lookup_hash: arg2 must be a hash ref: $hash_ref"; |
---|
2347 | local($1,$2,$3,$4); my(@matchingkey,@result); |
---|
2348 | my($keys_ref,$rhs_ref) = make_query_keys($addr,1,1); |
---|
2349 | for my $key (@$keys_ref) { # do the search |
---|
2350 | if (defined $$hash_ref{$key}) { # got it |
---|
2351 | push(@result,$$hash_ref{$key}); push(@matchingkey,$key); |
---|
2352 | last if !$get_all; |
---|
2353 | } |
---|
2354 | } |
---|
2355 | # do the right-hand side replacements if any $n, ${n} or $(n) is specified |
---|
2356 | for my $r (@result) { # remember that $r is just an alias to array elements |
---|
2357 | if (!ref($r) && $r=~/\$/) { # is a plain string containing a '$' |
---|
2358 | my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) } |
---|
2359 | { my($j)=$2+$3+$4; $j<1 ? '' : $rhs_ref->[$j-1] }gxse; |
---|
2360 | # bring taintedness of input to the result |
---|
2361 | $r .= substr($addr,0,0) if $any; |
---|
2362 | } |
---|
2363 | } |
---|
2364 | if (!ll(5)) { |
---|
2365 | # only bother with logging when needed |
---|
2366 | } elsif (!@result) { |
---|
2367 | do_log(5,"lookup_hash($addr), no matches"); |
---|
2368 | } elsif (!$get_all) { # first match wins |
---|
2369 | do_log(5,sprintf('lookup_hash(%s) matches key "%s", result=%s', |
---|
2370 | $addr,$matchingkey[0],$result[0])); |
---|
2371 | } else { # want all matches |
---|
2372 | do_log(5,"lookup_hash($addr) matches keys: ". |
---|
2373 | join(', ', map {sprintf('"%s"=>%s',$matchingkey[$_],$result[$_])} |
---|
2374 | (0..$#result))); |
---|
2375 | } |
---|
2376 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
2377 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
2378 | } |
---|
2379 | |
---|
2380 | # lookup_acl() performs a lookup for an e-mail address against |
---|
2381 | # access control list. |
---|
2382 | # |
---|
2383 | # Domain name of the supplied address is compared with each member of the |
---|
2384 | # access list in turn, the first match wins (terminates the search), |
---|
2385 | # and its value decides whether the result is true (yes, permit, pass) |
---|
2386 | # or false (no, deny, drop). Falling through without a match |
---|
2387 | # produces false (undef). Search is case-insensitive. |
---|
2388 | # |
---|
2389 | # If a list member contains a '@', the full e-mail address is compared, |
---|
2390 | # otherwise if a list member has a leading dot, the domain name part is |
---|
2391 | # matched only, and the domain as well as its subdomains can match. If there |
---|
2392 | # is no leading dot, the domain must match exactly (subdomains do not match). |
---|
2393 | # |
---|
2394 | # The presence of character '!' prepended to the list member decides |
---|
2395 | # whether the result will be true (without a '!') or false (with '!') |
---|
2396 | # in case this list member matches and terminates the search. |
---|
2397 | # |
---|
2398 | # Because search stops at the first match, it only makes sense |
---|
2399 | # to place more specific patterns before the more general ones. |
---|
2400 | # |
---|
2401 | # Although not a special case, it is good to remember that '.' always matches, |
---|
2402 | # so '.' would stop the search and return true, whereas '!.' would stop the |
---|
2403 | # search and return false (0) (which is normally not very useful, |
---|
2404 | # as false (undef) is also implied at the end of the list). |
---|
2405 | # |
---|
2406 | # Examples: |
---|
2407 | # |
---|
2408 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) |
---|
2409 | # 'me.ac.uk' matches me.ac.uk, returns true and search stops |
---|
2410 | # |
---|
2411 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) |
---|
2412 | # 'you.ac.uk' matches .ac.uk, returns false (because of '!') and search stops |
---|
2413 | # |
---|
2414 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) |
---|
2415 | # 'them.co.uk' matches .uk, returns true and search stops |
---|
2416 | # |
---|
2417 | # given: @acl = qw( me.ac.uk !.ac.uk .uk ) |
---|
2418 | # 'some.com' does not match anything, falls through and returns false (undef) |
---|
2419 | # |
---|
2420 | # given: @acl = qw( me.ac.uk !.ac.uk .uk !. ) |
---|
2421 | # 'some.com' similar to previous, except it returns 0 instead of undef |
---|
2422 | # |
---|
2423 | # given: @acl = qw( me.ac.uk !.ac.uk .uk . ) |
---|
2424 | # 'some.com' matches catchall ".", and returns true. The ".uk" is redundant |
---|
2425 | # |
---|
2426 | # more complex example: @acl = qw( |
---|
2427 | # !The.Boss@dept1.xxx.com .dept1.xxx.com |
---|
2428 | # .dept2.xxx.com .dept3.xxx.com lab.dept4.xxx.com |
---|
2429 | # sub.xxx.com !.sub.xxx.com |
---|
2430 | # me.d.aaa.com him.d.aaa.com !.d.aaa.com .aaa.com |
---|
2431 | # ); |
---|
2432 | |
---|
2433 | sub lookup_acl($$) { |
---|
2434 | my($addr, $acl_ref) = @_; |
---|
2435 | (ref($acl_ref) eq 'ARRAY') |
---|
2436 | or die "lookup_acl: arg2 must be a list ref: $acl_ref"; |
---|
2437 | return undef if !@$acl_ref; # empty list can't match anything |
---|
2438 | my($lpcs) = c('localpart_is_case_sensitive'); |
---|
2439 | my($localpart,$domain) = split_address($addr); $domain = lc($domain); |
---|
2440 | $localpart = lc($localpart) if !$lpcs; |
---|
2441 | local($1,$2); |
---|
2442 | # chop off leading @ and trailing dots |
---|
2443 | $domain = $1 if $domain =~ /^\@?(.*?)\.*\z/s; |
---|
2444 | my($lcaddr) = $localpart . '@' . $domain; |
---|
2445 | my($found, $matchingkey, $result); |
---|
2446 | for my $e (@$acl_ref) { |
---|
2447 | $result = 1; $matchingkey = $e; my($key) = $e; |
---|
2448 | if ($key =~ /^(!+)(.*)\z/s) { # starts with an exclamation mark(s) |
---|
2449 | $key = $2; |
---|
2450 | $result = 1-$result if (length($1) & 1); # negate if odd |
---|
2451 | } |
---|
2452 | if ($key =~ /^(.*?)\@([^@]*)\z/s) { # contains '@', check full address |
---|
2453 | $found++ if $localpart eq ($lpcs?$1:lc($1)) && $domain eq lc($2); |
---|
2454 | } elsif ($key =~ /^\.(.*)\z/s) { # leading dot: domain or subdomain |
---|
2455 | my($key_t) = lc($1); |
---|
2456 | $found++ if $domain eq $key_t || $domain =~ /(\.|\z)\Q$key_t\E\z/s; |
---|
2457 | } else { # match domain (but not its subdomains) |
---|
2458 | $found++ if $domain eq lc($key); |
---|
2459 | } |
---|
2460 | last if $found; |
---|
2461 | } |
---|
2462 | $matchingkey = $result = undef if !$found; |
---|
2463 | do_log(5, "lookup_acl($addr)". |
---|
2464 | (!$found?", no match":" matches key \"$matchingkey\", result=$result")); |
---|
2465 | !wantarray ? $result : ($result, $matchingkey); |
---|
2466 | } |
---|
2467 | |
---|
2468 | # Perform a lookup for an e-mail address against any number of supplied maps: |
---|
2469 | # - SQL map, |
---|
2470 | # - LDAP map, |
---|
2471 | # - hash map, |
---|
2472 | # - (access control) list, |
---|
2473 | # - a list of regular expressions, |
---|
2474 | # - a (defined) scalar always matches, and returns itself as the 'map' value |
---|
2475 | # (useful as a catchall for final 'pass' or 'fail'); |
---|
2476 | # (see lookup_hash, lookup_acl, lookup_sql and lookup_ldap for details). |
---|
2477 | # |
---|
2478 | # when $get_all is 0 (the common usage): |
---|
2479 | # If a match is found (a defined value), returns whatever the map returns, |
---|
2480 | # otherwise returns undef. FIRST match aborts further search sequence. |
---|
2481 | # when $get_all is true: |
---|
2482 | # Collects a list of results from ALL matching tables, and within each |
---|
2483 | # table from ALL matching key. Returns a ref to the list of results |
---|
2484 | # (and a ref to a list of matching keys if returning a pair). |
---|
2485 | # The first element of both lists is supposed to be what lookup() would |
---|
2486 | # have returned if $get_all were 0. The order of returned elements |
---|
2487 | # corresponds to the order of the search. |
---|
2488 | # |
---|
2489 | sub lookup($$@) { |
---|
2490 | my($get_all, $addr, @tables) = @_; |
---|
2491 | my($label, @result,@matchingkey); |
---|
2492 | for my $tb (@tables) { |
---|
2493 | my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection |
---|
2494 | if (!ref($t) || ref($t) eq 'SCALAR') { # a scalar always matches |
---|
2495 | my($r) = ref($t) ? $$t : $t; # allow direct or indirect reference |
---|
2496 | if (defined $r) { |
---|
2497 | do_log(5,"lookup: (scalar) matches, result=\"$r\""); |
---|
2498 | push(@result,$r); push(@matchingkey,"(constant:$r)"); |
---|
2499 | } |
---|
2500 | } elsif (ref($t) eq 'HASH') { |
---|
2501 | my($r,$mk) = lookup_hash($addr,$t,$get_all); |
---|
2502 | if (!defined $r) {} |
---|
2503 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
2504 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
2505 | } elsif (ref($t) eq 'ARRAY') { |
---|
2506 | my($r,$mk) = lookup_acl($addr,$t); |
---|
2507 | if (defined $r) { push(@result,$r); push(@matchingkey,$mk) } |
---|
2508 | } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label |
---|
2509 | # just a convenience for logging purposes, not a real lookup method |
---|
2510 | $label = $t->display; # grab the name, and proceed with the next table |
---|
2511 | } elsif ($t->isa('Amavis::Lookup::RE')) { |
---|
2512 | my($r,$mk) = $t->lookup_re($addr,$get_all); |
---|
2513 | if (!defined $r) {} |
---|
2514 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
2515 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
2516 | } elsif ($t->isa('Amavis::Lookup::SQL')) { |
---|
2517 | my($r,$mk) = $t->lookup_sql($addr,$get_all); |
---|
2518 | if (!defined $r) {} |
---|
2519 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
2520 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
2521 | } elsif ($t->isa('Amavis::Lookup::SQLfield')) { |
---|
2522 | my($r,$mk) = $t->lookup_sql_field($addr,$get_all); |
---|
2523 | if (!defined $r) {} |
---|
2524 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
2525 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
2526 | } elsif ($t->isa('Amavis::Lookup::LDAP')) { |
---|
2527 | my($r,$mk) = $t->lookup_ldap($addr,$get_all); |
---|
2528 | if (!defined $r) {} |
---|
2529 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
2530 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
2531 | } elsif ($t->isa('Amavis::Lookup::LDAPattr')) { |
---|
2532 | my($r,$mk) = $t->lookup_ldap_attr($addr,$get_all); |
---|
2533 | if (!defined $r) {} |
---|
2534 | elsif (!$get_all) { push(@result,$r); push(@matchingkey,$mk) } |
---|
2535 | elsif (@$r) { push(@result,@$r); push(@matchingkey,@$mk) } |
---|
2536 | } else { |
---|
2537 | die "TROUBLE: lookup table is an unknown object: " . ref($t); |
---|
2538 | } |
---|
2539 | last if @result && !$get_all; |
---|
2540 | } |
---|
2541 | # pretty logging |
---|
2542 | if ($label ne '') { $label = " ($label)" } |
---|
2543 | if (!ll(4)) { |
---|
2544 | # don't bother preparing log report which will not be printed |
---|
2545 | } elsif (!@tables) { |
---|
2546 | do_log(4,"lookup$label => undef, \"$addr\", no lookup tables"); |
---|
2547 | } elsif (!@result) { |
---|
2548 | do_log(4,"lookup$label => undef, \"$addr\" does not match"); |
---|
2549 | } elsif (!$get_all) { # first match wins |
---|
2550 | do_log(4,sprintf( |
---|
2551 | 'lookup%s => %-6s "%s" matches, result=%s, matching_key="%s"', |
---|
2552 | $label, $result[0]?'true,':'false,', $addr, |
---|
2553 | (ref $result[0] ne 'ARRAY' ? '"'.$result[0].'"' |
---|
2554 | : '('.join(',',@{$result[0]}).')'), |
---|
2555 | $matchingkey[0])); |
---|
2556 | } else { # want all matches |
---|
2557 | do_log(4,sprintf('lookup%s, %d matches for "%s", results: %s', |
---|
2558 | $label, scalar(@result), $addr, |
---|
2559 | join(', ', map {sprintf('"%s"=>%s', $matchingkey[$_], |
---|
2560 | (ref $result[$_] ne 'ARRAY' |
---|
2561 | ? '"'.$result[$_].'"' |
---|
2562 | : '('.join(',',@{$result[$_]}).')') )} |
---|
2563 | (0..$#result) ))); |
---|
2564 | } |
---|
2565 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
2566 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
2567 | } |
---|
2568 | |
---|
2569 | # ip_to_vec() takes IPv6 or IPv4 IP address with optional prefix length |
---|
2570 | # (or IPv4 mask), parses and validates it, and returns it as a 128-bit |
---|
2571 | # vector string that can be used as operand to Perl bitwise string operators. |
---|
2572 | # Syntax and other errors in the argument throw exception (die). |
---|
2573 | # If the second argument $allow_mask is 0, the prefix length or mask |
---|
2574 | # specification is not allowed as part of the IP address. |
---|
2575 | # |
---|
2576 | # The IPv6 syntax parsing and validation adheres to rfc3513. |
---|
2577 | # All the following IPv6 address forms are supported: |
---|
2578 | # x:x:x:x:x:x:x:x preferred form |
---|
2579 | # x:x:x:x:x:x:d.d.d.d alternative form |
---|
2580 | # ...::... zero-compressed form |
---|
2581 | # addr/prefix-length prefix length may be specified (defaults to 128) |
---|
2582 | # Optionally an "IPv6:" prefix may be prepended to the IPv6 address |
---|
2583 | # as specified by rfc2821. No brackets are allowed enclosing the address. |
---|
2584 | # |
---|
2585 | # The following IPv4 forms are allowed: |
---|
2586 | # d.d.d.d |
---|
2587 | # d.d.d.d/prefix-length CIDR mask length is allowed (defaults to 32) |
---|
2588 | # d.d.d.d/m.m.m.m network mask (gets converted to prefix-length) |
---|
2589 | # If prefix-length or a mask is specified with an IPv4 address, the address |
---|
2590 | # may be shortened to d.d.d/n or d.d/n or d/n. Such truncation is allowed |
---|
2591 | # for compatibility with earlier version, but is deprecated and is not |
---|
2592 | # allowed for IPv6 addresses. |
---|
2593 | # |
---|
2594 | # IPv4 addresses and masks are converted to IPv4-mapped IPv6 addresses |
---|
2595 | # of the form ::FFFF:d.d.d.d, The CIDR mask length (0..32) is converted |
---|
2596 | # to IPv6 prefix-length (96..128). The returned vector strings resulting |
---|
2597 | # from IPv4 and IPv6 forms are indistinguishable. |
---|
2598 | # |
---|
2599 | # NOTE: |
---|
2600 | # d.d.d.d is equivalent to ::FFFF:d.d.d.d (IPv4-mapped IPv6 address) |
---|
2601 | # which is not the same as ::d.d.d.d (IPv4-compatible IPv6 address) |
---|
2602 | # |
---|
2603 | # A triple is returned: |
---|
2604 | # - IP address represented as a 128-bit vector (a string) |
---|
2605 | # - network mask derived from prefix length, a 128-bit vector (string) |
---|
2606 | # - prefix length as an integer (0..128) |
---|
2607 | # |
---|
2608 | sub ip_to_vec($;$) { |
---|
2609 | my($ip,$allow_mask) = @_; |
---|
2610 | my($ip_len); my(@ip_fields); |
---|
2611 | local($1,$2,$3,$4,$5,$6); |
---|
2612 | $ip =~ s/^[ \t]+//; $ip =~ s/[ \t\n]+\z//s; # trim |
---|
2613 | my($ipa) = $ip; |
---|
2614 | ($ipa,$ip_len) = ($1,$2) if $allow_mask && $ip =~ m{^([^/]*)/(.*)\z}s; |
---|
2615 | if ($ipa =~ m{^(IPv6:)?(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z}si){ |
---|
2616 | # IPv6 alternative form x:x:x:x:x:x:d.d.d.d |
---|
2617 | !grep {$_ > 255} ($3,$4,$5,$6) |
---|
2618 | or die "Invalid decimal field value in IPv6 address: $ip"; |
---|
2619 | $ipa = $2 . sprintf("%02X%02X:%02X%02X", $3,$4,$5,$6); |
---|
2620 | } elsif ($ipa =~ m{^\d{1,3}(?:\.\d{1,3}){0,3}\z}) { # IPv4 form |
---|
2621 | my(@d) = split(/\./,$ipa,-1); |
---|
2622 | !grep {$_ > 255} @d or die "Invalid field value in IPv4 address: $ip"; |
---|
2623 | defined($ip_len) || @d==4 |
---|
2624 | or die "IPv4 address $ip contains fewer than 4 fields"; |
---|
2625 | $ipa = '::FFFF:' . sprintf("%02X%02X:%02X%02X", @d); # IPv4-mapped IPv6 |
---|
2626 | if (!defined($ip_len)) { $ip_len = 32; # no length, defaults to /32 |
---|
2627 | } elsif ($ip_len =~ /^\d{1,9}\z/) { # /n, IPv4 CIDR notation |
---|
2628 | } elsif ($ip_len =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) { |
---|
2629 | !grep {$_ > 255} ($1,$2,$3,$4) |
---|
2630 | or die "Illegal field value in IPv4 mask: $ip"; |
---|
2631 | my($mask1) = pack('C4',$1,$2,$3,$4); # /m.m.m.m |
---|
2632 | my($len) = unpack("%b*",$mask1); # count ones |
---|
2633 | my($mask2) = pack('B32', '1' x $len); # reconstruct mask from count |
---|
2634 | $mask1 eq $mask2 |
---|
2635 | or die "IPv4 mask not representing valid CIDR mask: $ip"; |
---|
2636 | $ip_len = $len; |
---|
2637 | } else { |
---|
2638 | die "Invalid IPv4 network mask or CIDR prefix length: $ip"; |
---|
2639 | } |
---|
2640 | $ip_len<=32 or die "IPv4 network prefix length greater than 32: $ip"; |
---|
2641 | $ip_len += 128-32; # convert IPv4 net mask length to IPv6 prefix length |
---|
2642 | } |
---|
2643 | $ip_len = 128 if !defined($ip_len); |
---|
2644 | $ip_len<=128 or die "IPv6 network prefix length greater than 128: $ip"; |
---|
2645 | $ipa =~ s/^IPv6://i; |
---|
2646 | # now we presumably have an IPv6 preferred form x:x:x:x:x:x:x:x |
---|
2647 | if ($ipa !~ /^(.*?)::(.*)\z/s) { # zero-compressing form used? |
---|
2648 | @ip_fields = split(/:/,$ipa,-1); # no |
---|
2649 | } else { # expand zero-compressing form |
---|
2650 | my(@a) = split(/:/,$1,-1); my(@b) = split(/:/,$2,-1); |
---|
2651 | my($missing_cnt) = 8-(@a+@b); $missing_cnt = 1 if $missing_cnt<1; |
---|
2652 | @ip_fields = (@a, (0) x $missing_cnt, @b); |
---|
2653 | } |
---|
2654 | !grep { !/^[0-9a-zA-Z]{1,4}\z/ } @ip_fields |
---|
2655 | or die "Invalid syntax of IPv6 address: $ip"; |
---|
2656 | @ip_fields<8 and die "IPv6 address $ip contains fewer than 8 fields"; |
---|
2657 | @ip_fields>8 and die "IPv6 address $ip contains more than 8 fields"; |
---|
2658 | my($vec) = pack("n8", map {hex} @ip_fields); |
---|
2659 | $ip_len=~/^\d{1,3}\z/ |
---|
2660 | or die "Invalid prefix length syntax in IP address: $ip"; |
---|
2661 | $ip_len<=128 or die "Invalid prefix length in IPv6 address: $ip"; |
---|
2662 | my($mask) = pack('B128', '1' x $ip_len); |
---|
2663 | # do_log(5,sprintf("ip_to_vec: %s => %s/%d\n", $ip,unpack("B*",$vec),$ip_len)); |
---|
2664 | ($vec,$mask,$ip_len); |
---|
2665 | } |
---|
2666 | |
---|
2667 | # lookup_ip_acl() performs a lookup for an IPv4 or IPv6 address |
---|
2668 | # against access control list or a hash of network or host addresses. |
---|
2669 | # |
---|
2670 | # IP address is compared to each member of the access list in turn, |
---|
2671 | # the first match wins (terminates the search), and its value decides |
---|
2672 | # whether the result is true (yes, permit, pass) or false (no, deny, drop). |
---|
2673 | # Falling through without a match produces false (undef). |
---|
2674 | # |
---|
2675 | # The presence of character '!' prepended to the list member decides |
---|
2676 | # whether the result will be true (without a '!') or false (with '!') |
---|
2677 | # in case this list member matches and terminates the search. |
---|
2678 | # |
---|
2679 | # Because search stops at the first match, it only makes sense |
---|
2680 | # to place more specific patterns before the more general ones. |
---|
2681 | # |
---|
2682 | # For IPv4 a network address can be specified in classless notation |
---|
2683 | # n.n.n.n/k, or using a mask n.n.n.n/m.m.m.m . Missing mask implies /32, |
---|
2684 | # i.e. a host address. For IPv6 addresses all rfc3513 forms are allowed. |
---|
2685 | # See also comments at ip_to_vec(). |
---|
2686 | # |
---|
2687 | # Although not a special case, it is good to remember that '::/0' |
---|
2688 | # always matches any IPv4 or IPv6 address. |
---|
2689 | # |
---|
2690 | # The '0/0' is equivalent to '::FFFF:0:0/0' and matches any IPv4 address |
---|
2691 | # (including IPv4-mapped IPv6 addresses), but not other IPv6 addresses! |
---|
2692 | # |
---|
2693 | # Example |
---|
2694 | # given: @acl = qw( !192.168.1.12 172.16.3.3 !172.16.3.0/255.255.255.0 |
---|
2695 | # 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 |
---|
2696 | # !0.0.0.0/8 !:: 127.0.0.0/8 ::1 ); |
---|
2697 | # matches rfc1918 private address space except host 192.168.1.12 |
---|
2698 | # and net 172.16.3/24 (but host 172.16.3.3 within 172.16.3/24 still matches). |
---|
2699 | # In addition the 'unspecified' (null) IPv4 and IPv6 addresses return false, |
---|
2700 | # and IPv4 and IPv6 loopback addresses match and return true. |
---|
2701 | # |
---|
2702 | sub lookup_ip_acl($@) { |
---|
2703 | my($ip, @nets_ref) = @_; |
---|
2704 | my($ip_vec,$ip_mask) = ip_to_vec($ip,0); |
---|
2705 | my($label,$found,$fullkey,$result); |
---|
2706 | for my $tb (@nets_ref) { |
---|
2707 | my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection |
---|
2708 | if (!ref($t) || ref($t) eq 'SCALAR') { # a scalar always matches |
---|
2709 | my($r) = ref($t) ? $$t : $t; # allow direct or indirect reference |
---|
2710 | $result = $r; $fullkey = "(constant:$r)"; |
---|
2711 | $found++ if defined $result; |
---|
2712 | } elsif (ref($t) eq 'HASH') { |
---|
2713 | # match the canonical IP address: dot-quad IPv4, or preferred IPv6 form |
---|
2714 | my($ip_c); # IP address in the canonical preferred form: x:x:x:x:x:x:x:x |
---|
2715 | my($ip_dq); # IPv4 in a dotted-quad form if IPv4-mapped, or undef |
---|
2716 | $ip_c = join(':', map {sprintf('%04x',$_)} unpack('n8',$ip_vec)); |
---|
2717 | my($ipv4_vec,$ipv4_mask) = ip_to_vec('::FFFF:0:0/96',1); |
---|
2718 | if ( ($ip_vec & $ipv4_mask) eq ($ipv4_vec & $ipv4_mask) ) { |
---|
2719 | # is an IPv4-mapped IPv6 address, format it in a dot-quad form |
---|
2720 | $ip_dq = join('.', unpack('C4',substr($ip_vec,12,4))); # last 32 bits |
---|
2721 | } |
---|
2722 | do_log(5, "lookup_ip_acl keys: \"$ip_dq\", \"$ip_c\""); |
---|
2723 | if (defined $ip_dq) { # try dot-quad if applicable |
---|
2724 | $fullkey = $ip_dq; $result = $t->{$fullkey}; |
---|
2725 | $found++ if defined $result; |
---|
2726 | } |
---|
2727 | if (!$found) { # try the 'preferred IPv6 form' |
---|
2728 | $fullkey = $ip_c; $result = $t->{$fullkey}; |
---|
2729 | $found++ if defined $result; |
---|
2730 | } |
---|
2731 | } elsif (ref($t) eq 'ARRAY') { |
---|
2732 | for my $net (@$t) { |
---|
2733 | $fullkey = $net; my($key) = $fullkey; $result = 1; |
---|
2734 | if ($key =~ /^(!+)(.*)\z/s) { # starts with exclamation mark(s) |
---|
2735 | $key = $2; |
---|
2736 | $result = 1 - $result if (length($1) & 1); # negate if odd |
---|
2737 | } |
---|
2738 | my($acl_ip_vec, $acl_mask) = ip_to_vec($key,1); |
---|
2739 | $found++ if ($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask); |
---|
2740 | last if $found; |
---|
2741 | } |
---|
2742 | } elsif ($t->isa('Amavis::Lookup::Label')) { # logging label |
---|
2743 | # just a convenience for logging purposes, not a real lookup method |
---|
2744 | $label = $t->display; # grab the name, and proceed with the next table |
---|
2745 | } else { |
---|
2746 | die "TROUBLE: lookup table is an unknown object: " . ref($t); |
---|
2747 | } |
---|
2748 | last if $found; |
---|
2749 | } |
---|
2750 | $fullkey = $result = undef if !$found; |
---|
2751 | if ($label ne '') { $label = " ($label)" } |
---|
2752 | ll(4) && do_log(4, "lookup_ip_acl$label: key=\"$ip\"" |
---|
2753 | . (!$found ? ", no match" : " matches \"$fullkey\", result=$result")); |
---|
2754 | !wantarray ? $result : ($result, $fullkey); |
---|
2755 | } |
---|
2756 | |
---|
2757 | 1; |
---|
2758 | |
---|
2759 | # |
---|
2760 | package Amavis::Expand; |
---|
2761 | use strict; |
---|
2762 | use re 'taint'; |
---|
2763 | |
---|
2764 | BEGIN { |
---|
2765 | use Exporter (); |
---|
2766 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
2767 | $VERSION = '2.034'; |
---|
2768 | @ISA = qw(Exporter); |
---|
2769 | %EXPORT_TAGS = (); |
---|
2770 | @EXPORT = (); |
---|
2771 | @EXPORT_OK = qw(&expand); |
---|
2772 | } |
---|
2773 | use subs @EXPORT_OK; |
---|
2774 | BEGIN { |
---|
2775 | import Amavis::Util qw(ll do_log); |
---|
2776 | } |
---|
2777 | |
---|
2778 | # Given a string reference and a hashref of predefined (builtin) macros, |
---|
2779 | # expand() performs a macro expansion and returns a ref to the resulting string |
---|
2780 | # |
---|
2781 | # This is a simple, yet fully fledged macro processor with proper lexical |
---|
2782 | # analysis, call stack, implied quoting levels, user supplied builtin macros, |
---|
2783 | # two builtin flow-control macros: selector and iterator, plus a macro #, |
---|
2784 | # which discards input tokens until NEWLINE (like 'dnl' in m4). |
---|
2785 | # Also recognized are the usual \c and \nnn forms for specifying special |
---|
2786 | # characters, where c can be any of: r, n, f, b, e, a, t. Lexical analysis |
---|
2787 | # of the input string is preformed only once, macro result values are not |
---|
2788 | # in danger of being lexically parsed and are treated as plain characters, |
---|
2789 | # loosing any special meaning they might have. No new macros can be defined |
---|
2790 | # by processing input string (at least in this version). |
---|
2791 | # |
---|
2792 | # Simple caller-provided macros have a single character name (usually a letter) |
---|
2793 | # and can evaluate to a string (possibly empty or undef), or an array of |
---|
2794 | # strings. It can also be a subroutine reference, in which case the subroutine |
---|
2795 | # will be called whenever macro value is needed. The subroutine must return |
---|
2796 | # a scalar: a string, or an array reference. The result will be treated as if |
---|
2797 | # it were specified directly. |
---|
2798 | # |
---|
2799 | # Two forms of simple macro calls are known: %x and %#x (where x is a single |
---|
2800 | # letter macro name, i.e. a key in a user-supplied hash): |
---|
2801 | # %x evaluates to the hash value associated with the name x; |
---|
2802 | # if the value is an array ref, the result is a single concatenated |
---|
2803 | # string of values separated with comma-space pairs; |
---|
2804 | # %#x evaluates to a number: if the macro value is a scalar, returns 0 |
---|
2805 | # for all-whitespace value, and 1 otherwise. If a value is an array ref, |
---|
2806 | # evaluates to the number of elements in the array. |
---|
2807 | # A simple macro is evaluated only in nonquoted context, i.e. top-level |
---|
2808 | # text or in the first argument of a selector (see below). A literal percent |
---|
2809 | # character can be produced by %% or \%. |
---|
2810 | # |
---|
2811 | # More powerful expansion is provided by two builtin macros, using syntax: |
---|
2812 | # [? arg1 | arg2 | ... ] a selector |
---|
2813 | # [ arg1 | arg2 | ... ] an iterator |
---|
2814 | # where [, [?, | and ] are required tokens. To take away the special meaning |
---|
2815 | # of these characters they can be quoted by a backslash, e.g. \[ or \\ . |
---|
2816 | # Arguments are arbitrary text, possibly multiline, whitespace counts. |
---|
2817 | # Nested macro calls are permitted, proper bracket nesting must be observed. |
---|
2818 | # |
---|
2819 | # SELECTOR lets its first argument be evaluated immediately, and implicitly |
---|
2820 | # protects the remaining arguments. The first argument chooses which of the |
---|
2821 | # remaining arguments is selected as a result value. The result is only then |
---|
2822 | # evaluated, remaining arguments are discarded without evaluation. The first |
---|
2823 | # argument is usually a number (with optional leading and trailing whitespace). |
---|
2824 | # If it is a non-numeric string, it is treated as 0 for all-whitespace, and |
---|
2825 | # as 1 otherwise. Value 0 selects the very next (second) argument, value 1 |
---|
2826 | # selects the one after it, etc. If the value is greater than the number |
---|
2827 | # of available arguments, the last one (but never the first) is selected. |
---|
2828 | # If there is only one (the first) alternative available but the value is |
---|
2829 | # greater than 0, an empty string is returned. |
---|
2830 | # Examples: |
---|
2831 | # [? 2 | zero | one | two | three ] -> two |
---|
2832 | # [? foo | none | any | two | three ] -> any |
---|
2833 | # [? 24 | 0 | one | many ] -> many |
---|
2834 | # [? 2 |No recipients] -> (empty string) |
---|
2835 | # [? %#R |No recipients|One recipient|%#R recipients] |
---|
2836 | # [? %q |No quarantine|Quarantined as %q] |
---|
2837 | # Note that a selector macro call can be considered a form of if-then-else, |
---|
2838 | # except that the 'then' and 'else' parts are swapped! |
---|
2839 | # |
---|
2840 | # ITERATOR in its full form takes three arguments (and ignores any extra |
---|
2841 | # arguments after that): |
---|
2842 | # [ %x | body-usually-containing-%x | separator ] |
---|
2843 | # All iterator's arguments are implicitly quoted, iterator performs its own |
---|
2844 | # substitutions (described below). The result of an iterator call is a body |
---|
2845 | # (the second argument) repeated as many times as there are elements in the |
---|
2846 | # array denoted by the first argument. In each instance of a body |
---|
2847 | # all occurrences of token %x in the body are replaced with each successive |
---|
2848 | # element of the array. Resulting body instances are then glued together |
---|
2849 | # with a string given as the third argument. The result is finally evaluated |
---|
2850 | # as any top-level text for possible further expansion. |
---|
2851 | # |
---|
2852 | # There are two simplified forms of iterator call: |
---|
2853 | # [ body | separator ] |
---|
2854 | # or [ body ] |
---|
2855 | # where missing separator is considered a null string, and the missing formal |
---|
2856 | # argument name is obtained by looking for the first token of the form %x |
---|
2857 | # in the body. |
---|
2858 | # Examples: |
---|
2859 | # [%V| ] a space-separated list of virus names |
---|
2860 | # |
---|
2861 | # [%V|\n] a newline-separated list of virus names |
---|
2862 | # |
---|
2863 | # [%V| |
---|
2864 | # ] same thing: a newline-separated list of virus names |
---|
2865 | # |
---|
2866 | # [ |
---|
2867 | # %V] a list of virus names, each preceeded by NL and spaces |
---|
2868 | # |
---|
2869 | # [ %R |%s --> <%R>|, ] a comma-space separated list of sender/recipient |
---|
2870 | # name pairs where recipient is iterated over the list |
---|
2871 | # of recipients. (Only the (first) token %x in the first |
---|
2872 | # argument is significant, other characters are ignored.) |
---|
2873 | # |
---|
2874 | # [%V|[%R|%R + %V|, ]|; ] produce all combinations of %R + %V elements |
---|
2875 | # |
---|
2876 | # A combined example: |
---|
2877 | # [? %#C |#|Cc: [<%C>|, ]] |
---|
2878 | # [? %#C ||Cc: [<%C>|, ]\n]# ... same thing |
---|
2879 | # evaluates to an empty string if there are no elements in the %C array, |
---|
2880 | # otherwise it evaluates to a line: Cc: <addr1>, <addr2>, ...\n |
---|
2881 | # The '#' removes input characters until and including newline after it. |
---|
2882 | # It can be used for clarity to allow newlines be placed in the source text |
---|
2883 | # but not resulting in empty lines in the expanded text. In the second example |
---|
2884 | # above, a backslash at the end of the line would achieve the same result, |
---|
2885 | # although the method is different: \NEWLINE is removed during initial lexical |
---|
2886 | # analysis, while # is an internal macro which, when called, actively discards |
---|
2887 | # tokens following it, until NEWLINE (or end of input) is encountered. |
---|
2888 | # Whitespace (including newlines) around the first argument %#C of selector |
---|
2889 | # call is ignored and can be used for clarity. |
---|
2890 | # |
---|
2891 | # These all produce the same result: |
---|
2892 | # To: [%T|%T|, ] |
---|
2893 | # To: [%T|, ] |
---|
2894 | # To: %T |
---|
2895 | # |
---|
2896 | # See further practical examples in the supplied notification messages; |
---|
2897 | # see also README.customize file. |
---|
2898 | # |
---|
2899 | # Author: Mark Martinec <Mark.Martinec@ijs.si>, 2002 |
---|
2900 | # |
---|
2901 | sub expand($$) { |
---|
2902 | my($str_ref) = shift; # a ref to a source string to be macro expanded; |
---|
2903 | my($builtins_href) = shift; # a hashref, mapping builtin macro names (single |
---|
2904 | # char) to macro values: strings or array refs |
---|
2905 | my($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) = |
---|
2906 | \('[', '[?', ']', '|', '#'); # lexical elements to be used as references |
---|
2907 | my(%lexmap); # maps string to reference in order to protect lexels |
---|
2908 | for (keys(%$builtins_href)) |
---|
2909 | { $lexmap{"%$_"} = \"%$_"; $lexmap{"%#$_"} = \"%#$_" } |
---|
2910 | for ($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) { $lexmap{$$_} = $_ } |
---|
2911 | # parse lexically |
---|
2912 | my(@tokens) = $$str_ref =~ /\G \# | \[\?? | [\]|] | % \#? . | \\ [^0-7] | |
---|
2913 | \\ [0-7]{1,3} | [^\[\]\\|%\n#]+ | [^\n]+? | \n /gcsx; |
---|
2914 | # replace lexical element strings with object references, |
---|
2915 | # unquote backslash-quoted characters and %%, and drop backslash-newlines |
---|
2916 | my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b", |
---|
2917 | e => "\e", a => "\a", t => "\t"); |
---|
2918 | for (@tokens) { |
---|
2919 | if (exists $lexmap{$_}) { $_ = $lexmap{$_} } # replace with refs |
---|
2920 | elsif ($_ eq "\\\n") { $_ = '' } # drop \NEWLINE |
---|
2921 | elsif (/^%(%)\z/) { $_ = $1 } # %% -> % |
---|
2922 | elsif (/^(%#?.)\z/s) { $_ = \$1 } # unknown builtins |
---|
2923 | elsif (/^\\([0-7]{1,3})\z/) { $_ = chr(oct($1)) } # \nnn |
---|
2924 | elsif (/^\\(.)\z/s) { $_ = (exists($esc{$1}) ? $esc{$1} : $1) } |
---|
2925 | } |
---|
2926 | my($level) = 0; my($quote_level) = 0; my(@macro_type, @arg); |
---|
2927 | my($output_str) = ''; my($whereto) = \$output_str; |
---|
2928 | while (@tokens) { |
---|
2929 | my($t) = shift(@tokens); |
---|
2930 | if ($t eq '') { # ignore leftovers |
---|
2931 | } elsif ($quote_level>0 && ref($t) && ($t == $lex_lbr || $t == $lex_lbrq)){ |
---|
2932 | $quote_level++; |
---|
2933 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t); |
---|
2934 | } elsif (ref($t) && $t == $lex_lbr) { # begin iterator macro call |
---|
2935 | $quote_level++; $level++; |
---|
2936 | unshift(@arg, [[]]); unshift(@macro_type, ''); $whereto = $arg[0][0]; |
---|
2937 | } elsif (ref($t) && $t == $lex_lbrq) { # begin selector macro call |
---|
2938 | $level++; unshift(@arg, [[]]); unshift(@macro_type, ''); |
---|
2939 | $whereto = $arg[0][0]; $macro_type[0] = 'select'; |
---|
2940 | } elsif ($quote_level > 1 && ref($t) && $t == $lex_rbr) { |
---|
2941 | $quote_level--; |
---|
2942 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t); |
---|
2943 | } elsif ($level > 0 && ref($t) && $t == $lex_sep) { # next argument |
---|
2944 | if ($quote_level == 0 && $macro_type[0] eq 'select' && @{$arg[0]} == 1) { |
---|
2945 | $quote_level++; |
---|
2946 | } |
---|
2947 | if ($quote_level == 1) { |
---|
2948 | unshift(@{$arg[0]}, []); $whereto = $arg[0][0]; # begin next arg |
---|
2949 | } else { |
---|
2950 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t); |
---|
2951 | } |
---|
2952 | } elsif ($quote_level > 0 && ref($t) && $t == $lex_rbr) { |
---|
2953 | $quote_level--; # quote level just dropped to 0, this is now a call |
---|
2954 | $level-- if $level > 0; |
---|
2955 | my(@result); |
---|
2956 | if ($macro_type[0] eq 'select') { |
---|
2957 | my($sel, @alternatives) = reverse @{$arg[0]}; # list of refs |
---|
2958 | $sel = !ref($sel) ? '' : join('', @$sel); # turn ref into string |
---|
2959 | if ($sel =~ /^\s*\z/) { $sel = 0 } |
---|
2960 | elsif ($sel =~ /^\s*(\d+)\s*\z/) { $sel = 0+$1 } # make numeric |
---|
2961 | else { $sel = 1 } |
---|
2962 | # provide an empty second alternative if we only have one specified |
---|
2963 | push(@alternatives, []) if @alternatives < 2 && $sel > 0; |
---|
2964 | if ($sel < 0) { $sel = 0 } |
---|
2965 | elsif ($sel > $#alternatives) { $sel = $#alternatives } |
---|
2966 | @result = @{$alternatives[$sel]}; |
---|
2967 | } else { # iterator |
---|
2968 | my($cvar_r, $sep_r, $body_r, $cvar); # give meaning to arguments |
---|
2969 | if (@{$arg[0]} >= 3) { ($cvar_r,$body_r,$sep_r) = reverse @{$arg[0]} } |
---|
2970 | else { ($body_r, $sep_r) = reverse @{$arg[0]}; $cvar_r = $body_r } |
---|
2971 | # find the formal argument name (iterator) |
---|
2972 | for (@$cvar_r) { |
---|
2973 | if (ref && $$_ =~ /^%(.)\z/s) { $cvar = $1; last } |
---|
2974 | } |
---|
2975 | if (exists($builtins_href->{$cvar})) { |
---|
2976 | my($values_r) = $builtins_href->{$cvar}; |
---|
2977 | while (ref($values_r) eq 'CODE') { $values_r = &$values_r } |
---|
2978 | $values_r = [$values_r] if !ref($values_r); |
---|
2979 | my($ind); |
---|
2980 | my($re) = qr/^%\Q$cvar\E\z/; |
---|
2981 | for my $val (@$values_r) { |
---|
2982 | push(@result, @$sep_r) if ++$ind > 1 && ref($sep_r); |
---|
2983 | push(@result, map { (ref && $$_ =~ /$re/) ? $val : $_ } @$body_r); |
---|
2984 | } |
---|
2985 | } |
---|
2986 | } |
---|
2987 | shift(@macro_type); # pop the call stack |
---|
2988 | shift(@arg); |
---|
2989 | $whereto = $level > 0 ? $arg[0][0] : \$output_str; |
---|
2990 | unshift(@tokens, @result); # active macro call, reevaluate result |
---|
2991 | } else { # quoted, plain string, simple macro call, or a misplaced token |
---|
2992 | my($s) = ''; |
---|
2993 | if ($quote_level > 0 || !ref($t)) { |
---|
2994 | $s = $t; # quoted or string |
---|
2995 | } elsif ($t == $lex_h) { # discard tokens to (and including) newline |
---|
2996 | while (@tokens) { last if shift(@tokens) eq "\n" } |
---|
2997 | } elsif ($$t =~ /^%\#(.)\z/s) { # provide number of elements |
---|
2998 | if (!exists($builtins_href->{$1})) { $s = 0 } # no such |
---|
2999 | else { |
---|
3000 | $s = $builtins_href->{$1}; |
---|
3001 | while (ref($s) eq 'CODE') { $s = &$s } # subroutine callback |
---|
3002 | # for array: number of elements; for scalar: nonwhite=1, other 0 |
---|
3003 | $s = ref($s) ? @$s : ($s !~ /^\s*\z/); |
---|
3004 | } |
---|
3005 | } elsif ($$t =~ /^%(.)\z/s) { # provide values of a builtin macro |
---|
3006 | if (!exists($builtins_href->{$1})) { $s = '' } # no such |
---|
3007 | else { |
---|
3008 | $s = $builtins_href->{$1}; |
---|
3009 | while (ref($s) eq 'CODE') { $s = &$s } # subroutine callback |
---|
3010 | $s = join(', ', @$s) if ref $s; |
---|
3011 | } |
---|
3012 | } else { $s = $$t } # misplaced token, e.g. a top level | or ] |
---|
3013 | ref($whereto) eq 'ARRAY' ? push(@$whereto, $s) : ($$whereto .= $s); |
---|
3014 | } |
---|
3015 | } |
---|
3016 | \$output_str; |
---|
3017 | } |
---|
3018 | |
---|
3019 | 1; |
---|
3020 | |
---|
3021 | # |
---|
3022 | package Amavis::In::Connection; |
---|
3023 | |
---|
3024 | # Keeps relevant information about how we received the message: |
---|
3025 | # client connection information, SMTP envelope and SMTP parameters |
---|
3026 | |
---|
3027 | use strict; |
---|
3028 | use re 'taint'; |
---|
3029 | |
---|
3030 | BEGIN { |
---|
3031 | use Exporter (); |
---|
3032 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
3033 | $VERSION = '2.034'; |
---|
3034 | @ISA = qw(Exporter); |
---|
3035 | } |
---|
3036 | |
---|
3037 | sub new |
---|
3038 | { my($class) = @_; bless {}, $class } |
---|
3039 | sub client_ip # client IP address (immediate SMTP client, i.e. our MTA) |
---|
3040 | { my($self)=shift; !@_ ? $self->{client_ip} : ($self->{client_ip}=shift) } |
---|
3041 | sub socket_ip # IP address of our interface that received connection |
---|
3042 | { my($self)=shift; !@_ ? $self->{socket_ip} : ($self->{socket_ip}=shift) } |
---|
3043 | sub socket_port # TCP port of our interface that received connection |
---|
3044 | { my($self)=shift; !@_ ? $self->{socket_port}:($self->{socket_port}=shift) } |
---|
3045 | sub proto # TCP/UNIX |
---|
3046 | { my($self)=shift; !@_ ? $self->{proto} : ($self->{proto}=shift) } |
---|
3047 | sub smtp_proto # SMTP/ESMTP(A|S|SA)/LMTP(A|S|SA) # rfc3848, or QMQP/QMQPqq |
---|
3048 | { my($self)=shift; !@_ ? $self->{smtp_proto}: ($self->{smtp_proto}=shift) } |
---|
3049 | sub smtp_helo # (E)SMTP HELO/EHLO parameter |
---|
3050 | { my($self)=shift; !@_ ? $self->{smtp_helo} : ($self->{smtp_helo}=shift) } |
---|
3051 | |
---|
3052 | 1; |
---|
3053 | |
---|
3054 | # |
---|
3055 | package Amavis::In::Message::PerRecip; |
---|
3056 | |
---|
3057 | use strict; |
---|
3058 | use re 'taint'; |
---|
3059 | |
---|
3060 | BEGIN { |
---|
3061 | use Exporter (); |
---|
3062 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
3063 | $VERSION = '2.034'; |
---|
3064 | @ISA = qw(Exporter); |
---|
3065 | } |
---|
3066 | |
---|
3067 | # per-recipient data are kept in an array of n-tuples: |
---|
3068 | # (recipient-address, destiny, done, smtp-response-text, remote-mta, ...) |
---|
3069 | sub new # NOTE: this class is a list, not a hash |
---|
3070 | { my($class) = @_; bless [(undef) x 11], $class } |
---|
3071 | |
---|
3072 | # subs to set or access individual elements of a n-tuple by name |
---|
3073 | sub recip_addr # recipient envelope e-mail address |
---|
3074 | { my($self)=shift; !@_ ? $$self[0] : ($$self[0]=shift) } |
---|
3075 | sub recip_addr_modified |
---|
3076 | { my($self)=shift; !@_ ? $$self[1] : ($$self[1]=shift) } |
---|
3077 | sub recip_destiny # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS |
---|
3078 | { my($self)=shift; !@_ ? $$self[2] : ($$self[2]=shift) } |
---|
3079 | sub recip_done # false: not done, true: done (1: faked, 2: truly sent) |
---|
3080 | { my($self)=shift; !@_ ? $$self[3] : ($$self[3]=shift) } |
---|
3081 | sub recip_smtp_response # rfc2821 response (3-digit + enhanced resp + text) |
---|
3082 | { my($self)=shift; !@_ ? $$self[4] : ($$self[4]=shift) } |
---|
3083 | sub recip_remote_mta_smtp_response # smtp response as issued by remote MTA |
---|
3084 | { my($self)=shift; !@_ ? $$self[5] : ($$self[5]=shift) } |
---|
3085 | sub recip_remote_mta # remote MTA that issued the smtp response |
---|
3086 | { my($self)=shift; !@_ ? $$self[6] : ($$self[6]=shift) } |
---|
3087 | sub recip_mbxname # mailbox name or file when known ('local:' or 'bsmtp:') |
---|
3088 | { my($self)=shift; !@_ ? $$self[7] : ($$self[7]=shift) } |
---|
3089 | sub recip_whitelisted_sender # recip considers this sender whitelisted (> 0) |
---|
3090 | { my($self)=shift; !@_ ? $$self[8] : ($$self[8]=shift) } |
---|
3091 | sub recip_blacklisted_sender # recip considers this sender blacklisted |
---|
3092 | { my($self)=shift; !@_ ? $$self[9] : ($$self[9]=shift) } |
---|
3093 | sub recip_score_boost # recip adds penalty spam points to the final score |
---|
3094 | { my($self)=shift; !@_ ? $$self[10] : ($$self[10]=shift) } |
---|
3095 | |
---|
3096 | sub recip_final_addr { # return recip_addr_modified if set, else recip_addr |
---|
3097 | my($self)=shift; |
---|
3098 | my($newaddr) = $self->recip_addr_modified; |
---|
3099 | defined $newaddr ? $newaddr : $self->recip_addr; |
---|
3100 | } |
---|
3101 | |
---|
3102 | 1; |
---|
3103 | |
---|
3104 | # |
---|
3105 | package Amavis::In::Message; |
---|
3106 | # the main purpose of this class is to contain information |
---|
3107 | # about the message being processed |
---|
3108 | |
---|
3109 | use strict; |
---|
3110 | use re 'taint'; |
---|
3111 | |
---|
3112 | BEGIN { |
---|
3113 | use Exporter (); |
---|
3114 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
3115 | $VERSION = '2.034'; |
---|
3116 | @ISA = qw(Exporter); |
---|
3117 | } |
---|
3118 | |
---|
3119 | BEGIN { |
---|
3120 | import Amavis::Conf qw( :platform ); |
---|
3121 | import Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp); |
---|
3122 | import Amavis::In::Message::PerRecip; |
---|
3123 | } |
---|
3124 | |
---|
3125 | sub new |
---|
3126 | { my($class) = @_; bless {}, $class } |
---|
3127 | sub rx_time # Unix time (s since epoch) of message reception by amavisd |
---|
3128 | { my($self)=shift; !@_ ? $self->{rx_time} : ($self->{rx_time}=shift) } |
---|
3129 | sub client_addr # original client IP addr, obtained from XFORWARD or milter |
---|
3130 | { my($self)=shift; !@_ ? $self->{cli_ip} : ($self->{cli_ip}=shift) } |
---|
3131 | sub client_name # orig. client DNS name, obtained from XFORWARD or milter |
---|
3132 | { my($self)=shift; !@_ ? $self->{cli_name} : ($self->{cli_name}=shift) } |
---|
3133 | sub client_proto # orig. client protocol, obtained from XFORWARD or milter |
---|
3134 | { my($self)=shift; !@_ ? $self->{cli_proto} : ($self->{cli_proto}=shift) } |
---|
3135 | sub client_helo # orig. client EHLO name, obtained from XFORWARD or milter |
---|
3136 | { my($self)=shift; !@_ ? $self->{cli_helo} : ($self->{cli_helo}=shift) } |
---|
3137 | sub queue_id # MTA queue ID of message if known (Courier, milter/AM.PDP) |
---|
3138 | { my($self)=shift; !@_ ? $self->{queue_id} : ($self->{queue_id}=shift) } |
---|
3139 | sub msg_size # ESMTP SIZE value, later corrected by actual message size |
---|
3140 | { my($self)=shift; !@_ ? $self->{msg_size} : ($self->{msg_size}=shift) } |
---|
3141 | sub auth_user # ESMTP AUTH username |
---|
3142 | { my($self)=shift; !@_ ? $self->{auth_user} : ($self->{auth_user}=shift) } |
---|
3143 | sub auth_pass # ESMTP AUTH password |
---|
3144 | { my($self)=shift; !@_ ? $self->{auth_pass} : ($self->{auth_pass}=shift) } |
---|
3145 | sub auth_submitter # ESMTP MAIL command AUTH option value |
---|
3146 | { my($self)=shift; !@_ ? $self->{auth_subm} : ($self->{auth_subm}=shift) } |
---|
3147 | sub body_type # ESMTP BODY parameter value |
---|
3148 | { my($self)=shift; !@_ ? $self->{body_type} : ($self->{body_type}=shift) } |
---|
3149 | sub sender # envelope sender |
---|
3150 | { my($self)=shift; !@_ ? $self->{sender} : ($self->{sender}=shift) } |
---|
3151 | sub sender_contact # unmangled sender address or undef (e.g. believed faked) |
---|
3152 | { my($self)=shift; !@_ ? $self->{sender_c} : ($self->{sender_c}=shift) } |
---|
3153 | sub sender_source # unmangled sender address or info from the trace |
---|
3154 | { my($self)=shift; !@_ ? $self->{sender_src} : ($self->{sender_src}=shift) } |
---|
3155 | sub mime_entity # MIME::Parser entity holding the message |
---|
3156 | { my($self)=shift; !@_ ? $self->{mime_entity}: ($self->{mime_entity}=shift)} |
---|
3157 | sub parts_root # Amavis::Unpackers::Part root object |
---|
3158 | { my($self)=shift; !@_ ? $self->{parts_root}: ($self->{parts_root}=shift)} |
---|
3159 | sub mail_text # rfc2822 msg: (open) file handle, or MIME::Entity object |
---|
3160 | { my($self)=shift; !@_ ? $self->{mail_text} : ($self->{mail_text}=shift) } |
---|
3161 | sub mail_text_fn # orig. mail filename or undef, e.g. mail_tempdir/email.txt |
---|
3162 | { my($self)=shift; !@_ ? $self->{mail_text_fn} : ($self->{mail_text_fn}=shift) } |
---|
3163 | sub mail_tempdir # work directory, either $TEMPBASE or supplied by client |
---|
3164 | { my($self)=shift; !@_ ? $self->{mail_tempdir} : ($self->{mail_tempdir}=shift) } |
---|
3165 | sub header_edits # Amavis::Out::EditHeader object or undef |
---|
3166 | { my($self)=shift; !@_ ? $self->{hdr_edits} : ($self->{hdr_edits}=shift) } |
---|
3167 | sub orig_header # original header - an arrayref of lines, with trailing LF |
---|
3168 | { my($self)=shift; !@_ ? $self->{orig_header}: ($self->{orig_header}=shift) } |
---|
3169 | sub orig_header_size # size of original header |
---|
3170 | { my($self)=shift; !@_ ? $self->{orig_hdr_s} : ($self->{orig_hdr_s}=shift) } |
---|
3171 | sub orig_body_size # size of original body |
---|
3172 | { my($self)=shift; !@_ ? $self->{orig_bdy_s} : ($self->{orig_bdy_s}=shift) } |
---|
3173 | sub body_digest # message digest of message body |
---|
3174 | { my($self)=shift; !@_ ? $self->{body_digest}: ($self->{body_digest}=shift) } |
---|
3175 | sub quarantined_to # list of quarantine mailbox names or addresses if quarantined |
---|
3176 | { my($self)=shift; !@_ ? $self->{quarantine} : ($self->{quarantine}=shift) } |
---|
3177 | sub dsn_sent # delivery status notification was sent(1) or faked(2) |
---|
3178 | { my($self)=shift; !@_ ? $self->{dsn_sent} : ($self->{dsn_sent}=shift) } |
---|
3179 | sub delivery_method # delivery method, or empty for implicit delivery (milter) |
---|
3180 | { my($self)=shift; !@_ ? $self->{delivery_method} : ($self->{delivery_method}=shift) } |
---|
3181 | sub client_delete # don't delete the tempdir, it is a client's reponsibility |
---|
3182 | { my($self)=shift; !@_ ? $self->{client_delete} : ($self->{client_delete}=shift) } |
---|
3183 | |
---|
3184 | # The order of entries in the list is the original order in which |
---|
3185 | # recipient addresses (e.g. obtained via 'MAIL TO:') were received. |
---|
3186 | # Only the entries that were accepted (via SMTP response code 2xx) |
---|
3187 | # are placed in the list. The ORDER MUST BE PRESERVED and no recipients |
---|
3188 | # may be added or removed from the list! This is vital in order to be able |
---|
3189 | # to produce correct per-recipient responses to a LMTP client! |
---|
3190 | # 'destiny' values match the meaning of 'final_*_destiny' |
---|
3191 | |
---|
3192 | sub per_recip_data { # get or set a listref of envelope recipient n-tuples |
---|
3193 | my($self) = shift; |
---|
3194 | # store a given listref of n-tuples (originals, not copies!) |
---|
3195 | if (@_) { @{$self->{recips}} = @{$_[0]} } |
---|
3196 | # return a listref to the original n-tuples, |
---|
3197 | # caller may modify the data if he knows what he is doing |
---|
3198 | $self->{recips}; |
---|
3199 | } |
---|
3200 | |
---|
3201 | sub recips { # get or set a listref of envelope recipients |
---|
3202 | my($self)=shift; |
---|
3203 | if (@_) { # store a copy of a given listref of recipient addresses |
---|
3204 | # wrap scalars (strings) into n-tuples |
---|
3205 | $self->per_recip_data([ map { |
---|
3206 | my($per_recip_obj) = Amavis::In::Message::PerRecip->new; |
---|
3207 | $per_recip_obj->recip_addr($_); |
---|
3208 | $per_recip_obj->recip_destiny(D_PASS); # default is Pass |
---|
3209 | $per_recip_obj } @{$_[0]} ]); |
---|
3210 | } |
---|
3211 | return if !defined wantarray; # don't bother |
---|
3212 | # return listref of recipient addresses |
---|
3213 | [ map { $_->recip_addr } @{$self->per_recip_data} ]; |
---|
3214 | } |
---|
3215 | |
---|
3216 | 1; |
---|
3217 | |
---|
3218 | # |
---|
3219 | package Amavis::Out::EditHeader; |
---|
3220 | |
---|
3221 | # Accumulates instructions on what lines need to be added to the message |
---|
3222 | # header, deleted, or how to change existing lines, then via a call |
---|
3223 | # to write_header() performs these edits on the fly. |
---|
3224 | |
---|
3225 | use strict; |
---|
3226 | use re 'taint'; |
---|
3227 | |
---|
3228 | BEGIN { |
---|
3229 | use Exporter (); |
---|
3230 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
3231 | $VERSION = '2.034'; |
---|
3232 | @ISA = qw(Exporter); |
---|
3233 | @EXPORT_OK = qw(&hdr); |
---|
3234 | } |
---|
3235 | |
---|
3236 | BEGIN { |
---|
3237 | import Amavis::Conf qw(:platform c cr ca); |
---|
3238 | import Amavis::Timing qw(section_time); |
---|
3239 | import Amavis::Util qw(ll do_log safe_encode q_encode); |
---|
3240 | } |
---|
3241 | use MIME::Words; |
---|
3242 | |
---|
3243 | sub new { my($class) = @_; bless {}, $class } |
---|
3244 | |
---|
3245 | sub prepend_header($$$;$) { |
---|
3246 | my($self, $field_name, $field_body, $structured) = @_; |
---|
3247 | unshift(@{$self->{prepend}}, hdr($field_name, $field_body, $structured)); |
---|
3248 | } |
---|
3249 | |
---|
3250 | sub append_header($$$;$) { |
---|
3251 | my($self, $field_name, $field_body, $structured) = @_; |
---|
3252 | push(@{$self->{append}}, hdr($field_name, $field_body, $structured)); |
---|
3253 | } |
---|
3254 | |
---|
3255 | sub delete_header($$) { |
---|
3256 | my($self, $field_name) = @_; |
---|
3257 | $self->{edit}{lc($field_name)} = undef; |
---|
3258 | } |
---|
3259 | |
---|
3260 | sub edit_header($$$;$) { |
---|
3261 | my($self, $field_name, $field_edit_sub, $structured) = @_; |
---|
3262 | # $field_edit_sub will be called with 2 args: field name and field body; |
---|
3263 | # it should return the replacement field body (no field name and colon), |
---|
3264 | # with or without the trailing NL |
---|
3265 | !defined($field_edit_sub) || ref($field_edit_sub) eq 'CODE' |
---|
3266 | or die "edit_header: arg#3 must be undef or a subroutine ref"; |
---|
3267 | $self->{edit}{lc($field_name)} = $field_edit_sub; |
---|
3268 | } |
---|
3269 | |
---|
3270 | # copy all header edits from another header-edits object into this one |
---|
3271 | sub inherit_header_edits($$) { |
---|
3272 | my($self, $other_edits) = @_; |
---|
3273 | if (defined $other_edits) { |
---|
3274 | unshift(@{$self->{prepend}}, |
---|
3275 | @{$other_edits->{prepend}}) if $other_edits->{prepend}; |
---|
3276 | unshift(@{$self->{append}}, |
---|
3277 | @{$other_edits->{append}}) if $other_edits->{append}; |
---|
3278 | if ($other_edits->{edit}) { |
---|
3279 | for (keys %{$other_edits->{edit}}) |
---|
3280 | { $self->{edit}{$_} = $other_edits->{edit}{$_} } |
---|
3281 | } |
---|
3282 | } |
---|
3283 | } |
---|
3284 | |
---|
3285 | # Insert space after colon if not present, RFC2047-encode if field body |
---|
3286 | # contains non-ASCII characters, fold long lines if needed, |
---|
3287 | # prepend space before each NL if missing, append NL if missing; |
---|
3288 | # Header fields with only spaces are not allowed. |
---|
3289 | # (rfc2822: Each line of characters MUST be no more than 998 characters, |
---|
3290 | # and SHOULD be no more than 78 characters, excluding the CRLF. |
---|
3291 | # '$structured' indicates that folding is only allowed at positions |
---|
3292 | # indicated by \n in the provided header body. |
---|
3293 | # |
---|
3294 | sub hdr($$;$) { |
---|
3295 | my($field_name, $field_body, $structured) = @_; |
---|
3296 | if ($field_name =~ /^(X-.*|Subject|Comments)\z/si && |
---|
3297 | $field_body =~ /[^\011\012\040-\176]/ #any nonprintable except TAB and LF |
---|
3298 | ) { # encode according to RFC 2047 |
---|
3299 | $field_body =~ s/\n([ \t])/$1/g; # unfold |
---|
3300 | chomp($field_body); |
---|
3301 | my($field_body_octets) = safe_encode(c('hdr_encoding'), $field_body); |
---|
3302 | my($qb) = c('hdr_encoding_qb'); |
---|
3303 | if (uc($qb) eq 'Q') { |
---|
3304 | $field_body = q_encode($field_body_octets, $qb, c('hdr_encoding')); |
---|
3305 | } else { |
---|
3306 | $field_body = MIME::Words::encode_mimeword($field_body_octets, |
---|
3307 | $qb, c('hdr_encoding')); |
---|
3308 | } |
---|
3309 | } else { # supposed to be in plain ASCII, let's make sure it is |
---|
3310 | $field_body = safe_encode('ascii', $field_body); |
---|
3311 | } |
---|
3312 | $field_name = safe_encode('ascii', $field_name); |
---|
3313 | my($str) = $field_name . ':'; |
---|
3314 | $str .= ' ' if $field_body !~ /^[ \t]/; |
---|
3315 | $str .= $field_body; |
---|
3316 | $str =~ s/\n([^ \t\n])/\n $1/g; # insert a space at line folds if missing |
---|
3317 | $str =~ s/\n([ \t]*\n)+/\n/g; # remove empty lines |
---|
3318 | chomp($str); # chop off trailing NL if present |
---|
3319 | if ($structured) { |
---|
3320 | my(@sublines) = split(/\n/, $str, -1); |
---|
3321 | $str = ''; my($s) = ''; my($s_l) = 0; |
---|
3322 | for (@sublines) { # join shorter field sections |
---|
3323 | if ($s !~ /^\s*\z/ && $s_l + length($_) > 78) { |
---|
3324 | $str .= "\n" if $str ne ''; |
---|
3325 | $str .= $s; $s = ''; $s_l = 0; |
---|
3326 | } |
---|
3327 | $s .= $_; $s_l += length($_); |
---|
3328 | } |
---|
3329 | if ($s !~ /^\s*\z/) { |
---|
3330 | $str .= "\n" if $str ne ''; |
---|
3331 | $str .= $s; |
---|
3332 | } |
---|
3333 | } elsif (length($str) > 998) { |
---|
3334 | # truncate the damn thing (to be done better) |
---|
3335 | $str = substr($str,0,998); |
---|
3336 | } |
---|
3337 | $str .= "\n"; # append final NL |
---|
3338 | do_log(5, "header: $str"); |
---|
3339 | $str; |
---|
3340 | } |
---|
3341 | |
---|
3342 | # Copy mail header to the supplied method (line by line) while adding, |
---|
3343 | # removing, or changing certain header lines as required, and append |
---|
3344 | # an empty line (end-of-header). Returns number of original 'Received:' |
---|
3345 | # header fields to make simple loop detection possible (as required |
---|
3346 | # by rfc2821 section 6.2). |
---|
3347 | # |
---|
3348 | # Assumes input file is properly positioned, leaves it positioned |
---|
3349 | # at the beginning of the body. |
---|
3350 | # |
---|
3351 | sub write_header($$$) { |
---|
3352 | my($self, $msg, $out_fh) = @_; |
---|
3353 | $out_fh = IO::Wrap::wraphandle($out_fh); # assure an IO::Handle-like obj |
---|
3354 | my($is_mime) = ref($msg) && $msg->isa('MIME::Entity'); |
---|
3355 | my(@header); |
---|
3356 | if ($is_mime) { |
---|
3357 | @header = map { /^[ \t]*\n?\z/ ? () # remove empty lines, ensure NL |
---|
3358 | : (/\n\z/ ? $_ : $_ . "\n") } @{$msg->header}; |
---|
3359 | } |
---|
3360 | my($received_cnt) = 0; my($str) = ''; |
---|
3361 | for (@{$self->{prepend}}) { $str .= $_ } |
---|
3362 | if ($str ne '') { $out_fh->print($str) or die "sending mail header1: $!" } |
---|
3363 | if (!defined($msg)) { |
---|
3364 | # existing header empty |
---|
3365 | } elsif (!exists($self->{edit}) || !scalar(%{$self->{edit}})) { |
---|
3366 | # no edits needed, do it the fast way |
---|
3367 | if ($is_mime) { |
---|
3368 | # NOTE: can't use method print_header, as it assumes file glob |
---|
3369 | for my $h (@header) |
---|
3370 | { $out_fh->print($h) or die "sending mail header2: $!" } |
---|
3371 | } else { # assume file handle |
---|
3372 | while (<$msg>) { # copy header only, read line by line |
---|
3373 | last if $_ eq $eol; # end of header |
---|
3374 | $out_fh->print($_) or die "sending mail header3: $!"; |
---|
3375 | } |
---|
3376 | } |
---|
3377 | } else { # header edits are required |
---|
3378 | my($curr_head, $next_head); |
---|
3379 | push(@header, $eol) if $is_mime; # append empty line as end-of-header |
---|
3380 | while (defined($next_head = $is_mime ? shift @header : <$msg>)) { |
---|
3381 | if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head } # folded |
---|
3382 | else { # new header |
---|
3383 | if (!defined($curr_head)) { # no previous complete header field |
---|
3384 | } elsif ($curr_head !~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s) { |
---|
3385 | # invalid header, but we don't care |
---|
3386 | $out_fh->print($curr_head) or die "sending mail header4: $!"; |
---|
3387 | } else { # count, edit, or delete |
---|
3388 | # obsolete rfc822 syntax allowed whitespace before colon |
---|
3389 | my($field_name, $field_body) = ($1, $2); |
---|
3390 | my($field_name_lc) = lc($field_name); |
---|
3391 | $received_cnt++ if $field_name_lc eq 'received'; |
---|
3392 | if (!exists($self->{edit}{$field_name_lc})) { # unchanged |
---|
3393 | $out_fh->print($curr_head) or die "sending mail header5: $!"; |
---|
3394 | } else { |
---|
3395 | my($edit) = $self->{edit}{$field_name_lc}; |
---|
3396 | if (defined($edit)) { # edit, not delete |
---|
3397 | chomp($field_body); |
---|
3398 | ### $field_body =~ s/\n([ \t])/$1/g; # unfold |
---|
3399 | $out_fh->print(hdr($field_name, &$edit($field_name,$field_body))) |
---|
3400 | or die "sending mail header6: $!"; |
---|
3401 | } |
---|
3402 | } |
---|
3403 | } |
---|
3404 | last if $next_head eq $eol; # end-of-header reached |
---|
3405 | $curr_head = $next_head; |
---|
3406 | } |
---|
3407 | } |
---|
3408 | } |
---|
3409 | $str = ''; |
---|
3410 | for (@{$self->{append}}) { $str .= $_ } |
---|
3411 | $str .= $eol; # end of header - separator line |
---|
3412 | $out_fh->print($str) or die "sending mail header7: $!"; |
---|
3413 | section_time('write-header'); |
---|
3414 | $received_cnt; |
---|
3415 | } |
---|
3416 | 1; |
---|
3417 | |
---|
3418 | # |
---|
3419 | package Amavis::Out::Local; |
---|
3420 | use strict; |
---|
3421 | use re 'taint'; |
---|
3422 | |
---|
3423 | BEGIN { |
---|
3424 | use Exporter (); |
---|
3425 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
3426 | $VERSION = '2.034'; |
---|
3427 | @ISA = qw(Exporter); |
---|
3428 | @EXPORT_OK = qw(&mail_to_local_mailbox); |
---|
3429 | } |
---|
3430 | |
---|
3431 | use Errno qw(ENOENT EACCES); |
---|
3432 | use POSIX qw(strftime); |
---|
3433 | use IO::File (); |
---|
3434 | use IO::Wrap; |
---|
3435 | |
---|
3436 | BEGIN { |
---|
3437 | import Amavis::Conf qw(:platform $gzip $bzip2 c cr ca); |
---|
3438 | import Amavis::Lock; |
---|
3439 | import Amavis::Util qw(ll do_log am_id exit_status_str run_command_consumer); |
---|
3440 | import Amavis::Timing qw(section_time); |
---|
3441 | import Amavis::rfc2821_2822_Tools; |
---|
3442 | import Amavis::Out::EditHeader; |
---|
3443 | } |
---|
3444 | |
---|
3445 | use subs @EXPORT_OK; |
---|
3446 | |
---|
3447 | # Deliver to local mailboxes only, ignore the rest: either to directory |
---|
3448 | # (maildir style), or file (Unix mbox). (normally used as a quarantine method) |
---|
3449 | # |
---|
3450 | sub mail_to_local_mailbox(@) { |
---|
3451 | my($via, $msginfo, $initial_submission, $filter) = @_; |
---|
3452 | $via =~ /^local:(.*)\z/si or die "Bad local method: $via"; |
---|
3453 | my($via_arg) = $1; |
---|
3454 | my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
3455 | @{$msginfo->per_recip_data}; |
---|
3456 | return 1 if !@per_recip_data; |
---|
3457 | my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object |
---|
3458 | if (defined($msg) && !$msg->isa('MIME::Entity')) { |
---|
3459 | # at this point, we have no idea what the user gave us... |
---|
3460 | # a globref? a FileHandle? |
---|
3461 | $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj |
---|
3462 | } |
---|
3463 | my($sender) = $msginfo->sender; |
---|
3464 | for my $r (@per_recip_data) { |
---|
3465 | # each one receives its own copy; these are not the original recipients |
---|
3466 | my($recip) = $r->recip_final_addr; |
---|
3467 | next if $recip eq ''; |
---|
3468 | my($localpart,$domain) = split_address($recip); |
---|
3469 | my($smtp_response); |
---|
3470 | |
---|
3471 | # %local_delivery_aliases emulates aliases map - this would otherwise |
---|
3472 | # be done by MTA's local delivery agent if we gave the message to MTA. |
---|
3473 | # This way we keep interface compatible with other mail delivery |
---|
3474 | # methods. The hash value may be a ref to a pair of fixed strings, |
---|
3475 | # or a subroutine ref (which must return such pair) to allow delayed |
---|
3476 | # (lazy) evaluation when some part of the pair is not yet known |
---|
3477 | # at initialization time. |
---|
3478 | # If no matching entry is found, the key ($localpart) is treated as |
---|
3479 | # a mailbox filename if nonempty, or else quarantining is skipped. |
---|
3480 | |
---|
3481 | my($mbxname, $suggested_filename); |
---|
3482 | { # a block is used as a 'switch' statement - 'last' will exit from it |
---|
3483 | my($ldar) = cr('local_delivery_aliases'); # a ref to a hash |
---|
3484 | my($alias) = $ldar->{$localpart}; |
---|
3485 | if (ref($alias) eq 'ARRAY') { |
---|
3486 | ($mbxname, $suggested_filename) = @$alias; |
---|
3487 | } elsif (ref($alias) eq 'CODE') { # lazy evaluation |
---|
3488 | ($mbxname, $suggested_filename) = &$alias; |
---|
3489 | } elsif ($alias ne '') { |
---|
3490 | ($mbxname, $suggested_filename) = ($alias, undef); |
---|
3491 | } elsif (!exists $ldar->{$localpart}) { |
---|
3492 | do_log(0, "no key '$localpart' in \%local_delivery_aliases, skip local delivery"); |
---|
3493 | } |
---|
3494 | if ($mbxname eq '') { |
---|
3495 | my($why) = !exists $ldar->{$localpart} ? 1 : $alias eq '' ? 2 : 3; |
---|
3496 | do_log(2, "skip local delivery($why): <$sender> -> <$recip>"); |
---|
3497 | $smtp_response = "250 2.6.0 Ok, skip local delivery($why)"; |
---|
3498 | last; # exit block, not the loop |
---|
3499 | } |
---|
3500 | my($ux); # is it a UNIX-style mailbox? |
---|
3501 | if (!-d $mbxname) { # assume a filename (need not exist yet) |
---|
3502 | $ux = 1; # $mbxname is a UNIX-style mailbox (one file) |
---|
3503 | } else { # a directory |
---|
3504 | $ux = 0; # $mbxname is a amavis/maildir style mailbox (a directory) |
---|
3505 | if ($suggested_filename eq '') |
---|
3506 | { $suggested_filename = $via_arg ne '' ? $via_arg : 'msg-%i-%n' } |
---|
3507 | $suggested_filename =~ s{%(.)} |
---|
3508 | { $1 eq 'b' ? $msginfo->body_digest |
---|
3509 | : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time)) |
---|
3510 | : $1 eq 'n' ? am_id() |
---|
3511 | : $1 eq '%' ? '%' : '%'.$1 }egs; |
---|
3512 | # one mail per file, will create specified file |
---|
3513 | $mbxname = "$mbxname/$suggested_filename"; |
---|
3514 | } |
---|
3515 | do_log(1, "local delivery: <$sender> -> <$recip>, mbx=$mbxname"); |
---|
3516 | my($mp,$pos,$pipe,$pid); |
---|
3517 | my($errn) = stat($mbxname) ? 0 : 0+$!; |
---|
3518 | local $SIG{CHLD} = 'DEFAULT'; |
---|
3519 | local $SIG{PIPE} = 'IGNORE'; # write to broken pipe would throw a signal |
---|
3520 | eval { # try to open the mailbox file for writing |
---|
3521 | if (!$ux) { # new file, traditional amavis, or maildir |
---|
3522 | if ($errn == ENOENT) { # good, no file, as expected |
---|
3523 | } elsif (!$errn && -e _) |
---|
3524 | { die "File $mbxname already exists, refuse to overwrite" } |
---|
3525 | if ($mbxname =~ /\.gz\z/) { |
---|
3526 | ($mp,$pid) = run_command_consumer($mbxname,undef,$gzip); |
---|
3527 | $pipe = 1; |
---|
3528 | } else { |
---|
3529 | $mp = IO::File->new; |
---|
3530 | $mp->open($mbxname,'>',0640) |
---|
3531 | or die "Can't create file $mbxname: $!"; |
---|
3532 | } |
---|
3533 | } else { # append to UNIX-style mailbox |
---|
3534 | # deliver only to non-executable regular files |
---|
3535 | if ($errn == ENOENT) { |
---|
3536 | $mp = IO::File->new; |
---|
3537 | $mp->open($mbxname,'>',0640) |
---|
3538 | or die "Can't create file $mbxname: $!"; |
---|
3539 | } elsif (!$errn && !-f _) { |
---|
3540 | die "Mailbox $mbxname is not a regular file, refuse to deliver"; |
---|
3541 | } elsif (-x _ || -X _) { |
---|
3542 | die "Mailbox file $mbxname is executable, refuse to deliver"; |
---|
3543 | } else { |
---|
3544 | $mp = IO::File->new; |
---|
3545 | $mp->open($mbxname,'>>',0640) or die "Can't append to $mbxname: $!"; |
---|
3546 | } |
---|
3547 | binmode($mp, ":bytes") or die "Can't cancel :utf8 mode: $!" |
---|
3548 | if $unicode_aware; |
---|
3549 | lock($mp); |
---|
3550 | $mp->seek(0,2) or die "Can't position mailbox file to its tail: $!"; |
---|
3551 | $pos = $mp->tell; |
---|
3552 | } |
---|
3553 | if (defined($msg) && !$msg->isa('MIME::Entity')) |
---|
3554 | { $msg->seek(0,0) or die "Can't rewind mail file: $!" } |
---|
3555 | }; |
---|
3556 | if ($@ ne '') { |
---|
3557 | chomp($@); |
---|
3558 | $smtp_response = $@ eq "timed out" ? "450 4.4.2" : "451 4.5.0"; |
---|
3559 | $smtp_response .= " Local delivery(1) to $mbxname failed: $@"; |
---|
3560 | last; # exit block, not the loop |
---|
3561 | } |
---|
3562 | eval { # if things fail from here on, try to restore mailbox state |
---|
3563 | if ($ux) { |
---|
3564 | $mp->printf("From %s %s$eol", quote_rfc2821_local($sender), |
---|
3565 | scalar(localtime($msginfo->rx_time)) ) |
---|
3566 | or die "Can't write to $mbxname: $!"; |
---|
3567 | } |
---|
3568 | my($hdr_edits) = $msginfo->header_edits; |
---|
3569 | if (!$hdr_edits) { |
---|
3570 | $hdr_edits = Amavis::Out::EditHeader->new; |
---|
3571 | $msginfo->header_edits($hdr_edits); |
---|
3572 | } |
---|
3573 | $hdr_edits->delete_header('Return-Path'); |
---|
3574 | $hdr_edits->prepend_header('Delivered-To', |
---|
3575 | quote_rfc2821_local($recip)); |
---|
3576 | $hdr_edits->prepend_header('Return-Path', |
---|
3577 | qquote_rfc2821_local($sender)); |
---|
3578 | my($received_cnt) = $hdr_edits->write_header($msg,$mp); |
---|
3579 | if ($received_cnt > 110) { |
---|
3580 | # loop detection required by rfc2821 section 6.2 |
---|
3581 | # Do not modify the signal text, it gets matched elsewhere! |
---|
3582 | die "Too many hops: $received_cnt 'Received:' header lines\n"; |
---|
3583 | } |
---|
3584 | if (!$ux) { # do it in blocks for speed if we can |
---|
3585 | while ($msg->read($_,16384) > 0) |
---|
3586 | { $mp->print($_) or die "Can't write to $mbxname: $!" } |
---|
3587 | } else { # for UNIX-style mailbox delivery: escape 'From ' |
---|
3588 | my($blank_line) = 1; |
---|
3589 | while (<$msg>) { |
---|
3590 | $mp->print('>') or die "Can't write to $mbxname: $!" |
---|
3591 | if $blank_line && /^From /; |
---|
3592 | $mp->print($_) or die "Can't write to $mbxname: $!"; |
---|
3593 | $blank_line = $_ eq $eol; |
---|
3594 | } |
---|
3595 | } |
---|
3596 | # must append an empty line for a Unix mailbox format |
---|
3597 | $mp->print($eol) or die "Can't write to $mbxname: $!" if $ux; |
---|
3598 | }; |
---|
3599 | my($failed) = 0; |
---|
3600 | if ($@ ne '') { # trouble |
---|
3601 | chomp($@); |
---|
3602 | if ($ux && defined($pos) && $can_truncate) { |
---|
3603 | # try to restore UNIX-style mailbox to previous size; |
---|
3604 | # Produces a fatal error if truncate isn't implemented |
---|
3605 | # on your system. |
---|
3606 | $mp->truncate($pos) or die "Can't truncate file $mbxname: $!"; |
---|
3607 | } |
---|
3608 | $failed = 1; |
---|
3609 | } |
---|
3610 | unlock($mp) if $ux; |
---|
3611 | if (!$pipe) { |
---|
3612 | $mp->close or die "Can't close $mbxname: $!"; |
---|
3613 | } else { |
---|
3614 | my($err); $mp->close or $err = $!; |
---|
3615 | $?==0 or die ("Closing pipe to $gzip: ".exit_status_str($?,$err)); |
---|
3616 | } |
---|
3617 | if (!$failed) { |
---|
3618 | $smtp_response = "250 2.6.0 Ok, delivered to $mbxname"; |
---|
3619 | } elsif ($@ eq "timed out") { |
---|
3620 | $smtp_response = "450 4.4.2 Local delivery to $mbxname timed out"; |
---|
3621 | } elsif ($@ =~ /too many hops/i) { |
---|
3622 | $smtp_response = "550 5.4.6 Rejected delivery to mailbox $mbxname: $@"; |
---|
3623 | } else { |
---|
3624 | $smtp_response = "451 4.5.0 Local delivery to mailbox $mbxname failed: $@"; |
---|
3625 | } |
---|
3626 | } # end of block, 'last' within block brings us here |
---|
3627 | do_log(-1, $smtp_response) if $smtp_response !~ /^2/; |
---|
3628 | $smtp_response .= ", id=" . am_id(); |
---|
3629 | $r->recip_smtp_response($smtp_response); $r->recip_done(2); |
---|
3630 | $r->recip_mbxname($mbxname) if $mbxname ne '' && $smtp_response =~ /^2/; |
---|
3631 | } |
---|
3632 | section_time('save-to-local-mailbox'); |
---|
3633 | } |
---|
3634 | |
---|
3635 | 1; |
---|
3636 | |
---|
3637 | # |
---|
3638 | package Amavis::Out; |
---|
3639 | use strict; |
---|
3640 | use re 'taint'; |
---|
3641 | |
---|
3642 | BEGIN { |
---|
3643 | use Exporter (); |
---|
3644 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
3645 | $VERSION = '2.034'; |
---|
3646 | @ISA = qw(Exporter); |
---|
3647 | %EXPORT_TAGS = (); |
---|
3648 | @EXPORT = qw(&mail_dispatch); |
---|
3649 | } |
---|
3650 | |
---|
3651 | use IO::File (); |
---|
3652 | use IO::Wrap; |
---|
3653 | use Net::Cmd; |
---|
3654 | use Net::SMTP 2.24; |
---|
3655 | # use Authen::SASL; |
---|
3656 | use POSIX qw(strftime |
---|
3657 | WIFEXITED WIFSIGNALED WIFSTOPPED |
---|
3658 | WEXITSTATUS WTERMSIG WSTOPSIG); |
---|
3659 | BEGIN { |
---|
3660 | import Amavis::Conf qw(:platform $DEBUG $QUARANTINEDIR |
---|
3661 | $relayhost_is_client c cr ca); |
---|
3662 | import Amavis::Util qw(untaint min max ll do_log debug_oneshot |
---|
3663 | am_id snmp_count retcode exit_status_str |
---|
3664 | prolong_timer run_command_consumer); |
---|
3665 | import Amavis::Timing qw(section_time); |
---|
3666 | import Amavis::rfc2821_2822_Tools; |
---|
3667 | import Amavis::Out::Local qw(mail_to_local_mailbox); |
---|
3668 | import Amavis::Out::EditHeader; |
---|
3669 | } |
---|
3670 | |
---|
3671 | # modify delivery method string if $relayhost_is_client and mail came in by TCP |
---|
3672 | sub dynamic_destination($$) { |
---|
3673 | my($method,$conn) = @_; |
---|
3674 | my($client_ip) = !defined($conn) ? undef : $conn->client_ip; |
---|
3675 | if ($client_ip ne '' && $method =~ /^smtp\b/i) { |
---|
3676 | my($new_method); my($relayhost,$relayhost_port,$rest); |
---|
3677 | (undef,$relayhost,$relayhost_port,$rest) = split(/:/,$method,4); |
---|
3678 | if ($relayhost_is_client) # old style |
---|
3679 | { ($relayhost,$relayhost_port,$rest) = ('*','*','') } |
---|
3680 | $relayhost = "[$client_ip]" if $relayhost eq '*'; |
---|
3681 | $relayhost_port = $conn->socket_port+1 if $relayhost_port eq '*'; |
---|
3682 | $rest = ':'.$rest if $rest ne ''; |
---|
3683 | $new_method = sprintf("smtp:%s:%s%s", $relayhost,$relayhost_port,$rest); |
---|
3684 | if ($new_method ne $method) { |
---|
3685 | do_log(3, "dynamic destination override: $method -> $new_method"); |
---|
3686 | $method = $new_method; |
---|
3687 | } |
---|
3688 | } |
---|
3689 | $method; |
---|
3690 | } |
---|
3691 | |
---|
3692 | sub mail_dispatch($$$;$) { |
---|
3693 | my($conn) = shift; my($msginfo,$initial_submission,$filter) = @_; |
---|
3694 | my($via) = $msginfo->delivery_method; |
---|
3695 | if ($via =~ /^smtp:/i) { |
---|
3696 | mail_via_smtp(dynamic_destination($via,$conn), @_); |
---|
3697 | } elsif ($via =~ /^pipe:/i) { |
---|
3698 | mail_via_pipe($via, @_); |
---|
3699 | } elsif ($via =~ /^bsmtp:/i) { |
---|
3700 | mail_via_bsmtp($via, @_); |
---|
3701 | } elsif ($via =~ /^local:/i) { |
---|
3702 | # 'local:' is used by the quarantine code to relieve it |
---|
3703 | # of the need to know which delivery method needs to be used. |
---|
3704 | # Deliver first what is local (whatever does not contain '@') |
---|
3705 | mail_to_local_mailbox($via, $msginfo, $initial_submission, |
---|
3706 | sub { shift->recip_final_addr !~ /\@/ ? 1 : 0 }); |
---|
3707 | if (grep { !$_->recip_done } @{$msginfo->per_recip_data}) { |
---|
3708 | my($nm) = c('notify_method'); # deliver the rest |
---|
3709 | if ($nm =~ /^smtp:/i) { mail_via_smtp(dynamic_destination($nm,$conn),@_)} |
---|
3710 | elsif ($nm =~ /^pipe:/i) { mail_via_pipe($nm, @_) } |
---|
3711 | elsif ($nm =~ /^bsmtp:/i) { mail_via_bsmtp($nm, @_) } |
---|
3712 | } |
---|
3713 | } |
---|
3714 | } |
---|
3715 | |
---|
3716 | #sub Net::Cmd::debug_print { |
---|
3717 | # my($cmd,$out,$text) = @_; |
---|
3718 | # do_log(0, "*** ".$cmd->debug_text($out,$text)) if $out; |
---|
3719 | #} |
---|
3720 | |
---|
3721 | # trivial OO wrapper around Net::SMTP::datasend |
---|
3722 | sub new_smtp_data { my($class, $sh) = @_; bless \$sh, $class } |
---|
3723 | |
---|
3724 | sub print { |
---|
3725 | my($self) = shift; |
---|
3726 | $$self->datasend(\@_) # datasend may be given an array ref |
---|
3727 | or die "datasend timed out while sending header\n"; |
---|
3728 | } |
---|
3729 | |
---|
3730 | # Send mail using SMTP - do multiple transactions if necessary |
---|
3731 | # (e.g. due to '452 Too many recipients') |
---|
3732 | # |
---|
3733 | sub mail_via_smtp(@) { |
---|
3734 | my($via, $msginfo, $initial_submission, $filter) = @_; |
---|
3735 | my($num_recips_undone) = |
---|
3736 | scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
3737 | @{$msginfo->per_recip_data}); |
---|
3738 | while ($num_recips_undone > 0) { |
---|
3739 | mail_via_smtp_single(@_); # send what we can in one transaction |
---|
3740 | my($num_recips_undone_after) = |
---|
3741 | scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
3742 | @{$msginfo->per_recip_data}); |
---|
3743 | if ($num_recips_undone_after >= $num_recips_undone) { |
---|
3744 | do_log(-2, "TROUBLE: Number of recipients ($num_recips_undone_after) " |
---|
3745 | . "not reduced in SMTP transaction, abandon the effort"); |
---|
3746 | last; |
---|
3747 | } |
---|
3748 | if ($num_recips_undone_after > 0) { |
---|
3749 | do_log(1, sprintf("Sent to %s recipients via SMTP, %s still to go", |
---|
3750 | $num_recips_undone - $num_recips_undone_after, |
---|
3751 | $num_recips_undone_after)); |
---|
3752 | } |
---|
3753 | $num_recips_undone = $num_recips_undone_after; |
---|
3754 | } |
---|
3755 | 1; |
---|
3756 | } |
---|
3757 | |
---|
3758 | # Send mail using SMTP - single transaction |
---|
3759 | # (e.g. forwarding original mail or sending notification) |
---|
3760 | # May throw exception (die) if temporary failure (4xx) or other problem |
---|
3761 | # |
---|
3762 | sub mail_via_smtp_single(@) { |
---|
3763 | my($via, $msginfo, $initial_submission, $filter) = @_; |
---|
3764 | my($which_section) = 'fwd_init'; |
---|
3765 | snmp_count('OutMsgs'); |
---|
3766 | local($1,$2,$3); # avoid Perl taint bug, still in 5.8.3 |
---|
3767 | $via =~ /^smtp: (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) /six |
---|
3768 | or die "Bad fwd method syntax: $via"; |
---|
3769 | my($relayhost, $relayhost_port) = ($1.$2, $3); |
---|
3770 | my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
3771 | @{$msginfo->per_recip_data}; |
---|
3772 | my($logmsg) = sprintf("%s via SMTP: [%s]:%s <%s>", |
---|
3773 | ($initial_submission ? 'SEND' : 'FWD'), |
---|
3774 | $relayhost, $relayhost_port, $msginfo->sender); |
---|
3775 | if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 } |
---|
3776 | do_log(1, $logmsg . " -> " . |
---|
3777 | qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data)); |
---|
3778 | my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object |
---|
3779 | my($smtp_handle, $smtp_response); my($smtp_code, $smtp_msg, $received_cnt); |
---|
3780 | my($any_valid_recips) = 0; my($any_tempfail_recips) = 0; |
---|
3781 | my($any_valid_recips_and_data_sent) = 0; my($in_datasend_mode) = 0; |
---|
3782 | if (defined($msg) && !$msg->isa('MIME::Entity')) { |
---|
3783 | # at this point, we have no idea what the user gave us... |
---|
3784 | # a globref? a FileHandle? |
---|
3785 | $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj |
---|
3786 | $msg->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
3787 | } |
---|
3788 | # NOTE: Net::SMTP uses alarm to do its own timing. |
---|
3789 | # We need to restart our timer when Net::SMTP is done using it !!! |
---|
3790 | my($remaining_time) = alarm(0); # check how much time is left, stop timer |
---|
3791 | eval { |
---|
3792 | $which_section = 'fwd-connect'; |
---|
3793 | # Timeout should be more than MTA normally takes to check DNS and RBL, |
---|
3794 | # which may take a minute or more in case of unreachable DNS. |
---|
3795 | # Specifying shorter timeout will cause alarm to terminate the wait |
---|
3796 | # for SMTP status line prematurely, resulting in status code 000. |
---|
3797 | # rfc2821 (section 4.5.3.2) requires timeout to be at least 5 minutes |
---|
3798 | my($localaddr) = c('local_client_bind_address'); # IP assigned to socket |
---|
3799 | my($heloname) = c('localhost_name'); # host name used in HELO/EHLO |
---|
3800 | $smtp_handle = Net::SMTP->new($relayhost, Port => $relayhost_port, |
---|
3801 | ($localaddr eq '' ? () : (LocalAddr => $localaddr)), |
---|
3802 | ($heloname eq '' ? () : (Hello => $heloname)), |
---|
3803 | ExactAddresses => 1, |
---|
3804 | Timeout => max(60, min(5 * 60, $remaining_time)), # for each operation |
---|
3805 | # Timeout => 0, # no timeouts, disable nonblocking mode on socket |
---|
3806 | # Debug => debug_oneshot(), |
---|
3807 | ); |
---|
3808 | defined($smtp_handle) |
---|
3809 | or die "Can't connect to $relayhost port $relayhost_port, $!"; |
---|
3810 | ll(5) && do_log(5,"Remote host presents itself as: ".$smtp_handle->domain); |
---|
3811 | |
---|
3812 | section_time($which_section); |
---|
3813 | prolong_timer($which_section, $remaining_time); # restart timer |
---|
3814 | $remaining_time = undef; |
---|
3815 | |
---|
3816 | $which_section = 'fwd-xforward'; |
---|
3817 | if ($msginfo->client_addr ne '' && $smtp_handle->supports('XFORWARD')) { |
---|
3818 | my($cmd) = join(' ', 'XFORWARD', map |
---|
3819 | { my($n,$v) = @$_; |
---|
3820 | # may encode value as xtext/rfc3461 in future attributes: |
---|
3821 | # char between "!" (33) and "~" (126) inclusive, except "+" and "=" |
---|
3822 | # $v =~ s/[^\041-\052\054-\074\076-\176]/sprintf("+%02X",ord($&))/eg; |
---|
3823 | # Wietse says not to xtext-encode these four attrs, just neuter them |
---|
3824 | $v =~ s/[^\041-\176]/?/g; |
---|
3825 | $v =~ s/[<>()\\";@]/?/g; # other chars that are special in headers |
---|
3826 | # postfix/smtpd/smtpd.c NEUTER_CHARACTERS (but ':' for IPv6) |
---|
3827 | $v = substr($v,0,255) if length($v) > 255; # see XFORWARD_README |
---|
3828 | $v eq '' ? () : ("$n=$v") } |
---|
3829 | ( ['ADDR', $msginfo->client_addr], ['NAME',$msginfo->client_name], |
---|
3830 | ['PROTO',$msginfo->client_proto],['HELO',$msginfo->client_helo] )); |
---|
3831 | do_log(5, "sending $cmd"); |
---|
3832 | $smtp_handle->command($cmd); |
---|
3833 | $smtp_handle->response()==2 or die "sending $cmd\n"; |
---|
3834 | section_time($which_section); prolong_timer($which_section); |
---|
3835 | } |
---|
3836 | |
---|
3837 | $which_section = 'fwd-auth'; |
---|
3838 | my($auth_user) = $msginfo->auth_user; |
---|
3839 | my($mechanisms) = $smtp_handle->supports('AUTH'); |
---|
3840 | if (!c('auth_required_out')) { |
---|
3841 | do_log(3,"AUTH not needed, user='$auth_user', MTA offers '$mechanisms'"); |
---|
3842 | } elsif ($mechanisms eq '') { |
---|
3843 | do_log(3,"INFO: MTA does not offer AUTH capability, user='$auth_user'"); |
---|
3844 | } elsif (!defined $auth_user) { |
---|
3845 | do_log(0,"INFO: AUTH needed for submission but AUTH data not available"); |
---|
3846 | } else { |
---|
3847 | do_log(3,"INFO: authenticating $auth_user, server supports AUTH $mechanisms"); |
---|
3848 | my($sasl) = Authen::SASL->new( |
---|
3849 | 'callback' => { 'user' => $auth_user, 'authname' => $auth_user, |
---|
3850 | 'pass' => $msginfo->auth_pass }); |
---|
3851 | $smtp_handle->auth($sasl) or die "sending AUTH, user=$auth_user\n"; |
---|
3852 | section_time($which_section); prolong_timer($which_section); |
---|
3853 | } |
---|
3854 | |
---|
3855 | $which_section = 'fwd-mail-from'; |
---|
3856 | # how to pass the $msginfo->auth_submitter ???!!! |
---|
3857 | $smtp_handle->mail(qquote_rfc2821_local($msginfo->sender)) |
---|
3858 | or die "sending MAIL FROM\n"; |
---|
3859 | section_time($which_section); prolong_timer($which_section); |
---|
3860 | |
---|
3861 | $which_section = 'fwd-rcpt-to'; |
---|
3862 | my($skipping_resp); |
---|
3863 | for my $r (@per_recip_data) { # send recipient addresses |
---|
3864 | if (defined $skipping_resp) { |
---|
3865 | $r->recip_smtp_response($skipping_resp); $r->recip_done(2); |
---|
3866 | next; |
---|
3867 | } |
---|
3868 | # send a RCPT TO command and get the response |
---|
3869 | $smtp_handle->recipient(qquote_rfc2821_local($r->recip_final_addr)); |
---|
3870 | $smtp_code = $smtp_handle->code; |
---|
3871 | $smtp_msg = $smtp_handle->message; |
---|
3872 | chomp($smtp_msg); |
---|
3873 | my($rcpt_smtp_resp) = "$smtp_code $smtp_msg"; |
---|
3874 | if ($smtp_code =~ /^2/) { |
---|
3875 | $any_valid_recips++; |
---|
3876 | } else { # not ok |
---|
3877 | do_log(3, "response to RCPT TO: \"$rcpt_smtp_resp\""); |
---|
3878 | if ($rcpt_smtp_resp =~ /^0/) { |
---|
3879 | # timeout, what to do, could cause duplicates |
---|
3880 | do_log(-1, "response to RCPT TO not yet available"); |
---|
3881 | $rcpt_smtp_resp = "450 4.4.2 ($rcpt_smtp_resp - probably timed out)"; |
---|
3882 | } |
---|
3883 | $r->recip_remote_mta($relayhost); |
---|
3884 | $r->recip_remote_mta_smtp_response($rcpt_smtp_resp); |
---|
3885 | if ($rcpt_smtp_resp =~ /^ (\d{3}) \s+ ([245] \. \d{1,3} \. \d{1,3})? |
---|
3886 | \s* (.*) \z/xs) |
---|
3887 | { |
---|
3888 | my($resp_code, $resp_enhcode, $resp_msg) = ($1, $2, $3); |
---|
3889 | if ($resp_enhcode eq '' && $resp_code =~ /^([245])/) { |
---|
3890 | my($c1) = $1; |
---|
3891 | $resp_enhcode = $resp_code eq '452' ? |
---|
3892 | "$c1.5.3" : "$c1.1.0"; # insert enhanced code |
---|
3893 | $rcpt_smtp_resp = "$resp_code $resp_enhcode $smtp_msg"; |
---|
3894 | } |
---|
3895 | } |
---|
3896 | if ($rcpt_smtp_resp =~ /^452/) { # too many recipients - see rfc2821 |
---|
3897 | do_log(-1, sprintf('Only %d recips sent in one go: "%s"', |
---|
3898 | $any_valid_recips, $rcpt_smtp_resp)); |
---|
3899 | $skipping_resp = $rcpt_smtp_resp; |
---|
3900 | } elsif ($rcpt_smtp_resp =~ /^4/) { |
---|
3901 | $any_tempfail_recips++; |
---|
3902 | $smtp_response = $rcpt_smtp_resp if !defined($smtp_response); |
---|
3903 | } |
---|
3904 | $r->recip_smtp_response($rcpt_smtp_resp); $r->recip_done(2); |
---|
3905 | $smtp_response = $rcpt_smtp_resp |
---|
3906 | if $rcpt_smtp_resp =~ /^5/ && $smtp_response !~ /^5/; # keep first 5x |
---|
3907 | } |
---|
3908 | } |
---|
3909 | section_time($which_section); prolong_timer($which_section); |
---|
3910 | $smtp_code = $smtp_msg = undef; |
---|
3911 | |
---|
3912 | # TODO: get the value of $dsn_per_recip_capable argument of check_mail(), |
---|
3913 | # or the actual protocol from In::Connection |
---|
3914 | my($dsn_per_recip_capable) = 0; |
---|
3915 | |
---|
3916 | if (!$any_valid_recips) { |
---|
3917 | do_log(-1,"mail_via_smtp: DATA skipped, no valid recips, $any_tempfail_recips"); |
---|
3918 | } elsif ($any_tempfail_recips && !$dsn_per_recip_capable) { |
---|
3919 | # we must not proceede if mail did not came in as LMTP, |
---|
3920 | # or we would generate mail duplicates on each delivery attempt |
---|
3921 | do_log(-1,"mail_via_smtp: DATA skipped, tempfailed recips: $any_tempfail_recips"); |
---|
3922 | } else { # send the message contents (enter DATA phase) |
---|
3923 | $which_section = 'fwd-data'; |
---|
3924 | $smtp_handle->data or die "sending DATA command\n"; |
---|
3925 | $in_datasend_mode = 1; |
---|
3926 | |
---|
3927 | my($smtp_resp) = $smtp_handle->code . " " . $smtp_handle->message; |
---|
3928 | chomp($smtp_resp); |
---|
3929 | do_log(5, "response to DATA: \"$smtp_resp\""); |
---|
3930 | |
---|
3931 | my($smtp_data_fh) = Amavis::Out->new_smtp_data($smtp_handle); |
---|
3932 | my($hdr_edits) = $msginfo->header_edits; |
---|
3933 | $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits; |
---|
3934 | $received_cnt = $hdr_edits->write_header($msg, $smtp_data_fh); |
---|
3935 | if ($received_cnt > 100) { |
---|
3936 | # loop detection required by rfc2821 6.2 |
---|
3937 | # Do not modify the signal text, it gets matched elsewhere! |
---|
3938 | die "Too many hops: $received_cnt 'Received:' header lines\n"; |
---|
3939 | } |
---|
3940 | if (!defined($msg)) { |
---|
3941 | # empty mail body |
---|
3942 | } elsif ($msg->isa('MIME::Entity')) { |
---|
3943 | $msg->print_body($smtp_data_fh); |
---|
3944 | } else { |
---|
3945 | # Using fixed-size reads instead of line-by-line approach |
---|
3946 | # makes feeding mail back to MTA (e.g. Postfix) more than |
---|
3947 | # twice as fast for larger mail. |
---|
3948 | |
---|
3949 | # to reduce the likelyhood of a qmail bare-LF bug (bare LF reported |
---|
3950 | # when CR and LF are separated by a TCP packet boundary) one may use |
---|
3951 | # this 'while' line, reading line by line, instead of the normal one: |
---|
3952 | ### while (defined($_=$msg->getline)) { |
---|
3953 | |
---|
3954 | while ($msg->read($_, 16384) > 0) { |
---|
3955 | $smtp_handle->datasend($_) |
---|
3956 | or die "datasend timed out while sending body\n"; |
---|
3957 | } |
---|
3958 | |
---|
3959 | } |
---|
3960 | section_time($which_section); prolong_timer($which_section); |
---|
3961 | |
---|
3962 | $which_section = 'fwd-data-end'; |
---|
3963 | # don't check status of dataend here, it may not yet be available |
---|
3964 | $smtp_handle->dataend; |
---|
3965 | $in_datasend_mode = 0; $any_valid_recips_and_data_sent = 1; |
---|
3966 | section_time($which_section); prolong_timer($which_section); |
---|
3967 | |
---|
3968 | $which_section = 'fwd-rundown-1'; |
---|
3969 | # figure out the final SMTP response |
---|
3970 | $smtp_code = $smtp_handle->code; |
---|
3971 | my(@msgs) = $smtp_handle->message; |
---|
3972 | # only the 'command()' resets messages list, so now we have both: |
---|
3973 | # 'End data with <CR><LF>.<CR><LF>' and 'Ok: queued as...' in @msgs |
---|
3974 | # and only the last SMTP response code in $smtp_handle->code |
---|
3975 | my($smtp_msg) = $msgs[$#msgs]; chomp($smtp_msg); # take the last one |
---|
3976 | $smtp_response = "$smtp_code $smtp_msg"; |
---|
3977 | do_log(5, "response to data end: \"$smtp_response\""); |
---|
3978 | for my $r (@per_recip_data) { |
---|
3979 | next if $r->recip_done; # skip those that failed at RCPT TO |
---|
3980 | $r->recip_remote_mta($relayhost); |
---|
3981 | $r->recip_remote_mta_smtp_response($smtp_response); |
---|
3982 | } |
---|
3983 | if ($smtp_code =~ /^[245]/) { |
---|
3984 | my($smtp_status) = substr($smtp_code, 0, 1); |
---|
3985 | $smtp_response = sprintf("%s %d.6.0 %s, id=%s, from MTA: %s", |
---|
3986 | $smtp_code, $smtp_status, ($smtp_status == 2 ? 'Ok' : 'Failed'), |
---|
3987 | am_id(), $smtp_response); |
---|
3988 | } |
---|
3989 | } |
---|
3990 | }; |
---|
3991 | my($err) = $@; |
---|
3992 | my($saved_section_name) = $which_section; |
---|
3993 | if ($err ne '') { chomp($err); $err = ' ' if $err eq '' } # careful chomp |
---|
3994 | prolong_timer($which_section, $remaining_time); # restart the timer |
---|
3995 | $which_section = 'fwd-rundown'; |
---|
3996 | if ($err ne '') { # fetch info about failure |
---|
3997 | do_log(3, "mail_via_smtp: session failed: $err"); |
---|
3998 | if (!defined($smtp_handle)) { $smtp_code = ''; $smtp_msg = '' } |
---|
3999 | else { |
---|
4000 | $smtp_code = $smtp_handle->code; $smtp_msg = $smtp_handle->message; |
---|
4001 | chomp($smtp_msg); |
---|
4002 | } |
---|
4003 | } |
---|
4004 | # terminate the SMTP session if still alive |
---|
4005 | if (!defined $smtp_handle) { |
---|
4006 | # nothing |
---|
4007 | } elsif ($in_datasend_mode) { |
---|
4008 | # We are aborting SMTP session. DATA send mode must NOT be normally |
---|
4009 | # terminated with a dataend (dot), otherwise recipient will receive |
---|
4010 | # a chopped-off mail (and possibly be receiving it over and over again |
---|
4011 | # during each MTA retry. |
---|
4012 | do_log(-1, "mail_via_smtp: NOTICE: aborting SMTP session, $err"); |
---|
4013 | $smtp_handle->close; # abruptly terminate the SMTP session, ignoring status |
---|
4014 | } else { |
---|
4015 | $smtp_handle->timeout(15); # don't wait too long for response to a QUIT |
---|
4016 | $smtp_handle->quit; # send a QUIT regardless of success so far |
---|
4017 | if ($err eq '' && $smtp_handle->status != CMD_OK) { |
---|
4018 | do_log(-1,"WARN: sending SMTP QUIT command failed: " |
---|
4019 | . $smtp_handle->code . " " . $smtp_handle->message); |
---|
4020 | } |
---|
4021 | } |
---|
4022 | # prepare final smtp response and log abnormal events |
---|
4023 | if ($err eq '') { # no errors |
---|
4024 | if ($any_valid_recips_and_data_sent && $smtp_response !~ /^[245]/) { |
---|
4025 | $smtp_response = |
---|
4026 | sprintf("451 4.6.0 Bad SMTP code, id=%s, from MTA: \"%s\"", |
---|
4027 | am_id(), $smtp_response); |
---|
4028 | } |
---|
4029 | } elsif ($err eq "timed out" || $err =~ /: Timeout\z/) { |
---|
4030 | my($msg) = ($in_datasend_mode && $smtp_code =~ /^354/) ? |
---|
4031 | '' : ", $smtp_code $smtp_msg"; |
---|
4032 | $smtp_response = sprintf("450 4.4.2 Timed out during %s%s, id=%s", |
---|
4033 | $saved_section_name, $msg, am_id()); |
---|
4034 | } elsif ($err =~ /^Can't connect/) { |
---|
4035 | $smtp_response = sprintf("450 4.4.1 %s, id=%s", $err, am_id()); |
---|
4036 | } elsif ($err =~ /^Too many hops/) { |
---|
4037 | $smtp_response = sprintf("550 5.4.6 Rejected: %s, id=%s", $err, am_id()); |
---|
4038 | } elsif ($smtp_code =~ /^5/) { # 5xx |
---|
4039 | $smtp_response = sprintf("%s 5.5.0 Rejected by MTA: %s %s, id=%s", |
---|
4040 | ($smtp_code !~ /^5\d\d\z/ ? "550" : $smtp_code), |
---|
4041 | $smtp_code, $smtp_msg, am_id()); |
---|
4042 | } elsif ($smtp_code =~ /^0/) { # 000 |
---|
4043 | $smtp_response = sprintf("450 4.4.2 No response during %s (%s): id=%s", |
---|
4044 | $saved_section_name, $err, am_id()); |
---|
4045 | } else { |
---|
4046 | $smtp_response = sprintf("%s 4.5.0 from MTA during %s (%s): %s %s, id=%s", |
---|
4047 | ($smtp_code !~ /^4\d\d\z/ ? "451" : $smtp_code), |
---|
4048 | $saved_section_name, $err, $smtp_code, $smtp_msg, |
---|
4049 | am_id()); |
---|
4050 | } |
---|
4051 | |
---|
4052 | do_log( ($smtp_response =~ /^2/ ? 3 : -1), |
---|
4053 | "mail_via_smtp: $smtp_response" ) if $smtp_response ne ''; |
---|
4054 | if (defined $smtp_response) { |
---|
4055 | for my $r (@per_recip_data) { |
---|
4056 | if (!$r->recip_done) { # mark it as done |
---|
4057 | $r->recip_smtp_response($smtp_response); $r->recip_done(2); |
---|
4058 | $r->recip_mbxname($r->recip_final_addr) if $smtp_response =~ /^2/; |
---|
4059 | } elsif ($any_valid_recips_and_data_sent |
---|
4060 | && $r->recip_smtp_response =~ /^452/) { |
---|
4061 | # 'undo' the RCPT TO '452 Too many recipients' situation, |
---|
4062 | # needs to be handled in more than one transaction |
---|
4063 | $r->recip_smtp_response(undef); $r->recip_done(undef); |
---|
4064 | } |
---|
4065 | } |
---|
4066 | } |
---|
4067 | if ( $smtp_response =~ /^2/) { snmp_count('OutMsgsDelivers') } |
---|
4068 | elsif ($smtp_response =~ /^4/) { snmp_count('OutAttemptFails') } |
---|
4069 | elsif ($smtp_response =~ /^5/) { snmp_count('OutMsgsRejects') } |
---|
4070 | section_time($which_section); |
---|
4071 | 1; |
---|
4072 | } |
---|
4073 | |
---|
4074 | # Send mail using external mail submission program 'sendmail' (also available |
---|
4075 | # with Postfix and Exim) - used for forwarding original mail or sending notif. |
---|
4076 | # May throw exception (die) if temporary failure (4xx) or other problem |
---|
4077 | # |
---|
4078 | sub mail_via_pipe(@) { |
---|
4079 | my($via, $msginfo, $initial_submission, $filter) = @_; |
---|
4080 | snmp_count('OutMsgs'); |
---|
4081 | $via =~ /^pipe:(.*)\z/si or die "Bad fwd method syntax: $via"; |
---|
4082 | my($pipe_args) = $1; |
---|
4083 | $pipe_args =~ s/^flags=\S*\s*//i; # flags are currently ignored, q implied |
---|
4084 | $pipe_args =~ s/^argv=//i; |
---|
4085 | my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
4086 | @{$msginfo->per_recip_data}; |
---|
4087 | my($logmsg) = sprintf("%s via PIPE: <%s>", |
---|
4088 | ($initial_submission ? 'SEND' : 'FWD'), $msginfo->sender); |
---|
4089 | if (!@per_recip_data) { |
---|
4090 | do_log(5, "$logmsg, nothing to do"); |
---|
4091 | return 1; |
---|
4092 | } |
---|
4093 | do_log(1, $logmsg . " -> " . |
---|
4094 | qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data)); |
---|
4095 | my($msg) = $msginfo->mail_text; # a file handle or a MIME::Entity object |
---|
4096 | if (defined($msg) && !$msg->isa('MIME::Entity')) { |
---|
4097 | # at this point, we have no idea what the user gave us... |
---|
4098 | # a globref? a FileHandle? |
---|
4099 | $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj |
---|
4100 | $msg->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
4101 | } |
---|
4102 | my(@pipe_args) = split(' ', $pipe_args); my(@command) = shift @pipe_args; |
---|
4103 | for (@pipe_args) { |
---|
4104 | # The sendmail command line expects addresses quoted as per RFC 822. |
---|
4105 | # "funny user"@some.domain |
---|
4106 | # For compatibility with Sendmail, the Postfix sendmail command line |
---|
4107 | # also accepts address formats that are legal in RFC 822 mail headers: |
---|
4108 | # Funny Dude <"funny user"@some.domain> |
---|
4109 | # Although addresses passed as args to sendmail initial submission |
---|
4110 | # should not be <...> bracketed, for some reason original sendmail |
---|
4111 | # issues a warning on null reverse-path, but gladly accepty <>. |
---|
4112 | # As this is not strictly wrong, we comply to make it happy. |
---|
4113 | if (/^\$\{sender\}\z/i) { |
---|
4114 | push(@command, |
---|
4115 | map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) } |
---|
4116 | $msginfo->sender); |
---|
4117 | } elsif (/^\$\{recipient\}\z/i) { |
---|
4118 | push(@command, |
---|
4119 | map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) } |
---|
4120 | map { $_->recip_final_addr } @per_recip_data); |
---|
4121 | } else { |
---|
4122 | push(@command, $_); |
---|
4123 | } |
---|
4124 | } |
---|
4125 | do_log(5, "mail_via_pipe running command: " . join(' ', @command)); |
---|
4126 | local $SIG{CHLD} = 'DEFAULT'; |
---|
4127 | local $SIG{PIPE} = 'IGNORE'; # write to broken pipe would throw a signal |
---|
4128 | my($mp,$pid) = run_command_consumer(undef,undef,@command); |
---|
4129 | binmode($mp) or die "Can't set pipe to binmode: $!"; # dflt since Perl 5.8.1 |
---|
4130 | my($hdr_edits) = $msginfo->header_edits; |
---|
4131 | $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits; |
---|
4132 | my($received_cnt) = $hdr_edits->write_header($msg, $mp); |
---|
4133 | if ($received_cnt > 100) { # loop detection required by rfc2821 6.2 |
---|
4134 | # deal with it later, for now just skip the body |
---|
4135 | } elsif (!defined($msg)) { |
---|
4136 | # empty mail body |
---|
4137 | } elsif ($msg->isa('MIME::Entity')) { |
---|
4138 | $msg->print_body($mp); |
---|
4139 | } else { |
---|
4140 | while ($msg->read($_, 16384) > 0) |
---|
4141 | { $mp->print($_) or die "Submitting mail text failed: $!" } |
---|
4142 | } |
---|
4143 | my($smtp_response); |
---|
4144 | if ($received_cnt > 100) { # loop detection required by rfc2821 6.2 |
---|
4145 | do_log(-2, "Too many hops: $received_cnt 'Received:' header lines"); |
---|
4146 | kill('TERM',$pid); # kill the process running mail submission program |
---|
4147 | $mp->close; # and ignore status |
---|
4148 | $smtp_response = "550 5.4.6 Rejected: " . |
---|
4149 | "Too many hops: $received_cnt 'Received:' header lines"; |
---|
4150 | } else { |
---|
4151 | my($err); $mp->close or $err=$!; my($child_stat) = $?; |
---|
4152 | my($error_str) = exit_status_str($child_stat,$err); |
---|
4153 | my($status) = WEXITSTATUS($child_stat); |
---|
4154 | # sendmail program (Postfix variant) can return the following exit codes: |
---|
4155 | # EX_OK(0), EX_DATAERR, EX_SOFTWARE, EX_TEMPFAIL, EX_NOUSER, EX_UNAVAILABLE |
---|
4156 | if ($status == EX_OK) { |
---|
4157 | $smtp_response = "250 2.6.0 Ok"; # submitted to MTA |
---|
4158 | snmp_count('OutMsgsDelivers'); |
---|
4159 | } elsif ($status == EX_TEMPFAIL) { |
---|
4160 | $smtp_response = "450 4.5.0 Temporary failure submitting message"; |
---|
4161 | snmp_count('OutAttemptFails'); |
---|
4162 | } elsif ($status == EX_NOUSER) { |
---|
4163 | $smtp_response = "550 5.1.1 Recipient unknown"; |
---|
4164 | snmp_count('OutMsgsRejects'); |
---|
4165 | } elsif ($status == EX_UNAVAILABLE) { |
---|
4166 | $smtp_response = "550 5.5.0 Mail submission service unavailable"; |
---|
4167 | snmp_count('OutMsgsRejects'); |
---|
4168 | } else { |
---|
4169 | $smtp_response = "451 4.5.0 Failed to submit a message: $error_str"; |
---|
4170 | snmp_count('OutAttemptFails'); |
---|
4171 | } |
---|
4172 | } |
---|
4173 | $smtp_response .= ", id=" . am_id(); |
---|
4174 | for my $r (@per_recip_data) { |
---|
4175 | next if $r->recip_done; |
---|
4176 | $r->recip_smtp_response($smtp_response); $r->recip_done(2); |
---|
4177 | $r->recip_mbxname($r->recip_final_addr) if $smtp_response =~ /^2/; |
---|
4178 | } |
---|
4179 | section_time('fwd-pipe'); |
---|
4180 | 1; |
---|
4181 | } |
---|
4182 | |
---|
4183 | sub mail_via_bsmtp(@) { |
---|
4184 | my($via, $msginfo, $initial_submission, $filter) = @_; |
---|
4185 | snmp_count('OutMsgs'); local($1); |
---|
4186 | $via =~ /^bsmtp:(.*)\z/si or die "Bad fwd method: $via"; |
---|
4187 | my($bsmtp_file_final) = $1; my($mbxname); |
---|
4188 | $bsmtp_file_final =~ s{%(.)} |
---|
4189 | { $1 eq 'b' ? $msginfo->body_digest |
---|
4190 | : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time)) |
---|
4191 | : $1 eq 'n' ? am_id() |
---|
4192 | : $1 eq '%' ? '%' : '%'.$1 }egs; |
---|
4193 | # prepend directory if not specified |
---|
4194 | $bsmtp_file_final = $QUARANTINEDIR."/".$bsmtp_file_final |
---|
4195 | if $QUARANTINEDIR ne '' && $bsmtp_file_final !~ m{^/}; |
---|
4196 | my($bsmtp_file_tmp) = $bsmtp_file_final . ".tmp"; |
---|
4197 | my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
4198 | @{$msginfo->per_recip_data}; |
---|
4199 | my($logmsg) = sprintf("%s via BSMTP: %s", |
---|
4200 | ($initial_submission ? 'SEND' : 'FWD'), |
---|
4201 | qquote_rfc2821_local($msginfo->sender)); |
---|
4202 | if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 } |
---|
4203 | do_log(1, $logmsg . " -> " . |
---|
4204 | qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data) . |
---|
4205 | ", file " . $bsmtp_file_final); |
---|
4206 | my($msg) = $msginfo->mail_text; # a scalar reference, or a file handle |
---|
4207 | if (defined($msg) && !$msg->isa('MIME::Entity')) { |
---|
4208 | # at this point, we have no idea what the user gave us... |
---|
4209 | # a globref? a FileHandle? |
---|
4210 | $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj |
---|
4211 | $msg->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
4212 | } |
---|
4213 | my($mp) = IO::File->new; |
---|
4214 | eval { |
---|
4215 | $mp->open($bsmtp_file_tmp,'>',0640) |
---|
4216 | or die "Can't create BSMTP file $bsmtp_file_tmp: $!"; |
---|
4217 | binmode($mp, ":bytes") or die "Can't set :bytes, $!" if $unicode_aware; |
---|
4218 | $mp->print("EHLO ", c('localhost_name'), $eol) |
---|
4219 | or die "print failed (EHLO): $!"; |
---|
4220 | $mp->printf("MAIL FROM:%s BODY=8BITMIME%s", # avoid conversion to 7bit |
---|
4221 | qquote_rfc2821_local($msginfo->sender), $eol) |
---|
4222 | or die "print failed (MAIL FROM): $!"; |
---|
4223 | for my $r (@per_recip_data) { |
---|
4224 | $mp->print("RCPT TO:", qquote_rfc2821_local($r->recip_final_addr), $eol) |
---|
4225 | or die "print failed (RCPT TO): $!"; |
---|
4226 | } |
---|
4227 | $mp->print("DATA", $eol) or die "print failed (DATA): $!"; |
---|
4228 | my($hdr_edits) = $msginfo->header_edits; |
---|
4229 | $hdr_edits = Amavis::Out::EditHeader->new if !$hdr_edits; |
---|
4230 | my($received_cnt) = $hdr_edits->write_header($msg,$mp); |
---|
4231 | if ($received_cnt > 100) { # loop detection required by rfc2821 6.2 |
---|
4232 | die "Too many hops: $received_cnt 'Received:' header lines"; |
---|
4233 | } elsif (!defined($msg)) { # empty mail body |
---|
4234 | } elsif ($msg->isa('MIME::Entity')) { |
---|
4235 | $msg->print_body($mp); |
---|
4236 | } else { |
---|
4237 | while (<$msg>) { |
---|
4238 | $mp->print(/^\./ ? (".",$_) : $_) or die "print failed-data: $!"; |
---|
4239 | } |
---|
4240 | } |
---|
4241 | $mp->print(".", $eol) or die "print failed (final dot): $!"; |
---|
4242 | # $mp->print("QUIT",$eol) or die "print failed (QUIT): $!"; |
---|
4243 | $mp->close or die "Can't close BSMTP file $bsmtp_file_tmp: $!"; |
---|
4244 | $mp = undef; |
---|
4245 | rename($bsmtp_file_tmp, $bsmtp_file_final) |
---|
4246 | or die "Can't rename BSMTP file to $bsmtp_file_final: $!"; |
---|
4247 | $mbxname = $bsmtp_file_final; |
---|
4248 | }; |
---|
4249 | my($err) = $@; my($smtp_response); |
---|
4250 | if ($err eq '') { |
---|
4251 | $smtp_response = "250 2.6.0 Ok, queued as BSMTP $bsmtp_file_final"; |
---|
4252 | snmp_count('OutMsgsDelivers'); |
---|
4253 | } else { |
---|
4254 | chomp($err); |
---|
4255 | unlink($bsmtp_file_tmp) |
---|
4256 | or do_log(-2,"Can't delete half-finished BSMTP file $bsmtp_file_tmp: $!"); |
---|
4257 | $mp->close if defined $mp; # ignore status |
---|
4258 | if ($err =~ /too many hops/i) { |
---|
4259 | $smtp_response = "550 5.4.6 Rejected: $err"; |
---|
4260 | snmp_count('OutMsgsRejects'); |
---|
4261 | } else { |
---|
4262 | $smtp_response = "451 4.5.0 Writing $bsmtp_file_tmp failed: $err"; |
---|
4263 | snmp_count('OutAttemptFails'); |
---|
4264 | } |
---|
4265 | } |
---|
4266 | $smtp_response .= ", id=" . am_id(); |
---|
4267 | for my $r (@per_recip_data) { |
---|
4268 | next if $r->recip_done; |
---|
4269 | $r->recip_smtp_response($smtp_response); $r->recip_done(2); |
---|
4270 | $r->recip_mbxname($mbxname) if $mbxname ne '' && $smtp_response =~ /^2/; |
---|
4271 | } |
---|
4272 | section_time('fwd-bsmtp'); |
---|
4273 | 1; |
---|
4274 | } |
---|
4275 | |
---|
4276 | 1; |
---|
4277 | |
---|
4278 | # |
---|
4279 | package Amavis::UnmangleSender; |
---|
4280 | use strict; |
---|
4281 | use re 'taint'; |
---|
4282 | |
---|
4283 | BEGIN { |
---|
4284 | use Exporter (); |
---|
4285 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
4286 | $VERSION = '2.034'; |
---|
4287 | @ISA = qw(Exporter); |
---|
4288 | %EXPORT_TAGS = (); |
---|
4289 | @EXPORT = (); |
---|
4290 | @EXPORT_OK = qw(&best_try_originator_ip &best_try_originator |
---|
4291 | &first_received_from); |
---|
4292 | } |
---|
4293 | use subs @EXPORT_OK; |
---|
4294 | |
---|
4295 | BEGIN { |
---|
4296 | import Amavis::Conf qw(:platform @viruses_that_fake_sender_maps); |
---|
4297 | import Amavis::Util qw(ll do_log); |
---|
4298 | import Amavis::rfc2821_2822_Tools qw( |
---|
4299 | split_address parse_received fish_out_ip_from_received); |
---|
4300 | import Amavis::Lookup qw(lookup lookup_ip_acl); |
---|
4301 | } |
---|
4302 | use Mail::Address; |
---|
4303 | |
---|
4304 | # Returns the envelope sender address, or reconstructs it if there is |
---|
4305 | # a good reason to believe the envelope address has been changed or forged, |
---|
4306 | # as is common for some varieties of viruses. Returns best guess of the |
---|
4307 | # sender address, or undef if it can not be determined. |
---|
4308 | # |
---|
4309 | sub unmangle_sender($$$) { |
---|
4310 | my $sender = shift; # rfc2821 envelope sender address |
---|
4311 | my $from = shift; # rfc2822 'From:' header, may include comment |
---|
4312 | my $virusname_list = shift; # list ref containing names of detected viruses |
---|
4313 | # based on ideas from Furio Ercolessi, Mike Atkinson, Mark Martinec |
---|
4314 | |
---|
4315 | my($best_try_originator) = $sender; |
---|
4316 | my($localpart,$domain) = split_address($sender); |
---|
4317 | # extract the RFC2822 'from' address, ignoring phrase and comment |
---|
4318 | chomp($from); |
---|
4319 | { |
---|
4320 | local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted ! |
---|
4321 | $from = (Mail::Address->parse($from))[0]; |
---|
4322 | } |
---|
4323 | $from = $from->address if $from ne ''; |
---|
4324 | # NOTE: rfc2822 allows multiple addresses in the From field! |
---|
4325 | |
---|
4326 | if (grep { /magistr/i } @$virusname_list) { |
---|
4327 | for my $j (0..2) { # assemble possible `shifted' candidates |
---|
4328 | next if $j >= length($localpart); |
---|
4329 | my($try) = $sender; |
---|
4330 | substr($try, $j, 1) = chr(ord(substr($try, $j, 1)) - 1); |
---|
4331 | if (lc($from) eq lc($try)) { $best_try_originator = $try; last } |
---|
4332 | } |
---|
4333 | } |
---|
4334 | # |
---|
4335 | # Virus names are AV-checker vendor specific, but many use same |
---|
4336 | # or similar virus names. This requires attention and adjustments |
---|
4337 | # from Amavis administrators. |
---|
4338 | # |
---|
4339 | if (grep { /badtrans/i } @$virusname_list) { |
---|
4340 | if ($from =~ /^ # these are fake built-in addresses |
---|
4341 | (joanna\@mail\.utexas\.edu | powerpuff\@videotron\.ca | |
---|
4342 | (mary\@c-com | support\@cyberramp | admin\@gte | |
---|
4343 | administrator\@border) \.net | |
---|
4344 | (monika\@telia | jessica\@aol | spiderroll\@hotmail | |
---|
4345 | lgonzal\@hotmail | andy\@hweb-media | Gravity49\@aol | |
---|
4346 | tina0828\@yahoo | JUJUB271\@AOL | aizzo\@home) \.com |
---|
4347 | ) \z/xi ) |
---|
4348 | { # discard recipient's address used as a fake 'MAIL FROM:' |
---|
4349 | $best_try_originator = undef; |
---|
4350 | } else { |
---|
4351 | $best_try_originator = $1 if $from=~/^_(.+)\z/s && lc($sender) ne lc($1); |
---|
4352 | } |
---|
4353 | } |
---|
4354 | for my $vn (@$virusname_list) { |
---|
4355 | my($result,$matching_key) = lookup(0,$vn,@viruses_that_fake_sender_maps); |
---|
4356 | if ($result) { |
---|
4357 | do_log(2, "Virus $vn matches $matching_key, sender addr ignored"); |
---|
4358 | $best_try_originator = undef; |
---|
4359 | last; |
---|
4360 | } |
---|
4361 | } |
---|
4362 | $best_try_originator; |
---|
4363 | } |
---|
4364 | |
---|
4365 | # Given a dotted-quad IPv4 address try reverse DNS resolve, and then |
---|
4366 | # forward DNS resolve. If they match, return domain name, |
---|
4367 | # otherwise return the IP address in brackets. (resolves IPv4 only) |
---|
4368 | # |
---|
4369 | sub ip_addr_to_name($) { |
---|
4370 | my($addr) = @_; # dotted-quad address string |
---|
4371 | local($1,$2,$3,$4); my($result); |
---|
4372 | if ($addr !~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) { |
---|
4373 | $result = $addr; # not an IPv4 address |
---|
4374 | } else { |
---|
4375 | my($binaddr) = pack('C4', $1,$2,$3,$4); # to binary string |
---|
4376 | do_log(5, "ip_addr_to_name: DNS reverse-resolving: $addr"); |
---|
4377 | my(@addr) = gethostbyaddr($binaddr,2); # IP -> name |
---|
4378 | $result = '['.$addr.']'; # IP address in brackets if nothing matches |
---|
4379 | if (@addr) { |
---|
4380 | my($name,$aliases,$addrtype,$length,@addrs) = @addr; |
---|
4381 | if ($name =~ /[^.]\.[a-zA-Z]+\z/s) { |
---|
4382 | do_log(5, "ip_addr_to_name: DNS forward-resolving: $name"); |
---|
4383 | my(@raddr) = gethostbyname($name); # name -> IP |
---|
4384 | my($rname,$raliases,$raddrtype,$rlength,@raddrs) = @raddr; |
---|
4385 | for my $ra (@raddrs) { |
---|
4386 | if (lc($ra) eq lc($binaddr)) { $result = $name; last } |
---|
4387 | } |
---|
4388 | } |
---|
4389 | } |
---|
4390 | } |
---|
4391 | do_log(3, "ip_addr_to_name: returning: $result"); |
---|
4392 | $result; |
---|
4393 | } |
---|
4394 | |
---|
4395 | # Obtain and parse the first entry (chronologically) in the 'Received:' header |
---|
4396 | # path trace - to be used as the value of the macro %t in customized messages |
---|
4397 | # |
---|
4398 | sub first_received_from($) { |
---|
4399 | my($entity) = shift; |
---|
4400 | my($first_received); |
---|
4401 | if (defined($entity)) { |
---|
4402 | my($fields) = parse_received($entity->head->get('received', -1)); |
---|
4403 | if (exists $fields->{'from'}) { |
---|
4404 | my($item, $v1, $v2, $v3, $comment) = @{$fields->{'from'}}; |
---|
4405 | $first_received = join(' ', $item, $comment); |
---|
4406 | $first_received =~ s/^[ \t\n\r]+//s; # discard leading whitespace |
---|
4407 | $first_received =~ s/[ \t\n\r]+\z//s; # discard trailing whitespace |
---|
4408 | } |
---|
4409 | do_log(5, "first_received_from: $first_received"); |
---|
4410 | } |
---|
4411 | $first_received; |
---|
4412 | } |
---|
4413 | |
---|
4414 | # Try to extract sender's public IP address from the Received trace |
---|
4415 | # |
---|
4416 | sub best_try_originator_ip($) { |
---|
4417 | my($entity) = @_; |
---|
4418 | my($first_received_from_ip); |
---|
4419 | if (defined($entity)) { |
---|
4420 | my(@publicnetworks) = qw( |
---|
4421 | !0.0.0.0/8 !127.0.0.0/8 !172.16.0.0/12 !192.168.0.0/16 !10.0.0.0/8 |
---|
4422 | !169.254.0.0/16 !192.0.2.0/24 !192.88.99.0/24 !224.0.0.0/4 |
---|
4423 | ::FFFF:0:0/96 |
---|
4424 | !:: !::1 !FF00::/8 !FE80::/10 !FEC0::/10 |
---|
4425 | ::/0 ); # rfc3330, rfc3513 |
---|
4426 | my(@received) = reverse $entity->head->get('received'); |
---|
4427 | $#received = 5 if $#received > 5; # first six, chronologically |
---|
4428 | for my $r (@received) { |
---|
4429 | $first_received_from_ip = fish_out_ip_from_received($r); |
---|
4430 | last if $first_received_from_ip ne '' && |
---|
4431 | eval { lookup_ip_acl($first_received_from_ip,\@publicnetworks) }; |
---|
4432 | } |
---|
4433 | do_log(5, "best_try_originator_ip: $first_received_from_ip"); |
---|
4434 | } |
---|
4435 | $first_received_from_ip; |
---|
4436 | } |
---|
4437 | |
---|
4438 | # For the purpose of informing administrators try to obtain true sender |
---|
4439 | # address or at least its site, as most viruses and spam have a nasty habit |
---|
4440 | # of faking envelope sender address. Return a pair of addresses: |
---|
4441 | # - the first (if defined) appears valid and may be used for sender |
---|
4442 | # notifications; |
---|
4443 | # - the second should only be used in generating customizable notification |
---|
4444 | # messages (macro %o), NOT to be used as address for sending notifications, |
---|
4445 | # as it can contain invalid address (but can be more informative). |
---|
4446 | # |
---|
4447 | sub best_try_originator($$$) { |
---|
4448 | my($sender, $entity, $virusname_list) = @_; |
---|
4449 | return ($sender,$sender) if !defined($entity); # don't bother if no header |
---|
4450 | my($originator) = |
---|
4451 | unmangle_sender($sender, $entity->head->get('from',0), $virusname_list); |
---|
4452 | return ($originator, $originator) if defined $originator; |
---|
4453 | my($first_received_from_ip) = best_try_originator_ip($entity); |
---|
4454 | $originator = '?@' . ip_addr_to_name($first_received_from_ip) |
---|
4455 | if $first_received_from_ip ne ''; |
---|
4456 | (undef, $originator); |
---|
4457 | } |
---|
4458 | |
---|
4459 | 1; |
---|
4460 | |
---|
4461 | # |
---|
4462 | package Amavis::Unpackers::NewFilename; |
---|
4463 | use strict; |
---|
4464 | use re 'taint'; |
---|
4465 | |
---|
4466 | BEGIN { |
---|
4467 | use Exporter (); |
---|
4468 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
4469 | $VERSION = '2.034'; |
---|
4470 | @ISA = qw(Exporter); |
---|
4471 | @EXPORT_OK = qw(&consumed_bytes); |
---|
4472 | } |
---|
4473 | |
---|
4474 | BEGIN { |
---|
4475 | import Amavis::Conf qw(c cr ca |
---|
4476 | $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR |
---|
4477 | $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR); |
---|
4478 | import Amavis::Util qw(ll do_log min max); |
---|
4479 | } |
---|
4480 | |
---|
4481 | use vars qw($avail_quota); # available bytes quota for unpacked mail |
---|
4482 | use vars qw($rem_quota); # remaining bytes quota for unpacked mail |
---|
4483 | |
---|
4484 | sub new($;$$) { # create a file name generator object |
---|
4485 | my($class, $maxfiles,$mail_size) = @_; |
---|
4486 | # calculate and initialize quota |
---|
4487 | $avail_quota = $rem_quota = # quota in bytes |
---|
4488 | max($MIN_EXPANSION_QUOTA, $mail_size * $MIN_EXPANSION_FACTOR, |
---|
4489 | min($MAX_EXPANSION_QUOTA, $mail_size * $MAX_EXPANSION_FACTOR)); |
---|
4490 | do_log(4,"Original mail size: $mail_size; quota set to: $avail_quota bytes"); |
---|
4491 | # create object |
---|
4492 | bless { |
---|
4493 | num_of_issued_names => 0, first_issued_ind => 1, last_issued_ind => 0, |
---|
4494 | maxfiles => $maxfiles, # undef disables limit |
---|
4495 | objlist => [], |
---|
4496 | }, $class; |
---|
4497 | } |
---|
4498 | |
---|
4499 | sub parts_list_reset($) { # clear a list of recently issued names |
---|
4500 | my($self) = shift; |
---|
4501 | $self->{num_of_issued_names} = 0; |
---|
4502 | $self->{first_issued_ind} = $self->{last_issued_ind} + 1; |
---|
4503 | $self->{objlist} = []; |
---|
4504 | } |
---|
4505 | |
---|
4506 | sub parts_list($) { # returns a ref to a list of recently issued names |
---|
4507 | my($self) = shift; |
---|
4508 | $self->{objlist}; |
---|
4509 | } |
---|
4510 | |
---|
4511 | sub parts_list_add($$) { # add a parts object to the list of parts |
---|
4512 | my($self, $part) = @_; |
---|
4513 | push(@{$self->{objlist}}, $part); |
---|
4514 | } |
---|
4515 | |
---|
4516 | sub generate_new_num($) { # make-up a new number for a file and return it |
---|
4517 | my($self) = @_; |
---|
4518 | if (defined($self->{maxfiles}) && |
---|
4519 | $self->{num_of_issued_names} >= $self->{maxfiles}) { |
---|
4520 | # do not change the text in die without adjusting decompose_part() |
---|
4521 | die "Maximum number of files ($self->{maxfiles}) exceeded"; |
---|
4522 | } |
---|
4523 | $self->{num_of_issued_names}++; $self->{last_issued_ind}++; |
---|
4524 | $self->{last_issued_ind}; |
---|
4525 | } |
---|
4526 | |
---|
4527 | sub consumed_bytes($$;$$) { |
---|
4528 | my($bytes, $bywhom, $tentatively, $exquota) = @_; |
---|
4529 | my($perc) = !$avail_quota ? '' : sprintf(", (%.0f%%)", |
---|
4530 | 100 * ($avail_quota - ($rem_quota - $bytes)) / $avail_quota); |
---|
4531 | do_log(4,"Charging $bytes bytes to remaining quota $rem_quota" |
---|
4532 | . " (out of $avail_quota$perc) - by $bywhom"); |
---|
4533 | if ($bytes > $rem_quota && $rem_quota >= 0) { |
---|
4534 | # Do not modify the following signal text, it gets matched elsewhere! |
---|
4535 | my($msg) = "Exceeded storage quota $avail_quota bytes by $bywhom; ". |
---|
4536 | "last chunk $bytes bytes"; |
---|
4537 | do_log(-1, $msg); |
---|
4538 | die "$msg\n" if !$exquota; |
---|
4539 | } |
---|
4540 | $rem_quota -= $bytes unless $tentatively; |
---|
4541 | $rem_quota; # return remaining quota |
---|
4542 | } |
---|
4543 | |
---|
4544 | 1; |
---|
4545 | |
---|
4546 | # |
---|
4547 | package Amavis::Unpackers::Part; |
---|
4548 | use strict; |
---|
4549 | use re 'taint'; |
---|
4550 | |
---|
4551 | BEGIN { |
---|
4552 | use Exporter (); |
---|
4553 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
4554 | $VERSION = '2.034'; |
---|
4555 | @ISA = qw(Exporter); |
---|
4556 | } |
---|
4557 | |
---|
4558 | BEGIN { |
---|
4559 | import Amavis::Util qw(ll do_log); |
---|
4560 | } |
---|
4561 | |
---|
4562 | use vars qw($file_generator_object); |
---|
4563 | sub init($) { $file_generator_object = shift } |
---|
4564 | |
---|
4565 | sub new($;$$) { # create a part descriptor object |
---|
4566 | my($class, $dir_name,$parent) = @_; |
---|
4567 | my($self) = bless {}, $class; |
---|
4568 | if (!defined($dir_name) && !defined($parent)) { |
---|
4569 | # just make an empty object, presumably used as a new root |
---|
4570 | } else { |
---|
4571 | $self->number($file_generator_object->generate_new_num); |
---|
4572 | $self->dir_name($dir_name) if defined $dir_name; |
---|
4573 | if (defined $parent) { |
---|
4574 | $self->parent($parent); |
---|
4575 | my($ch_ref) = $parent->children; |
---|
4576 | push(@$ch_ref,$self); $parent->children($ch_ref); |
---|
4577 | } |
---|
4578 | $file_generator_object->parts_list_add($self); # save it |
---|
4579 | ll(4) && do_log(4, "Issued a new " . |
---|
4580 | (defined $dir_name ? "file name" : "pseudo part") . ": " . |
---|
4581 | $self->base_name); |
---|
4582 | } |
---|
4583 | $self; |
---|
4584 | } |
---|
4585 | |
---|
4586 | sub number |
---|
4587 | { my($self)=shift; !@_ ? $self->{number} : ($self->{number}=shift) }; |
---|
4588 | sub dir_name |
---|
4589 | { my($self)=shift; !@_ ? $self->{dir_name} : ($self->{dir_name}=shift) }; |
---|
4590 | sub parent |
---|
4591 | { my($self)=shift; !@_ ? $self->{parent} : ($self->{parent}=shift) }; |
---|
4592 | sub children |
---|
4593 | { my($self)=shift; !@_ ? $self->{children}||[] : ($self->{children}=shift) }; |
---|
4594 | sub mime_placement # part location within a MIME tree, e.g. "1/1/3" |
---|
4595 | { my($self)=shift; !@_ ? $self->{place} : ($self->{place}=shift) }; |
---|
4596 | sub type_short # string or a ref to a list of strings |
---|
4597 | { my($self)=shift; !@_ ? $self->{ty_short} : ($self->{ty_short}=shift) }; |
---|
4598 | sub type_long |
---|
4599 | { my($self)=shift; !@_ ? $self->{ty_long} : ($self->{ty_long}=shift) }; |
---|
4600 | sub type_declared |
---|
4601 | { my($self)=shift; !@_ ? $self->{ty_decl} : ($self->{ty_decl}=shift) }; |
---|
4602 | sub name_declared # string or a ref to a list of strings |
---|
4603 | { my($self)=shift; !@_ ? $self->{nm_decl} : ($self->{nm_decl}=shift) }; |
---|
4604 | sub size |
---|
4605 | { my($self)=shift; !@_ ? $self->{size} : ($self->{size}=shift) }; |
---|
4606 | sub exists |
---|
4607 | { my($self)=shift; !@_ ? $self->{exists} : ($self->{exists}=shift) }; |
---|
4608 | sub attributes # listref of characters representing attributes |
---|
4609 | { my($self)=shift; !@_ ? $self->{attr} : ($self->{attr}=shift) }; |
---|
4610 | sub attributes_add { # U=undecodable, C=crypted, D=directory,S=special,L=link |
---|
4611 | my($self)=shift; my($a) = $self->{attr} || []; |
---|
4612 | for my $arg (@_) { push(@$a,$arg) if $arg ne '' && !grep {$_ eq $arg} @$a } |
---|
4613 | $self->{attr} = $a; |
---|
4614 | }; |
---|
4615 | |
---|
4616 | sub base_name { my($self)=shift; sprintf("p%03d",$self->number) } |
---|
4617 | |
---|
4618 | sub full_name { |
---|
4619 | my($self)=shift; my($d) = $self->dir_name; |
---|
4620 | !defined($d) ? undef : $d.'/'.$self->base_name; |
---|
4621 | } |
---|
4622 | |
---|
4623 | # returns a ref to a list of part ancestors, starting with the root object, |
---|
4624 | # and including the part object itself |
---|
4625 | sub path { |
---|
4626 | my($self)=shift; |
---|
4627 | my(@path); |
---|
4628 | for (my($p)=$self; defined($p); $p=$p->parent) { unshift(@path,$p) } |
---|
4629 | \@path; |
---|
4630 | }; |
---|
4631 | |
---|
4632 | 1; |
---|
4633 | |
---|
4634 | # |
---|
4635 | package Amavis::Unpackers::OurFiler; |
---|
4636 | use strict; |
---|
4637 | use re 'taint'; |
---|
4638 | |
---|
4639 | BEGIN { |
---|
4640 | use Exporter (); |
---|
4641 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
4642 | $VERSION = '2.034'; |
---|
4643 | @ISA = qw(Exporter MIME::Parser::Filer); # subclass of MIME::Parser::Filer |
---|
4644 | %EXPORT_TAGS = (); |
---|
4645 | @EXPORT = (); |
---|
4646 | @EXPORT_OK = (); |
---|
4647 | } |
---|
4648 | # This package will be used by mime_decode(). |
---|
4649 | # |
---|
4650 | # We don't want no heavy MIME::Parser machinery for file name extension |
---|
4651 | # guessing, decoding charsets in filenames (and listening to complaints |
---|
4652 | # about it), checking for evil filenames, checking for filename contention, ... |
---|
4653 | # (which can not be turned off completely by ignore_filename(1) !!!) |
---|
4654 | # Just enforce our file name! And while at it, collect generated filenames. |
---|
4655 | # |
---|
4656 | sub new($$$) { |
---|
4657 | my($class, $dir, $parent_obj) = @_; |
---|
4658 | $dir =~ s{/+\z}{}; # chop off trailing slashes from directory name |
---|
4659 | bless {parent => $parent_obj, directory => $dir}, $class; |
---|
4660 | } |
---|
4661 | |
---|
4662 | # provide a generated file name |
---|
4663 | sub output_path($@) { |
---|
4664 | my($self, $head) = @_; |
---|
4665 | my($newpart_obj) = |
---|
4666 | Amavis::Unpackers::Part->new($self->{directory}, $self->{parent}); |
---|
4667 | get_amavisd_part($head, $newpart_obj); # store object into head |
---|
4668 | $newpart_obj->full_name; |
---|
4669 | } |
---|
4670 | |
---|
4671 | sub get_amavisd_part($;$) { |
---|
4672 | my($head) = shift; |
---|
4673 | !@_ ? $head->{amavisd_parts_obj} : ($head->{amavisd_parts_obj} = shift); |
---|
4674 | } |
---|
4675 | |
---|
4676 | 1; |
---|
4677 | |
---|
4678 | # |
---|
4679 | package Amavis::Unpackers::Validity; |
---|
4680 | use strict; |
---|
4681 | use re 'taint'; |
---|
4682 | |
---|
4683 | BEGIN { |
---|
4684 | use Exporter (); |
---|
4685 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
4686 | $VERSION = '2.034'; |
---|
4687 | @ISA = qw(Exporter); |
---|
4688 | %EXPORT_TAGS = (); |
---|
4689 | @EXPORT = (); |
---|
4690 | @EXPORT_OK = qw(&check_header_validity &check_for_banned_names); |
---|
4691 | } |
---|
4692 | |
---|
4693 | BEGIN { |
---|
4694 | import Amavis::Util qw(ll do_log sanitize_str); |
---|
4695 | import Amavis::Conf qw(:platform c cr ca); |
---|
4696 | import Amavis::Lookup qw(lookup); |
---|
4697 | } |
---|
4698 | |
---|
4699 | use subs @EXPORT_OK; |
---|
4700 | |
---|
4701 | sub check_header_validity($$) { |
---|
4702 | my($conn, $msginfo) = @_; |
---|
4703 | my(@bad); |
---|
4704 | my($curr_head); |
---|
4705 | for my $next_head (@{$msginfo->orig_header}, "\n") { |
---|
4706 | if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head } # folded |
---|
4707 | else { # new header |
---|
4708 | if (!defined($curr_head)) { # no previous complete header |
---|
4709 | } else { |
---|
4710 | # obsolete rfc822 syntax allowed whitespace before colon |
---|
4711 | my($field_name, $field_body) = |
---|
4712 | $curr_head =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s |
---|
4713 | ? ($1, $2) : (undef, $curr_head); |
---|
4714 | my($msg1,$msg2); |
---|
4715 | if (!defined($field_name) && $curr_head=~/^()()(.*)\z/s) { |
---|
4716 | $msg1 = "Invalid header field head"; |
---|
4717 | } elsif ($curr_head =~ /^(.*?)([\000\015])(.*)\z/s) { |
---|
4718 | $msg1 = "Improper use of control character"; |
---|
4719 | } elsif ($curr_head =~ /^(.*?)([\200-\377])(.*)\z/s) { |
---|
4720 | $msg1 = "Non-encoded 8-bit data"; |
---|
4721 | } elsif ($curr_head =~ /^(.*?)([^\000-\377])(.*)\z/s) { |
---|
4722 | $msg1 = "Non-encoded Unicode character"; |
---|
4723 | } elsif ($curr_head =~ /^()()([ \t]+)$/m) { |
---|
4724 | $msg1 ="Improper folded header field made up entirely of whitespace"; |
---|
4725 | } |
---|
4726 | if (defined $msg1) { |
---|
4727 | my($pre, $ch, $post) = ($1, $2, $3); |
---|
4728 | if (length($post) > 20) { $post = substr($post,0,15) . "..." } |
---|
4729 | if (length($pre)-length($field_name)-2 > 50-length($post)) { |
---|
4730 | $pre = "$field_name: ..." |
---|
4731 | . substr($pre, length($pre) - (45-length($post))); |
---|
4732 | } |
---|
4733 | $msg1 .= sprintf(" (char %02X hex)", ord($ch)) if length($ch)==1; |
---|
4734 | $msg1 .= " in message header '$field_name'" if $field_name ne ''; |
---|
4735 | $msg2 = sanitize_str($pre); my($msg2_pre_l) = length($msg2); |
---|
4736 | $msg2 .= sanitize_str($ch . $post); |
---|
4737 | # push(@bad, "$msg1\n $msg2\n " . (' ' x $msg2_pre_l) . '^'); |
---|
4738 | push(@bad, "$msg1: $msg2"); |
---|
4739 | } |
---|
4740 | } |
---|
4741 | last if $next_head eq $eol; # end-of-header reached |
---|
4742 | $curr_head = $next_head; |
---|
4743 | } |
---|
4744 | } |
---|
4745 | @bad; |
---|
4746 | } |
---|
4747 | |
---|
4748 | sub check_for_banned_names($) { |
---|
4749 | my($parts_root) = @_; |
---|
4750 | do_log(3, "Checking for banned types and filenames"); |
---|
4751 | my(@banned_part_descr,@banned_matching_keys,@banned_rhs); my($part); |
---|
4752 | my($bfnmr) = ca('banned_filename_maps'); # a ref to a list |
---|
4753 | for (my(@unvisited)=($parts_root); |
---|
4754 | @unvisited and $part=shift(@unvisited); |
---|
4755 | push(@unvisited,@{$part->children})) |
---|
4756 | { # traverse decomposed parts tree breadth-first |
---|
4757 | my(@path) = @{$part->path}; |
---|
4758 | next if @path <= 1; |
---|
4759 | # ll(5) && do_log(5, "part path: ".join(", ", map {$_->base_name} @path)); |
---|
4760 | shift(@path); # ignore place-holder root node |
---|
4761 | next if @{$part->children}; # ignore non-leaf nodes |
---|
4762 | my(@descr); my($found,$key_val,$key_what,$result,$matchingkey); |
---|
4763 | for my $p (@path) { |
---|
4764 | my(@k,$n); |
---|
4765 | $n = $p->base_name; |
---|
4766 | if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"P=$n") } |
---|
4767 | $n = $p->mime_placement; |
---|
4768 | if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"L=$n") } |
---|
4769 | $n = $p->type_declared; |
---|
4770 | $n = [$n] if !ref($n); |
---|
4771 | for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"M=$m")} } |
---|
4772 | $n = $p->type_short; |
---|
4773 | $n = [$n] if !ref($n); |
---|
4774 | for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"T=$m")} } |
---|
4775 | $n = $p->name_declared; |
---|
4776 | $n = [$n] if !ref($n); |
---|
4777 | for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"N=$m")} } |
---|
4778 | $n = $p->attributes; |
---|
4779 | $n = [$n] if !ref($n); |
---|
4780 | for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"A=$m")} } |
---|
4781 | push(@descr, join("\t",@k)); |
---|
4782 | if (!$found && @$bfnmr) { # still searching? (old style) |
---|
4783 | for my $k (@k) { |
---|
4784 | $k =~ /^([a-zA-Z0-9])=(.*)\z/s; |
---|
4785 | ($key_what,$key_val) = ($1,$2); |
---|
4786 | next unless $key_what =~ /^[TMNA]\z/; |
---|
4787 | if ($key_what eq 'T') { |
---|
4788 | $key_val = '.' . $key_val; # prepend a dot for compatibility |
---|
4789 | } elsif ($key_what eq 'A') { |
---|
4790 | if ($key_val eq 'U') { $key_val = 'UNDECIPHERABLE' } else { next } |
---|
4791 | } |
---|
4792 | do_log(4, sprintf("check_for_banned (%s) %s=%s", |
---|
4793 | $p->base_name, $key_what, $key_val)); |
---|
4794 | ($result,$matchingkey) = lookup(0,$key_val,@$bfnmr); |
---|
4795 | $found++ if defined $result; |
---|
4796 | last if $found; |
---|
4797 | } |
---|
4798 | } |
---|
4799 | } |
---|
4800 | my($key_val_str) = join(' | ',@descr); $key_val_str =~ s/\t/,/g; |
---|
4801 | if (!$found) { # try new style |
---|
4802 | ($result,$matchingkey) = |
---|
4803 | lookup(0,join("\n",@descr), |
---|
4804 | Amavis::Lookup::Label->new('banned_namepath_re'), |
---|
4805 | cr('banned_namepath_re')); |
---|
4806 | $found++ if defined $result; |
---|
4807 | } |
---|
4808 | my($ll) = $result ? 1 : 3; |
---|
4809 | if (ll($ll)) { # only bother with logging when needed |
---|
4810 | my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b", |
---|
4811 | e => "\e", a => "\a", t => "\t"); |
---|
4812 | my($mk) = $matchingkey; # pretty-print |
---|
4813 | $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : '\\'.$1 }egsx; |
---|
4814 | do_log($ll, sprintf('p.path%s: "%s"%s', |
---|
4815 | !$result?'':" BANNED:$result", $key_val_str, |
---|
4816 | !defined $result ? '' : ", matching_key=\"$mk\"")); |
---|
4817 | } |
---|
4818 | if ($result) { |
---|
4819 | push(@banned_part_descr, $key_val_str); |
---|
4820 | push(@banned_matching_keys, $matchingkey); |
---|
4821 | push(@banned_rhs, $result); |
---|
4822 | } |
---|
4823 | } |
---|
4824 | # return listrefs of parts descriptors, matching keys and lookup results |
---|
4825 | (\@banned_part_descr, \@banned_matching_keys, \@banned_rhs); |
---|
4826 | } |
---|
4827 | |
---|
4828 | 1; |
---|
4829 | |
---|
4830 | # |
---|
4831 | package Amavis::Unpackers::MIME; |
---|
4832 | use strict; |
---|
4833 | use re 'taint'; |
---|
4834 | |
---|
4835 | BEGIN { |
---|
4836 | use Exporter (); |
---|
4837 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
4838 | $VERSION = '2.034'; |
---|
4839 | @ISA = qw(Exporter); |
---|
4840 | %EXPORT_TAGS = (); |
---|
4841 | @EXPORT = (); |
---|
4842 | @EXPORT_OK = qw(&mime_decode); |
---|
4843 | } |
---|
4844 | use Errno qw(ENOENT EACCES); |
---|
4845 | use MIME::Parser; |
---|
4846 | use MIME::Words; |
---|
4847 | |
---|
4848 | BEGIN { |
---|
4849 | import Amavis::Conf qw(:platform c cr ca); |
---|
4850 | import Amavis::Timing qw(section_time); |
---|
4851 | import Amavis::Util qw(snmp_count ll do_log); |
---|
4852 | import Amavis::Unpackers::NewFilename qw(consumed_bytes); |
---|
4853 | } |
---|
4854 | |
---|
4855 | use subs @EXPORT_OK; |
---|
4856 | |
---|
4857 | # save MIME preamble and epilogue (if nontrivial) as extra (pseudo)parts |
---|
4858 | sub mime_decode_pre_epi($$$$$) { |
---|
4859 | my($pe_name, $pe_lines, $tempdir, $parent_obj, $placement) = @_; |
---|
4860 | if (defined $pe_lines && @$pe_lines) { |
---|
4861 | do_log(5, "mime_decode_$pe_name: " . scalar(@$pe_lines) . " lines"); |
---|
4862 | if (@$pe_lines > 5 || "@$pe_lines" !~ m{^[a-zA-Z0-9/\@:;,. \t\n_-]*\z}s) { |
---|
4863 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts", |
---|
4864 | $parent_obj); |
---|
4865 | $newpart_obj->mime_placement($placement); |
---|
4866 | $newpart_obj->name_declared($pe_name); |
---|
4867 | my($newpart) = $newpart_obj->full_name; |
---|
4868 | my($outpart) = IO::File->new; |
---|
4869 | $outpart->open($newpart,'>') |
---|
4870 | or die "Can't create $pe_name file $newpart: $!"; |
---|
4871 | binmode($outpart, ":bytes") or die "Can't cancel :utf8 mode: $!" |
---|
4872 | if $unicode_aware; |
---|
4873 | my($len); |
---|
4874 | for (@$pe_lines) { |
---|
4875 | $outpart->print($_) or die "Can't write $pe_name to $newpart: $!"; |
---|
4876 | $len += length($_); |
---|
4877 | } |
---|
4878 | $outpart->close or die "Can't close $pe_name $newpart: $!"; |
---|
4879 | $newpart_obj->size($len); |
---|
4880 | consumed_bytes($len, "mime_decode_$pe_name", 0, 1); |
---|
4881 | } |
---|
4882 | } |
---|
4883 | } |
---|
4884 | |
---|
4885 | # traverse MIME::Entity object depth-first, |
---|
4886 | # extracting preambles and epilogues as extra (pseudo)parts, and |
---|
4887 | # filling-in additional information into Amavis::Unpackers::Part objects |
---|
4888 | sub mime_traverse($$$$$); # prototype |
---|
4889 | sub mime_traverse($$$$$) { |
---|
4890 | my($entity, $tempdir, $parent_obj, $depth, $placement) = @_; |
---|
4891 | mime_decode_pre_epi('preamble', $entity->preamble, |
---|
4892 | $tempdir, $parent_obj, $placement); |
---|
4893 | my($mt, $et) = ($entity->mime_type, $entity->effective_type); |
---|
4894 | my($part); my($head) = $entity->head; my($body) = $entity->bodyhandle; |
---|
4895 | if (!defined($body)) { # a MIME container only contains parts, no bodypart |
---|
4896 | # create pseudo-part objects for MIME containers (e.g. multipart/* ) |
---|
4897 | $part = Amavis::Unpackers::Part->new(undef,$parent_obj); |
---|
4898 | # $part->type_short('no-file'); |
---|
4899 | do_log(2, $part->base_name." $placement Content-Type: $mt"); |
---|
4900 | } else { # does have a body part (i.e. not a MIME container) |
---|
4901 | my($fn) = $body->path; my($size); |
---|
4902 | if (!defined($fn)) { $size = length($body->as_string) } |
---|
4903 | else { |
---|
4904 | my($msg); my($errn) = lstat($fn) ? 0 : 0+$!; |
---|
4905 | if ($errn == ENOENT) { $msg = "does not exist" } |
---|
4906 | elsif ($errn) { $msg = "is inaccessible: $!" } |
---|
4907 | elsif (!-r _) { $msg = "is not readable" } |
---|
4908 | elsif (!-f _) { $msg = "is not a regular file" } |
---|
4909 | else { |
---|
4910 | $size = -s _; |
---|
4911 | do_log(4,"mime_traverse: file $fn is empty") if !$size; |
---|
4912 | } |
---|
4913 | do_log(-1,"WARN: mime_traverse: file $fn $msg") if defined $msg; |
---|
4914 | } |
---|
4915 | consumed_bytes($size, 'mime_decode', 0, 1); |
---|
4916 | # retrieve Amavis::Unpackers::Part object (if any), stashed into head obj |
---|
4917 | $part = Amavis::Unpackers::OurFiler::get_amavisd_part($head); |
---|
4918 | if (defined $part) { |
---|
4919 | $part->size($size); |
---|
4920 | if ($size==0) { $part->type_short('empty'); $part->type_long('empty') } |
---|
4921 | do_log(2, $part->base_name." $placement Content-Type: $mt" . |
---|
4922 | ", size: $size B, name: ".$entity->head->recommended_filename); |
---|
4923 | my($old_parent_obj) = $part->parent; |
---|
4924 | if ($parent_obj ne $old_parent_obj) { # reparent if necessary |
---|
4925 | ll(5) && do_log(5,sprintf("reparenting %s from %s to %s", |
---|
4926 | $part->base_name, |
---|
4927 | $old_parent_obj->base_name, $parent_obj->base_name)); |
---|
4928 | my($ch_ref) = $old_parent_obj->children; |
---|
4929 | $old_parent_obj->children([grep {$_ ne $part} @$ch_ref]); |
---|
4930 | $ch_ref = $parent_obj->children; |
---|
4931 | push(@$ch_ref,$part); $parent_obj->children($ch_ref); |
---|
4932 | $part->parent($parent_obj); |
---|
4933 | } |
---|
4934 | } |
---|
4935 | } |
---|
4936 | if (defined $part) { |
---|
4937 | $part->mime_placement($placement); |
---|
4938 | $part->type_declared($mt eq $et ? $mt : [$mt, $et]); |
---|
4939 | my(@rn); # recommended file names, both raw and RFC 2047 decoded |
---|
4940 | my($val, $val_decoded); |
---|
4941 | $val = $head->mime_attr('content-disposition.filename'); |
---|
4942 | if ($val ne '') { |
---|
4943 | push(@rn, $val); |
---|
4944 | $val_decoded = MIME::Words::decode_mimewords($val); |
---|
4945 | push(@rn, $val_decoded) if $val_decoded ne $val; |
---|
4946 | } |
---|
4947 | $val = $head->mime_attr('content-type.name'); |
---|
4948 | if ($val ne '') { |
---|
4949 | $val_decoded = MIME::Words::decode_mimewords($val); |
---|
4950 | push(@rn, $val_decoded) if !grep { $_ eq $val_decoded } @rn; |
---|
4951 | push(@rn, $val) if !grep { $_ eq $val } @rn; |
---|
4952 | } |
---|
4953 | $part->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn; |
---|
4954 | } |
---|
4955 | mime_decode_pre_epi('epilogue', $entity->epilogue, |
---|
4956 | $tempdir, $parent_obj, $placement); |
---|
4957 | my($item_num) = 0; |
---|
4958 | for my $e ($entity->parts) { # recursive descent |
---|
4959 | $item_num++; |
---|
4960 | mime_traverse($e,$tempdir,$part,$depth+1,"$placement/$item_num"); |
---|
4961 | } |
---|
4962 | } |
---|
4963 | |
---|
4964 | # Break up mime parts, return MIME::Entity object |
---|
4965 | sub mime_decode($$$) { |
---|
4966 | my($fileh, $tempdir, $parent_obj) = @_; |
---|
4967 | # $fileh may be an open file handle, or a file name |
---|
4968 | |
---|
4969 | my($parser) = MIME::Parser->new; |
---|
4970 | $parser->filer(Amavis::Unpackers::OurFiler->new("$tempdir/parts", |
---|
4971 | $parent_obj)); |
---|
4972 | $parser->ignore_errors(1); # also is the default |
---|
4973 | # $parser->extract_nested_messages(0); |
---|
4974 | $parser->extract_nested_messages("NEST"); # parse embedded message/rfc822 |
---|
4975 | $parser->extract_uuencode(1); |
---|
4976 | my($entity); |
---|
4977 | snmp_count('OpsDecByMimeParser'); |
---|
4978 | if (ref($fileh)) { # assume open file handle |
---|
4979 | do_log(4, "Extracting mime components"); |
---|
4980 | $fileh->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
4981 | local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted ! |
---|
4982 | $entity = $parser->parse($fileh); |
---|
4983 | } else { # assume $fileh is a file name |
---|
4984 | do_log(4, "Extracting mime components from $fileh"); |
---|
4985 | local($1,$2,$3,$4); # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted ! |
---|
4986 | $entity = $parser->parse_open("$tempdir/parts/$fileh"); |
---|
4987 | } |
---|
4988 | # my($mime_err) = $parser->last_error; # deprecated |
---|
4989 | my($mime_err) = $parser->results->errors; |
---|
4990 | $mime_err=~s/\s+\z//; $mime_err=~s/[ \t\r]*\n+/; /g; $mime_err=~s/\s+/ /g; |
---|
4991 | $mime_err = substr($mime_err,0,250) . '...' if length($mime_err) > 250; |
---|
4992 | do_log(1, "WARN: MIME::Parser $mime_err") if $mime_err ne ''; |
---|
4993 | mime_traverse($entity, $tempdir, $parent_obj, 0, '1'); |
---|
4994 | section_time('mime_decode'); |
---|
4995 | ($entity, $mime_err); |
---|
4996 | } |
---|
4997 | |
---|
4998 | 1; |
---|
4999 | |
---|
5000 | # |
---|
5001 | package Amavis::Notify; |
---|
5002 | use strict; |
---|
5003 | use re 'taint'; |
---|
5004 | |
---|
5005 | BEGIN { |
---|
5006 | use Exporter (); |
---|
5007 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
5008 | $VERSION = '2.034'; |
---|
5009 | @ISA = qw(Exporter); |
---|
5010 | %EXPORT_TAGS = (); |
---|
5011 | @EXPORT = (); |
---|
5012 | @EXPORT_OK = qw(&delivery_status_notification &delivery_short_report |
---|
5013 | &string_to_mime_entity &defanged_mime_entity); |
---|
5014 | } |
---|
5015 | |
---|
5016 | BEGIN { |
---|
5017 | import Amavis::Util qw(ll do_log am_id safe_encode q_encode); |
---|
5018 | import Amavis::Timing qw(section_time); |
---|
5019 | import Amavis::Conf qw(:platform $myhostname c cr ca); |
---|
5020 | import Amavis::Lookup qw(lookup); |
---|
5021 | import Amavis::Expand qw(expand); |
---|
5022 | import Amavis::rfc2821_2822_Tools; |
---|
5023 | } |
---|
5024 | # use Encode; # Perl 5.8 UTF-8 support |
---|
5025 | use MIME::Entity; |
---|
5026 | |
---|
5027 | use subs @EXPORT_OK; |
---|
5028 | |
---|
5029 | # Convert mail (that was obtained by macro-expanding notification templates) |
---|
5030 | # into proper MIME::Entity object. Some ad-hoc solutions are used |
---|
5031 | # for compatibility with previous version. |
---|
5032 | # |
---|
5033 | sub string_to_mime_entity($) { |
---|
5034 | my($mail_as_string_ref) = @_; |
---|
5035 | local($1,$2,$3); my($entity); my($m_hdr,$m_body); |
---|
5036 | ($m_hdr, $m_body) = ($1, $3) |
---|
5037 | if $$mail_as_string_ref =~ /^(.*?\r?\n)(\r?\n|\z)(.*)\z/s; |
---|
5038 | $m_body = safe_encode(c('bdy_encoding'), $m_body); |
---|
5039 | # make sure _our_ source line number is reported in case of failure |
---|
5040 | my($nxmh) = c('notify_xmailer_header'); |
---|
5041 | eval {$entity = MIME::Entity->build( |
---|
5042 | Type => 'text/plain', Encoding => '-SUGGEST', Charset=> c('bdy_encoding'), |
---|
5043 | (defined $nxmh && $nxmh eq '' ? () # leave the MIME::Entity default |
---|
5044 | : ('X-Mailer' => $nxmh) ), # X-Mailer hdr or undef |
---|
5045 | Data => $m_body); 1} or do {chomp($@); die $@}; |
---|
5046 | my($head) = $entity->head; |
---|
5047 | # insert header fields from template into MIME::Head entity |
---|
5048 | $m_hdr =~ s/\r?\n([ \t])/$1/g; # unfold template header |
---|
5049 | for my $hdr_line (split(/\r?\n/, $m_hdr)) { |
---|
5050 | if ($hdr_line =~ /^([^:]*):\s*(.*)\z/s) { |
---|
5051 | my($fhead, $fbody) = ($1, $2); |
---|
5052 | # encode according to RFC 2047 if necessary |
---|
5053 | $fhead = safe_encode('ascii', $fhead); |
---|
5054 | if ($fhead =~ /^(X-.*|Subject|Comments)\z/si && |
---|
5055 | $fbody =~ /[^\011\012\040-\176]/) # nonprint. except TAB and LF? |
---|
5056 | { # encode according to RFC 2047 |
---|
5057 | # TODO: shouldn't we unfold first?! |
---|
5058 | my($fbody_octets); |
---|
5059 | if (!$unicode_aware) { $fbody_octets = $fbody } |
---|
5060 | else { |
---|
5061 | $fbody_octets = safe_encode(c('hdr_encoding'), $fbody); |
---|
5062 | do_log(5, "string_to_mime_entity UTF-8 body: $fbody"); |
---|
5063 | do_log(5, "string_to_mime_entity body octets: $fbody_octets"); |
---|
5064 | } |
---|
5065 | my($qb) = c('hdr_encoding_qb'); |
---|
5066 | if (uc($qb) eq 'Q') { |
---|
5067 | $fbody = q_encode($fbody_octets, $qb, c('hdr_encoding')); |
---|
5068 | } else { |
---|
5069 | $fbody = MIME::Words::encode_mimeword($fbody_octets, |
---|
5070 | $qb, c('hdr_encoding')); |
---|
5071 | } |
---|
5072 | } else { # supposed to be in plain ASCII, let's make sure it is |
---|
5073 | $fbody = safe_encode('ascii', $fbody); |
---|
5074 | } |
---|
5075 | do_log(5, sprintf("string_to_mime_entity %s: %s", $fhead, $fbody)); |
---|
5076 | # make sure _our_ source line number is reported in case of failure |
---|
5077 | if (!eval { $head->replace($fhead, $fbody); 1 }) { |
---|
5078 | chomp($@); |
---|
5079 | die sprintf("%s header field '%s: %s'", |
---|
5080 | ($@ eq '' ? "invalid" : "$@, "), $fhead, $fbody); |
---|
5081 | } |
---|
5082 | } |
---|
5083 | } |
---|
5084 | $entity; # return the built MIME::Entity |
---|
5085 | } |
---|
5086 | |
---|
5087 | # Generate delivery status notification according to |
---|
5088 | # rfc1892 (now rfc3462) and rfc1894 (now rfc3464). |
---|
5089 | # Return dsn message object if dsn is needed, or undef otherwise. |
---|
5090 | # |
---|
5091 | sub delivery_status_notification($$$$$) { |
---|
5092 | my($conn,$msginfo,$report_success_dsn_also,$builtins_ref,$template_ref) = @_; |
---|
5093 | my($dsn_time) = time; # time of dsn creation - now |
---|
5094 | my($notification); |
---|
5095 | if ($msginfo->sender eq '') { # must not respond to null reverse path |
---|
5096 | do_log(4, "Not sending DSN to empty return path"); |
---|
5097 | } else { |
---|
5098 | my($from_mta, $client_ip) = ($conn->smtp_helo, $conn->client_ip); |
---|
5099 | my($msg) = ''; # constructed dsn text according to rfc3464 |
---|
5100 | $msg .= "Reporting-MTA: dns; $myhostname\n"; |
---|
5101 | $msg .= "Received-From-MTA: smtp; $from_mta ([$client_ip])\n" |
---|
5102 | if $from_mta ne ''; |
---|
5103 | $msg .= "Arrival-Date: " . rfc2822_timestamp($msginfo->rx_time) . "\n"; |
---|
5104 | |
---|
5105 | my($any); # any recipients with failed delivery? |
---|
5106 | for my $r (@{$msginfo->per_recip_data}) { |
---|
5107 | my($remote_mta) = $r->recip_remote_mta; |
---|
5108 | my($smtp_resp) = $r->recip_smtp_response; |
---|
5109 | if (!$r->recip_done) { |
---|
5110 | if ($msginfo->delivery_method eq '') { # e.g. milter |
---|
5111 | # as far as we are concerned all is ok, delivery will be performed |
---|
5112 | # by a helper program or MTA |
---|
5113 | $smtp_resp = "250 2.5.0 Ok, continue delivery"; |
---|
5114 | } else { |
---|
5115 | do_log(-2,"TROUBLE: recipient not done: <" |
---|
5116 | . $r->recip_addr . "> " . $smtp_resp); |
---|
5117 | } |
---|
5118 | } |
---|
5119 | my($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg); |
---|
5120 | if ($smtp_resp =~ /^ (\d{3}) \s+ ([245] \. \d{1,3} \. \d{1,3})? |
---|
5121 | \s* (.*) \z/xs) { |
---|
5122 | ($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg) = ($1,$2,$3); |
---|
5123 | } else { |
---|
5124 | $smtp_resp_msg = $smtp_resp; |
---|
5125 | } |
---|
5126 | my($smtp_resp_class) = $smtp_resp_code =~ /^(\d)/ ? $1 : '0'; |
---|
5127 | if ($smtp_resp_enhcode eq '' && $smtp_resp_class =~ /^([245])\z/) { |
---|
5128 | $smtp_resp_enhcode = "$1.0.0"; |
---|
5129 | } |
---|
5130 | # skip success notifications |
---|
5131 | next unless $smtp_resp_class ne '2' || $report_success_dsn_also; |
---|
5132 | $any++; |
---|
5133 | $msg .= "\n"; # empty line between groups of per-recipient fields |
---|
5134 | if ($remote_mta ne '' && $r->recip_final_addr ne $r->recip_addr) { |
---|
5135 | $msg .= "X-NextToLast-Final-Recipient: rfc822; " |
---|
5136 | . quote_rfc2821_local($r->recip_addr) . "\n"; |
---|
5137 | $msg .= "Final-Recipient: rfc822; " |
---|
5138 | . quote_rfc2821_local($r->recip_final_addr) . "\n"; |
---|
5139 | } else { |
---|
5140 | $msg .= "Final-Recipient: rfc822; " |
---|
5141 | . quote_rfc2821_local($r->recip_addr) . "\n"; |
---|
5142 | } |
---|
5143 | $msg .= "Action: ".($smtp_resp_class eq '2' ? 'delivered':'failed')."\n"; |
---|
5144 | $msg .= "Status: $smtp_resp_enhcode\n"; |
---|
5145 | my($rem_smtp_resp) = $r->recip_remote_mta_smtp_response; |
---|
5146 | if ($remote_mta eq '' || $rem_smtp_resp eq '') { |
---|
5147 | $msg .= "Diagnostic-Code: smtp; $smtp_resp\n"; |
---|
5148 | } else { |
---|
5149 | $msg .= "Remote-MTA: dns; $remote_mta\n"; |
---|
5150 | $msg .= "Diagnostic-Code: smtp; $rem_smtp_resp\n"; |
---|
5151 | } |
---|
5152 | $msg .= "Last-Attempt-Date: " . rfc2822_timestamp($dsn_time) . "\n"; |
---|
5153 | } |
---|
5154 | return $notification if !$any; # don't bother, we won't be sending DSN |
---|
5155 | |
---|
5156 | my($to_hdr) = qquote_rfc2821_local($msginfo->sender_contact); |
---|
5157 | |
---|
5158 | # use the provided template text |
---|
5159 | my(%mybuiltins) = %$builtins_ref; # make a local copy |
---|
5160 | $mybuiltins{'f'} = c('hdrfrom_notify_sender'); |
---|
5161 | $mybuiltins{'T'} = $to_hdr; |
---|
5162 | $mybuiltins{'d'} = rfc2822_timestamp($dsn_time); |
---|
5163 | my($dsn) = expand($template_ref, \%mybuiltins); |
---|
5164 | |
---|
5165 | my($dsn_entity) = string_to_mime_entity($dsn); |
---|
5166 | $dsn_entity->make_multipart; |
---|
5167 | my($head) = $dsn_entity->head; |
---|
5168 | |
---|
5169 | # rfc3464: The From field of the message header of the DSN SHOULD contain |
---|
5170 | # the address of a human who is responsible for maintaining the mail system |
---|
5171 | # at the Reporting MTA site (e.g. Postmaster), so that a reply to the |
---|
5172 | # DSN will reach that person. |
---|
5173 | eval { $head->replace('From', c('hdrfrom_notify_sender')); 1 } |
---|
5174 | or do { chomp($@); die $@ }; |
---|
5175 | eval { $head->replace('To', $to_hdr); 1 } or do { chomp($@); die $@ }; |
---|
5176 | eval { $head->replace('Date', rfc2822_timestamp($dsn_time)); 1 } |
---|
5177 | or do { chomp($@); die $@ }; |
---|
5178 | |
---|
5179 | my($field) = Mail::Field->new('Content_type'); # underline, not hyphen! |
---|
5180 | $field->type("multipart/report; report-type=delivery-status"); |
---|
5181 | $field->boundary(MIME::Entity::make_boundary()); |
---|
5182 | $head->replace('Content-type', $field->stringify); |
---|
5183 | $head = undef; |
---|
5184 | |
---|
5185 | # make sure _our_ source line number is reported in case of failure |
---|
5186 | eval {$dsn_entity->attach( |
---|
5187 | Type => 'message/delivery-status', Encoding => '7bit', |
---|
5188 | Description => 'Delivery error report', |
---|
5189 | Data => $msg); 1} or do {chomp($@); die $@}; |
---|
5190 | eval {$dsn_entity->attach( |
---|
5191 | Type => 'text/rfc822-headers', Encoding => '-SUGGEST', |
---|
5192 | Description => 'Undelivered-message headers', |
---|
5193 | Data => $msginfo->orig_header); 1} or do {chomp($@); die $@}; |
---|
5194 | $notification = Amavis::In::Message->new; |
---|
5195 | $notification->rx_time($dsn_time); |
---|
5196 | $notification->delivery_method(c('notify_method')); |
---|
5197 | $notification->sender(c('mailfrom_notify_sender')); # should be empty! |
---|
5198 | $notification->auth_submitter('<>'); |
---|
5199 | $notification->auth_user(c('amavis_auth_user')); |
---|
5200 | $notification->auth_pass(c('amavis_auth_pass')); |
---|
5201 | $notification->recips([$msginfo->sender_contact]); |
---|
5202 | $notification->mail_text($dsn_entity); |
---|
5203 | } |
---|
5204 | $notification; |
---|
5205 | } |
---|
5206 | |
---|
5207 | # Return a pair of arrayrefs of quoted recipient addresses (the first lists |
---|
5208 | # recipients with successful delivery status, the second all the rest), |
---|
5209 | # plus a list of short per-recipient delivery reports for failed deliveries, |
---|
5210 | # that can be used in the first MIME part (the free text format) of delivery |
---|
5211 | # status notifications. |
---|
5212 | # |
---|
5213 | sub delivery_short_report($) { |
---|
5214 | my($msginfo) = @_; |
---|
5215 | my(@succ_recips, @failed_recips, @failed_recips_full); |
---|
5216 | for my $r (@{$msginfo->per_recip_data}) { |
---|
5217 | my($remote_mta) = $r->recip_remote_mta; |
---|
5218 | my($smtp_resp) = $r->recip_smtp_response; |
---|
5219 | my($qrecip_addr) = scalar(qquote_rfc2821_local($r->recip_addr)); |
---|
5220 | if ($r->recip_destiny == D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)) { |
---|
5221 | push(@succ_recips, $qrecip_addr); |
---|
5222 | } else { |
---|
5223 | push(@failed_recips, $qrecip_addr); |
---|
5224 | push(@failed_recips_full, |
---|
5225 | sprintf("%s:%s\n %s", $qrecip_addr, |
---|
5226 | ($remote_mta eq ''?'':" $remote_mta said:"), $smtp_resp)); |
---|
5227 | } |
---|
5228 | } |
---|
5229 | (\@succ_recips, \@failed_recips, \@failed_recips_full); |
---|
5230 | } |
---|
5231 | |
---|
5232 | # Build a new MIME::Entity object based on the original mail, but hopefully |
---|
5233 | # safer to mail readers: conventional mail header fields are retained, |
---|
5234 | # original mail becomes an attachment of type 'message/rfc822'. |
---|
5235 | # Text in $first_part becomes the first MIME part of type 'text/plain'. |
---|
5236 | # |
---|
5237 | sub defanged_mime_entity($$$) { |
---|
5238 | my($conn,$msginfo,$first_part) = @_; |
---|
5239 | my($new_entity); |
---|
5240 | my($resent_time) = $msginfo->rx_time; |
---|
5241 | $first_part = safe_encode(c('bdy_encoding'), $first_part); |
---|
5242 | # make sure _our_ source line number is reported in case of failure |
---|
5243 | my($nxmh) = c('notify_xmailer_header'); |
---|
5244 | eval {$new_entity = MIME::Entity->build( |
---|
5245 | Type => 'multipart/mixed', |
---|
5246 | (defined $nxmh && $nxmh eq '' ? () # leave the MIME::Entity default |
---|
5247 | : ('X-Mailer' => $nxmh) ), # X-Mailer hdr or undef |
---|
5248 | ); 1} or do {chomp($@); die $@}; |
---|
5249 | my($head) = $new_entity->head; |
---|
5250 | my($orig_head) = $msginfo->mime_entity->head; |
---|
5251 | # TODO: we should retain the ordering of Resent-* with their Received fields |
---|
5252 | for my $field_head ( # copy some of the original header fields |
---|
5253 | qw(Received From Sender To Cc Reply-To Date Message-ID |
---|
5254 | Resent-From Resent-Sender Resent-To Resent-Cc |
---|
5255 | Resent-Date Resent-Message-ID |
---|
5256 | In-Reply-To References Subject |
---|
5257 | Comments Keywords Organization X-Mailer) ) { |
---|
5258 | for my $value ($orig_head->get_all($field_head)) { |
---|
5259 | do_log(4, "copying-over the header field: $field_head"); |
---|
5260 | eval { $head->add($field_head, $value); 1 } or do {chomp($@); die $@}; |
---|
5261 | } |
---|
5262 | } |
---|
5263 | $head = undef; # object not needed any longer |
---|
5264 | eval {$new_entity->attach( |
---|
5265 | Type => 'text/plain', Encoding => '-SUGGEST', Charset => c('bdy_encoding'), |
---|
5266 | Data => $first_part); 1} or do {chomp($@); die $@}; |
---|
5267 | eval {$new_entity->attach( # rfc2046 |
---|
5268 | Type => 'message/rfc822; x-spam-type=original', |
---|
5269 | Encoding => '8bit', Path => $msginfo->mail_text_fn, |
---|
5270 | Description => 'Original message', |
---|
5271 | Filename => 'message.txt', Disposition => 'attachment'); 1} |
---|
5272 | or do {chomp($@); die $@}; |
---|
5273 | $new_entity; |
---|
5274 | } |
---|
5275 | |
---|
5276 | 1; |
---|
5277 | |
---|
5278 | # |
---|
5279 | package Amavis::Cache; |
---|
5280 | # offer an 'IPC::Cache'-compatible simple interface |
---|
5281 | # to a local (per-process) memory-based cache; |
---|
5282 | use strict; |
---|
5283 | use re 'taint'; |
---|
5284 | |
---|
5285 | BEGIN { |
---|
5286 | import Amavis::Util qw(ll do_log); |
---|
5287 | } |
---|
5288 | BEGIN { |
---|
5289 | use Exporter (); |
---|
5290 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
5291 | $VERSION = '2.0341'; |
---|
5292 | @ISA = qw(Exporter); |
---|
5293 | } |
---|
5294 | |
---|
5295 | # simple local memory-based cache |
---|
5296 | sub new { # called by each child process |
---|
5297 | my($class) = @_; |
---|
5298 | do_log(5,"BerkeleyDB not available, using memory-based local cache"); |
---|
5299 | bless {}, $class; |
---|
5300 | } |
---|
5301 | sub get { my($self,$key) = @_; thaw($self->{$key}) } |
---|
5302 | sub set { my($self,$key,$obj) = @_; $self->{$key} = freeze($obj) } |
---|
5303 | |
---|
5304 | # protect % and ~, as well as NUL and \200 for good measure |
---|
5305 | sub encode($) { |
---|
5306 | my($str) = @_; $str =~ s/[%~\000\200]/sprintf("%%%02X",ord($&))/egs; $str; |
---|
5307 | } |
---|
5308 | |
---|
5309 | # simple Storable::freeze lookalike |
---|
5310 | sub freeze($); # prototype |
---|
5311 | sub freeze($) { |
---|
5312 | my($obj) = @_; my($ty) = ref($obj); |
---|
5313 | if (!defined($obj)) { 'U' } |
---|
5314 | elsif (!$ty) { join('~', '', encode($obj)) } # string |
---|
5315 | elsif ($ty eq 'SCALAR') { join('~', 'S', encode(freeze($$obj))) } |
---|
5316 | elsif ($ty eq 'REF') { join('~', 'R', encode(freeze($$obj))) } |
---|
5317 | elsif ($ty eq 'ARRAY') { join('~', 'A', map {encode(freeze($_))} @$obj) } |
---|
5318 | elsif ($ty eq 'HASH') { |
---|
5319 | join('~','H',map {(encode($_),encode(freeze($obj->{$_})))} sort keys %$obj) |
---|
5320 | } else { die "Can't freeze object type $ty" } |
---|
5321 | } |
---|
5322 | |
---|
5323 | # simple Storable::thaw lookalike |
---|
5324 | sub thaw($); # prototype |
---|
5325 | sub thaw($) { |
---|
5326 | my($str) = @_; |
---|
5327 | return undef if !defined $str; |
---|
5328 | my($ty,@val) = split(/~/,$str,-1); |
---|
5329 | for (@val) { s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg } |
---|
5330 | if ($ty eq 'U') { undef } |
---|
5331 | elsif ($ty eq '') { $val[0] } |
---|
5332 | elsif ($ty eq 'S') { my($obj)=thaw($val[0]); \$obj } |
---|
5333 | elsif ($ty eq 'R') { my($obj)=thaw($val[0]); \$obj } |
---|
5334 | elsif ($ty eq 'A') { [map {thaw($_)} @val] } |
---|
5335 | elsif ($ty eq 'H') { |
---|
5336 | my($hr) = {}; |
---|
5337 | while (@val) { my($k) = shift @val; $hr->{$k} = thaw(shift @val) } |
---|
5338 | $hr; |
---|
5339 | } else { die "Can't thaw object type $ty" } |
---|
5340 | } |
---|
5341 | |
---|
5342 | 1; |
---|
5343 | |
---|
5344 | # |
---|
5345 | package Amavis; |
---|
5346 | require 5.005; # need qr operator and \z in regexps |
---|
5347 | use strict; |
---|
5348 | use re 'taint'; |
---|
5349 | |
---|
5350 | use POSIX qw(strftime); |
---|
5351 | use Errno qw(ENOENT EACCES); |
---|
5352 | use IO::File (); |
---|
5353 | # body digest for caching, either SHA1 or MD5 |
---|
5354 | #use Digest::SHA1; |
---|
5355 | use Digest::MD5; |
---|
5356 | use Net::Server 0.83; |
---|
5357 | use Net::Server::PreForkSimple; |
---|
5358 | |
---|
5359 | BEGIN { |
---|
5360 | import Amavis::Conf qw(:platform :sa :confvars c cr ca); |
---|
5361 | import Amavis::Util qw(untaint min max ll do_log sanitize_str debug_oneshot |
---|
5362 | am_id snmp_counters_init snmp_count prolong_timer); |
---|
5363 | import Amavis::Log; |
---|
5364 | import Amavis::Timing qw(section_time get_time_so_far); |
---|
5365 | import Amavis::rfc2821_2822_Tools; |
---|
5366 | import Amavis::Lookup qw(lookup lookup_ip_acl); |
---|
5367 | import Amavis::Out; |
---|
5368 | import Amavis::Out::EditHeader; |
---|
5369 | import Amavis::UnmangleSender qw(best_try_originator_ip best_try_originator |
---|
5370 | first_received_from); |
---|
5371 | import Amavis::Unpackers::Validity qw( |
---|
5372 | check_header_validity check_for_banned_names); |
---|
5373 | import Amavis::Unpackers::MIME qw(mime_decode); |
---|
5374 | import Amavis::Expand qw(expand); |
---|
5375 | import Amavis::Notify qw(delivery_status_notification delivery_short_report |
---|
5376 | string_to_mime_entity defanged_mime_entity); |
---|
5377 | import Amavis::In::Connection; |
---|
5378 | import Amavis::In::Message; |
---|
5379 | } |
---|
5380 | |
---|
5381 | # Make it a subclass of Net::Server::PreForkSimple |
---|
5382 | # to override method &process_request (and others if desired) |
---|
5383 | use vars qw(@ISA); |
---|
5384 | # @ISA = qw(Net::Server); |
---|
5385 | @ISA = qw(Net::Server::PreForkSimple); |
---|
5386 | |
---|
5387 | delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; |
---|
5388 | |
---|
5389 | use vars qw( |
---|
5390 | $extra_code_db $extra_code_cache |
---|
5391 | $extra_code_sql $extra_code_ldap |
---|
5392 | $extra_code_in_amcl $extra_code_in_smtp $extra_code_in_qmqpqq |
---|
5393 | $extra_code_antivirus $extra_code_antispam $extra_code_unpackers); |
---|
5394 | |
---|
5395 | use vars qw(%modules_basic); |
---|
5396 | use vars qw($spam_level $spam_status $spam_report); |
---|
5397 | use vars qw($user_id_sql $wb_listed_sql $implicit_maps_inserted); |
---|
5398 | use vars qw($db_env $snmp_db); |
---|
5399 | use vars qw($body_digest $body_digest_cache); |
---|
5400 | use vars qw(%builtins); # customizable notification messages |
---|
5401 | use vars qw($child_invocation_count $child_task_count); |
---|
5402 | # $child_invocation_count # counts child re-use from 1 to max_requests |
---|
5403 | # $child_task_count # counts check_mail() calls - this normally runs |
---|
5404 | # in sync with $child_invocation_count, but with |
---|
5405 | # SMTP or LMTP input there may be more than one |
---|
5406 | # message passed during a single SMTP session |
---|
5407 | use vars qw(@config_files); |
---|
5408 | use vars qw($VIRUSFILE $CONN $MSGINFO); |
---|
5409 | use vars qw($av_output @virusname @detecting_scanners |
---|
5410 | @banned_filename @bad_headers); |
---|
5411 | |
---|
5412 | use vars qw($amcl_in_obj $smtp_in_obj); # Amavis::In::AMCL and In::SMTP objects |
---|
5413 | use vars qw($qmqpqq_in_obj); # Amavis::In::QMQPqq object |
---|
5414 | use vars qw($sql_policy $sql_wblist); # Amavis::Lookup::SQL objects |
---|
5415 | use vars qw($ldap_policy); # Amavis::Lookup::LDAP objects |
---|
5416 | |
---|
5417 | # initialize some remaining global variables; |
---|
5418 | # invoked after chroot and after privileges have been dropped |
---|
5419 | sub after_chroot_init() { |
---|
5420 | $child_invocation_count = $child_task_count = 0; |
---|
5421 | %modules_basic = %INC; # helps to track missing modules in chroot |
---|
5422 | my(@msg); |
---|
5423 | my($euid) = $>; # effective UID |
---|
5424 | $> = 0; # try to become root |
---|
5425 | POSIX::setuid(0) if $> != 0; # and try some more |
---|
5426 | if ($> == 0) { # succeded? panic! |
---|
5427 | @msg = ("It is possible to change EUID from $euid to root, ABORTING!", |
---|
5428 | "Perhaps you forgot to patch the Net::Server - see:", |
---|
5429 | " http://www.ijs.si/software/amavisd/#net-server-sec", |
---|
5430 | "or start as non-root, e.g. by su(1) or using option -u user"); |
---|
5431 | } elsif ($daemon_chroot_dir eq '') { |
---|
5432 | # A quick check on vulnerability/protection of a config file |
---|
5433 | # (non-exhaustive: doesn't test for symlink tricks and higher directories). |
---|
5434 | # The config file has already been executed by now, so it may be |
---|
5435 | # too late to feel sorry now, but better late then never. |
---|
5436 | for my $config_file (@config_files) { |
---|
5437 | my($fh) = IO::File->new; |
---|
5438 | my($errn) = lstat($config_file) ? 0 : 0+$!; |
---|
5439 | if ($errn) { # not accessible, don't bother to test further |
---|
5440 | } elsif ($fh->open($config_file,'+<')) { |
---|
5441 | push(@msg, "Config file \"$config_file\" is writable, ". |
---|
5442 | "UID $<, EUID $>, EGID $)" ); |
---|
5443 | $fh->close; # close, ignoring status |
---|
5444 | } elsif (rename($config_file, $config_file.'.moved')) { |
---|
5445 | my($m) = 'appears writable (unconfirmed)'; |
---|
5446 | if (!-e $config_file && -e $config_file.'.moved') { |
---|
5447 | rename($config_file.'.moved', $config_file); # try to rename back |
---|
5448 | $m = 'is writable (confirmed)'; |
---|
5449 | } |
---|
5450 | push(@msg, "Directory of a config file \"$config_file\" $m, ". |
---|
5451 | "UID $<, EUID $>, EGID $)" ); |
---|
5452 | } |
---|
5453 | last if @msg; |
---|
5454 | } |
---|
5455 | } |
---|
5456 | if (@msg) { |
---|
5457 | do_log(-3,"FATAL: $_") for @msg; |
---|
5458 | print STDERR (map {"$_\n"} @msg); |
---|
5459 | die "SECURITY PROBLEM, ABORTING"; |
---|
5460 | exit 1; # just in case |
---|
5461 | } |
---|
5462 | # report versions of some modules |
---|
5463 | for my $m ('Amavis::Conf', |
---|
5464 | sort map { s/\.pm\z//; s[/][::]g; $_ } grep { /\.pm\z/ } keys %INC){ |
---|
5465 | next if !grep { $_ eq $m } qw(Amavis::Conf |
---|
5466 | Archive::Tar Archive::Zip Compress::Zlib Convert::TNEF Convert::UUlib |
---|
5467 | MIME::Entity MIME::Parser MIME::Tools Mail::Header Mail::Internet |
---|
5468 | Mail::ClamAV Mail::SpamAssassin Mail::SpamAssassin::SpamCopURI URI |
---|
5469 | Razor2::Client::Version Mail::SPF::Query Authen::SASL |
---|
5470 | Net::DNS Net::SMTP Net::Cmd Net::Server Net::LDAP |
---|
5471 | DBI BerkeleyDB DB_File SAVI Unix::Syslog Time::HiRes); |
---|
5472 | do_log(0, sprintf("Module %-19s %s", $m, $m->VERSION || '?')); |
---|
5473 | } |
---|
5474 | if (c('forward_method') eq '' && $extra_code_in_smtp) { |
---|
5475 | do_log(1,"forward_method in default policy bank is null (milter setup?), ". |
---|
5476 | "DISABLING SMTP-in AS A PRECAUTION"); |
---|
5477 | $extra_code_in_smtp = undef; |
---|
5478 | } |
---|
5479 | do_log(0,"Amavis::DB code ".($extra_code_db ?'':" NOT")." loaded"); |
---|
5480 | do_log(0,"Amavis::Cache code ".($extra_code_cache ?'':" NOT")." loaded"); |
---|
5481 | do_log(0,"Lookup::SQL code ".($extra_code_sql ?'':" NOT")." loaded"); |
---|
5482 | do_log(0,"Lookup::LDAP code ".($extra_code_ldap ?'':" NOT")." loaded"); |
---|
5483 | do_log(0,"AMCL-in protocol code ".($extra_code_in_amcl?'':" NOT")." loaded"); |
---|
5484 | do_log(0,"SMTP-in protocol code ".($extra_code_in_smtp?'':" NOT")." loaded"); |
---|
5485 | do_log(0,"QMQPqq-in protocol code ".($extra_code_in_qmqpqq?'':" NOT")." loaded"); |
---|
5486 | do_log(0,"ANTI-VIRUS code ".($extra_code_antivirus?'':" NOT")." loaded"); |
---|
5487 | do_log(0,"ANTI-SPAM code ".($extra_code_antispam ?'':" NOT")." loaded"); |
---|
5488 | do_log(0,"Unpackers code ".($extra_code_unpackers?'':" NOT")." loaded"); |
---|
5489 | |
---|
5490 | # Prepare a hash of macros to be used in notification message expansion. |
---|
5491 | # A key (macro name) must be a single character. Most characters are |
---|
5492 | # allowed, but to be on the safe side and for clarity it is suggested |
---|
5493 | # that only letters are used. Upper case letters may (as a mnemonic) |
---|
5494 | # suggest the value is an array, lower case may suggest the value is |
---|
5495 | # a scalar string - but this is only a convention and not enforced. |
---|
5496 | # |
---|
5497 | # A value may be a reference to a subroutine which will be called later at |
---|
5498 | # the time of macro expansion. This way we can provide a method for obtaining |
---|
5499 | # information which is not yet available, such as AV scanner results, |
---|
5500 | # or provide a lazy evaluation for more expensive calculations. |
---|
5501 | # Subroutine will be called in scalar context with no arguments. |
---|
5502 | # It may return a scalar string (or undef), or an array reference. |
---|
5503 | %builtins = ( |
---|
5504 | '.' => undef, |
---|
5505 | p => sub {c('policy_bank_path')}, |
---|
5506 | # mail reception timestamp (e.g. start of SMTP transaction): |
---|
5507 | d => sub {rfc2822_timestamp($MSGINFO->rx_time)}, # rfc2822 local date-time |
---|
5508 | # U => sub {iso8601_timestamp($MSGINFO->rx_time)}, # iso8601, local time |
---|
5509 | U => sub {iso8601_utc_timestamp($MSGINFO->rx_time)}, # iso8601 UTC |
---|
5510 | y => sub {sprintf("%.0f", 1000*get_time_so_far())}, # elapsed time in ms |
---|
5511 | u => sub {sprintf("%010d",$MSGINFO->rx_time)}, # s since Unix epoch (UTC) |
---|
5512 | |
---|
5513 | h => $myhostname, # dns name of this host, or configurable name |
---|
5514 | l => sub {my($ip) = $MSGINFO->client_addr; my($val); |
---|
5515 | $val = $ip ne '' ? lookup_ip_acl($ip,@{ca('mynetworks_maps')}) |
---|
5516 | : lookup(0,$MSGINFO->sender_source, |
---|
5517 | @{ca('local_domains_maps')}); |
---|
5518 | $val ? 1 : undef}, # sender's client IP (if known) from @mynetworks |
---|
5519 | # (if IP is known), or sender domain is local |
---|
5520 | s => sub {qquote_rfc2821_local($MSGINFO->sender)}, # original envelope sender in <> |
---|
5521 | S => sub { # unmangled sender or sender address to be notified, or empty... |
---|
5522 | sanitize_str($MSGINFO->sender_contact) }, # ..if sender unknown |
---|
5523 | o => sub { # best attempt at determining true sender (origin) of the virus, |
---|
5524 | sanitize_str($MSGINFO->sender_source) }, # normally same as %s |
---|
5525 | R => sub {$MSGINFO->recips}, # original message recipients list |
---|
5526 | D => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $y}, # succ.delivered |
---|
5527 | O => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $n}, # failed recips |
---|
5528 | N => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $f}, # short dsn |
---|
5529 | Q => sub {$MSGINFO->queue_id}, # MTA queue ID of the message if known |
---|
5530 | m => sub { local($_) = $MSGINFO->mime_entity; # Message-ID of the message |
---|
5531 | if (defined) { $_ = $_->head->get('Message-ID',0); chomp; |
---|
5532 | s/^[ \t]+//; s/[ \t\n]+\z//; # trim whitespace |
---|
5533 | # protect space and \n, other special characters |
---|
5534 | # will be sanitized before logging |
---|
5535 | s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }}, |
---|
5536 | r => sub { local($_) = $MSGINFO->mime_entity; # first Resent-Message-ID |
---|
5537 | if (defined) { $_ = $_->head->get('Resent-Message-ID',0); chomp; |
---|
5538 | s/^[ \t]+//; s/[ \t\n]+\z//; # trim whitespace |
---|
5539 | s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }}, |
---|
5540 | j => sub { local($_) = $MSGINFO->mime_entity; # Subject of the message |
---|
5541 | if (defined) { $_ = $_->head->get('Subject',0); chomp; |
---|
5542 | s/\n([ \t])/$1/g; # unfold |
---|
5543 | s{([=\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }}, |
---|
5544 | b => sub {$MSGINFO->body_digest}, # original message body digest |
---|
5545 | n => \&am_id, # amavis internal message id (for log entries) |
---|
5546 | i => sub {$VIRUSFILE}, # some quarantine id, e.g. quarantine filename |
---|
5547 | q => sub {my($q) = $MSGINFO->quarantined_to; |
---|
5548 | !defined($q) ? undef : |
---|
5549 | [map { my($m)=$_; $m=~s{^\Q$QUARANTINEDIR\E/}{}; $m } @$q]; |
---|
5550 | }, # list of quarantine mailboxes |
---|
5551 | v => sub {[split(/[ \t]*\r?\n/,$av_output)]}, # anti-virus scanner output |
---|
5552 | V => sub {my(%seen); [grep {!$seen{$_}++} @virusname]}, #unique virus names |
---|
5553 | F => sub {@banned_filename<=1 ? \@banned_filename |
---|
5554 | : [$banned_filename[0], '...'] }, # list of banned file names |
---|
5555 | X => sub {\@bad_headers}, # list of header syntax violations |
---|
5556 | W => sub {\@detecting_scanners}, # list of av scanners detecting a virus |
---|
5557 | H => sub {[map {my $h=$_; chomp($h); $h} @{$MSGINFO->orig_header}]},# orig hdr |
---|
5558 | A => sub {[split(/\r?\n/, $spam_report)]}, # SpamAssassin report lines |
---|
5559 | c => sub {!defined $spam_level ? '-' # SA hits/score |
---|
5560 | : 0+sprintf("%.3f",$spam_level+min(map {$_->recip_score_boost} |
---|
5561 | @{$MSGINFO->per_recip_data}))}, |
---|
5562 | z => sub {$MSGINFO->orig_body_size+1+$MSGINFO->orig_header_size}, # mail size |
---|
5563 | t => sub { # first entry in the Received trace |
---|
5564 | sanitize_str(first_received_from($MSGINFO->mime_entity)) }, |
---|
5565 | e => sub { # first valid public IP in the Received trace |
---|
5566 | sanitize_str(best_try_originator_ip($MSGINFO->mime_entity)) }, |
---|
5567 | a => sub {$MSGINFO->client_addr}, # original SMTP session client IP address |
---|
5568 | g => sub { # original SMTP session client DNS name |
---|
5569 | sanitize_str($MSGINFO->client_name) }, |
---|
5570 | k => sub { my($kill_level); |
---|
5571 | scalar(grep # any recipient declared the message be killed ? |
---|
5572 | { !$_->recip_whitelisted_sender && |
---|
5573 | ($_->recip_blacklisted_sender || |
---|
5574 | ($kill_level=lookup(0,$_->recip_addr, |
---|
5575 | @{ca('spam_kill_level_maps')}), |
---|
5576 | defined $spam_level && defined $kill_level && |
---|
5577 | $spam_level + $_->recip_score_boost >= $kill_level) ) |
---|
5578 | } @{$MSGINFO->per_recip_data}) }, |
---|
5579 | '1'=> sub { my($tag_level); |
---|
5580 | scalar(grep # above tag level for any recipient? |
---|
5581 | { !$_->recip_whitelisted_sender && |
---|
5582 | ($_->recip_blacklisted_sender || |
---|
5583 | ($tag_level=lookup(0,$_->recip_addr, |
---|
5584 | @{ca('spam_tag_level_maps')}), |
---|
5585 | defined $spam_level && defined $tag_level && |
---|
5586 | $spam_level + $_->recip_score_boost >= $tag_level) ) |
---|
5587 | } @{$MSGINFO->per_recip_data}) }, |
---|
5588 | '2'=> sub { my($tag2_level); |
---|
5589 | scalar(grep # above tag2 level for any recipient? |
---|
5590 | { !$_->recip_whitelisted_sender && |
---|
5591 | ($_->recip_blacklisted_sender || |
---|
5592 | ($tag2_level=lookup(0,$_->recip_addr, |
---|
5593 | @{ca('spam_tag2_level_maps')}), |
---|
5594 | defined $spam_level && defined $tag2_level && |
---|
5595 | $spam_level + $_->recip_score_boost >= $tag2_level) ) |
---|
5596 | } @{$MSGINFO->per_recip_data}) }, |
---|
5597 | # macros f, T, C, B will be defined for each notification as appropriate |
---|
5598 | # (representing From:, To:, Cc:, and Bcc: respectively) |
---|
5599 | # remaining free letters: wxyEGIJKLMPYZ |
---|
5600 | ); |
---|
5601 | |
---|
5602 | # Map local virtual 'localpart' to a mailbox (e.g. to a quarantine filename |
---|
5603 | # or a directory). Used by method 'local:', i.e. in mail_to_local_mailbox(), |
---|
5604 | # for direct local quarantining. The hash value may be a ref to a pair of |
---|
5605 | # fixed strings, or a subroutine ref (which must return a pair of strings |
---|
5606 | # (a list, not a list ref)) which makes possible lazy evaluation |
---|
5607 | # when some part of the pair is not known before the final delivery time. |
---|
5608 | # |
---|
5609 | # The first string in a pair must be either: |
---|
5610 | # - empty or undef, which will disable saving the message, |
---|
5611 | # - a filename, indicating a Unix-style mailbox, |
---|
5612 | # - a directory name, indicating a maildir-style mailbox, |
---|
5613 | # in which case the second string may provide a suggested file name. |
---|
5614 | # |
---|
5615 | %Amavis::Conf::local_delivery_aliases = ( |
---|
5616 | 'virus-quarantine' => sub { ($QUARANTINEDIR, $VIRUSFILE) }, |
---|
5617 | 'banned-quarantine' => sub { ($QUARANTINEDIR, $VIRUSFILE) }, |
---|
5618 | 'bad-header-quarantine'=>sub { ($QUARANTINEDIR, $VIRUSFILE) }, |
---|
5619 | # 'spam-quarantine' => sub { ($QUARANTINEDIR, $VIRUSFILE) }, # normal |
---|
5620 | 'spam-quarantine' => sub { ($QUARANTINEDIR, "$VIRUSFILE.gz") }, # gzipped |
---|
5621 | |
---|
5622 | # some more examples: |
---|
5623 | 'sender-quarantine' => |
---|
5624 | sub { my($s) = $MSGINFO->sender; local($1); |
---|
5625 | $s =~ s/[^a-zA-Z0-9._@]/=/g; $s =~ s/\@/%/g; |
---|
5626 | $s = untaint($s) if $s =~ /^([a-zA-Z0-9._=%]+)\z/; # untaint |
---|
5627 | $s =~ s/%/%%/g; # protect % |
---|
5628 | ( $QUARANTINEDIR, "sender-$s-%i-%n.gz" ); # suggested file name |
---|
5629 | }, |
---|
5630 | 'recip-quarantine' => |
---|
5631 | sub { ("$QUARANTINEDIR/recip-archive.mbox", undef) }, |
---|
5632 | 'ham-quarantine' => |
---|
5633 | sub { ("$QUARANTINEDIR/ham.mbox", undef) }, |
---|
5634 | 'outgoing-quarantine' => |
---|
5635 | sub { ("$QUARANTINEDIR/outgoing.mbox", undef) }, |
---|
5636 | 'incoming-quarantine' => |
---|
5637 | sub { ("$QUARANTINEDIR/incoming.mbox", undef) }, |
---|
5638 | ); |
---|
5639 | # store policy names into 'policy_bank_name' fields, if not explicitly set |
---|
5640 | for my $name (keys %policy_bank) { |
---|
5641 | if (ref($policy_bank{$name}) eq 'HASH' && |
---|
5642 | !exists($policy_bank{$name}{'policy_bank_name'})) { |
---|
5643 | $policy_bank{$name}{'policy_bank_name'} = $name; |
---|
5644 | $policy_bank{$name}{'policy_bank_path'} = $name; |
---|
5645 | } |
---|
5646 | } |
---|
5647 | }; |
---|
5648 | |
---|
5649 | # overlay the current policy bank by settings from the |
---|
5650 | # $policy_bank{$policy_bank_name}, or load the default policy bank (empty name) |
---|
5651 | sub load_policy_bank($) { |
---|
5652 | my($policy_bank_name) = @_; |
---|
5653 | if (!exists $policy_bank{$policy_bank_name}) { |
---|
5654 | do_log(-1,"policy bank \"$policy_bank_name\" does not exist, ignored"); |
---|
5655 | } elsif ($policy_bank_name eq '') { |
---|
5656 | %current_policy_bank = %{$policy_bank{$policy_bank_name}}; |
---|
5657 | do_log(4,'loaded base policy bank'); |
---|
5658 | } else { |
---|
5659 | my($cpbp) = c('policy_bank_path'); # currently loaded bank |
---|
5660 | for my $k (keys %{$policy_bank{$policy_bank_name}}) { |
---|
5661 | do_log(-1,"loading policy bank \"$policy_bank_name\": ". |
---|
5662 | "unknown field \"$k\"") if !exists $current_policy_bank{$k}; |
---|
5663 | $current_policy_bank{$k} = $policy_bank{$policy_bank_name}{$k}; |
---|
5664 | } |
---|
5665 | $current_policy_bank{'policy_bank_path'} = |
---|
5666 | ($cpbp eq '' ? '' : $cpbp.'/') . $policy_bank_name; |
---|
5667 | do_log(2,sprintf('loaded policy bank "%s"%s', $policy_bank_name, |
---|
5668 | $cpbp eq '' ? '' : " over \"$cpbp\"")); |
---|
5669 | } |
---|
5670 | } |
---|
5671 | |
---|
5672 | ### Net::Server hook |
---|
5673 | ### This hook occurs after chroot, change of user, and change of group has |
---|
5674 | ### occured. It allows for preparation before looping begins. |
---|
5675 | sub pre_loop_hook { |
---|
5676 | my($self) = @_; |
---|
5677 | local $SIG{CHLD} = 'DEFAULT'; |
---|
5678 | eval { |
---|
5679 | after_chroot_init(); # the rest of the top-level initialization |
---|
5680 | |
---|
5681 | # this needs to be done only after chroot, otherwise paths will be wrong |
---|
5682 | find_external_programs([split(/:/, $path, -1)]); |
---|
5683 | # do some sanity checking |
---|
5684 | my($name) = $TEMPBASE; |
---|
5685 | $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne ''; |
---|
5686 | my($errn) = stat($TEMPBASE) ? 0 : 0+$!; |
---|
5687 | if ($errn==ENOENT) { die "No TEMPBASE directory: $name" } |
---|
5688 | elsif ($errn) { die "TEMPBASE directory inaccessible, $!: $name" } |
---|
5689 | elsif (!-d _) { die "TEMPBASE is not a directory: $name" } |
---|
5690 | elsif (!-w _) { die "TEMPBASE directory is not writable: $name" } |
---|
5691 | if ($enable_global_cache && $extra_code_db) { |
---|
5692 | my($name) = $db_home; |
---|
5693 | $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne ''; |
---|
5694 | $errn = stat($db_home) ? 0 : 0+$!; |
---|
5695 | if ($errn == ENOENT) { |
---|
5696 | die "Please create an empty directory $name to hold a database". |
---|
5697 | " (config variable \$db_home)\n" } |
---|
5698 | elsif ($errn) { die "db_home inaccessible, $!: $name" } |
---|
5699 | elsif (!-d _) { die "db_home is not a directory : $name" } |
---|
5700 | elsif (!-w _) { die "db_home directory is not writable: $name" } |
---|
5701 | Amavis::DB::init(1); |
---|
5702 | } |
---|
5703 | if ($QUARANTINEDIR ne '') { |
---|
5704 | my($name) = $QUARANTINEDIR; |
---|
5705 | $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne ''; |
---|
5706 | $errn = stat($QUARANTINEDIR) ? 0 : 0+$!; |
---|
5707 | if ($errn == ENOENT) { } # ok |
---|
5708 | elsif ($errn) { die "QUARANTINEDIR inaccessible, $!: $name" } |
---|
5709 | elsif (-d _ && !-w _) { die "QUARANTINEDIR directory not writable: $name" } |
---|
5710 | } |
---|
5711 | Amavis::SpamControl::init() if $extra_code_antispam; |
---|
5712 | }; |
---|
5713 | if ($@ ne '') { |
---|
5714 | chomp($@); my($msg) = "TROUBLE in pre_loop_hook: $@"; do_log(-2,$msg); |
---|
5715 | die ("Suicide (" . am_id() . ") " . $msg . "\n"); # kills child, not parent |
---|
5716 | } |
---|
5717 | 1; |
---|
5718 | } |
---|
5719 | |
---|
5720 | ### log routine Net::Server hook |
---|
5721 | ### (Sys::Syslog MUST NOT be specified as a value of 'log_file'!) |
---|
5722 | # |
---|
5723 | # Redirect Net::Server logging to use Amavis' do_log(). |
---|
5724 | # The main reason is that Net::Server uses Sys::Syslog |
---|
5725 | # (and has two bugs in doing it, at least the Net-Server-0.82), |
---|
5726 | # and Amavis users are acustomed to Unix::Syslog. |
---|
5727 | sub write_to_log_hook { |
---|
5728 | my($self,$level,$msg) = @_; |
---|
5729 | my($prop) = $self->{server}; |
---|
5730 | local $SIG{CHLD} = 'DEFAULT'; |
---|
5731 | chomp($msg); |
---|
5732 | do_log(1, "Net::Server: " . $msg); # just call Amavis' traditional logging |
---|
5733 | 1; |
---|
5734 | } |
---|
5735 | |
---|
5736 | ### user customizable Net::Server hook |
---|
5737 | sub child_init_hook { |
---|
5738 | my($self) = @_; |
---|
5739 | local $SIG{CHLD} = 'DEFAULT'; |
---|
5740 | $0 = 'amavisd (virgin child)'; |
---|
5741 | eval { |
---|
5742 | $db_env = $snmp_db = $body_digest_cache = undef; # just in case |
---|
5743 | Amavis::Timing::init(); snmp_counters_init(); |
---|
5744 | if ($extra_code_db) { |
---|
5745 | $db_env = Amavis::DB->new; # get access to a bdb environment |
---|
5746 | $snmp_db = Amavis::DB::SNMP->new($db_env); |
---|
5747 | $snmp_db->register_proc('') if defined $snmp_db; # process alive & idle |
---|
5748 | } |
---|
5749 | # if $db_env is undef the Amavis::Cache::new creates a memory-based cache |
---|
5750 | $body_digest_cache = Amavis::Cache->new($db_env); |
---|
5751 | if ($extra_code_db) { # is it worth reporting the timing? |
---|
5752 | section_time('bdb-open'); |
---|
5753 | do_log(2, Amavis::Timing::report()); # report elapsed times |
---|
5754 | } |
---|
5755 | }; |
---|
5756 | if ($@ ne '') { |
---|
5757 | chomp($@); do_log(-2, "TROUBLE in child_init_hook: $@"); |
---|
5758 | die "Suicide in child_init_hook: $@\n"; |
---|
5759 | } |
---|
5760 | Amavis::Timing::go_idle('vir'); |
---|
5761 | } |
---|
5762 | |
---|
5763 | ### user customizable Net::Server hook |
---|
5764 | sub post_accept_hook { |
---|
5765 | my($self) = @_; |
---|
5766 | local $SIG{CHLD} = 'DEFAULT'; |
---|
5767 | $child_invocation_count++; |
---|
5768 | $0 = sprintf("amavisd (ch%d-accept)", $child_invocation_count); |
---|
5769 | Amavis::Timing::go_busy('hi '); |
---|
5770 | # establish initial time right after 'accept' |
---|
5771 | Amavis::Timing::init(); snmp_counters_init(); |
---|
5772 | $snmp_db->register_proc('A') if defined $snmp_db; # in 'accept' state |
---|
5773 | load_policy_bank(''); # start with a builting policy bank |
---|
5774 | } |
---|
5775 | |
---|
5776 | ### user customizable Net::Server hook |
---|
5777 | ### if this hook returns 1 the request is processed |
---|
5778 | ### if this hook returns 0 the request is denied |
---|
5779 | sub allow_deny_hook { |
---|
5780 | my($self) = @_; |
---|
5781 | local($1,$2,$3,$4); # Perl bug: $1 and $2 come tainted from Net::Server ! |
---|
5782 | local $SIG{CHLD} = 'DEFAULT'; |
---|
5783 | my($prop) = $self->{server}; my($sock) = $prop->{client}; my($bank_name); |
---|
5784 | my($is_ux) = UNIVERSAL::can($sock,'NS_proto') && $sock->NS_proto eq 'UNIX'; |
---|
5785 | if ($is_ux) { |
---|
5786 | $bank_name = $interface_policy{"SOCK"}; # possibly undef |
---|
5787 | } else { |
---|
5788 | my($myif,$myport) = ($prop->{sockaddr}, $prop->{sockport}); |
---|
5789 | if (defined $interface_policy{"$myif:$myport"}) { |
---|
5790 | $bank_name = $interface_policy{"$myif:$myport"}; |
---|
5791 | } elsif (defined $interface_policy{$myport}) { |
---|
5792 | $bank_name = $interface_policy{$myport}; |
---|
5793 | } |
---|
5794 | } |
---|
5795 | load_policy_bank($bank_name) if defined $bank_name && |
---|
5796 | $bank_name ne c('policy_bank_name'); |
---|
5797 | # note that the new policy bank may have replaced the inet_acl access table |
---|
5798 | if ($is_ux) { |
---|
5799 | # always permit access - unix sockets are immune to this check |
---|
5800 | } else { |
---|
5801 | my($permit,$fullkey) = lookup_ip_acl($prop->{peeraddr},ca('inet_acl')); |
---|
5802 | if (!$permit) { |
---|
5803 | my($msg) = sprintf("DENIED ACCESS from IP %s, policy bank '%s'", |
---|
5804 | $prop->{peeraddr}, c('policy_bank_name') ); |
---|
5805 | $msg .= ", blocked by rule $fullkey" if defined $fullkey; |
---|
5806 | do_log(-1,$msg); |
---|
5807 | return 0; |
---|
5808 | } |
---|
5809 | } |
---|
5810 | 1; |
---|
5811 | } |
---|
5812 | |
---|
5813 | # use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC); |
---|
5814 | # sub cloexec_on($;$) { |
---|
5815 | # my($fd,$name) = @_; my($flags); |
---|
5816 | # $flags = fcntl($fd, F_GETFD, 0) |
---|
5817 | # or die "Can't get flags from the file descriptor: $!"; |
---|
5818 | # if ($flags & FD_CLOEXEC == 0) { |
---|
5819 | # do_log(4,"Turning on FD_CLOEXEC flag on $name"); |
---|
5820 | # fcntl($fd, F_SETFD, $flags | FD_CLOEXEC) |
---|
5821 | # or die "Can't set FD_CLOEXEC on file descriptor $name: $!"; |
---|
5822 | # } |
---|
5823 | # } |
---|
5824 | |
---|
5825 | ### The heart of the program |
---|
5826 | ### user customizable Net::Server hook |
---|
5827 | sub process_request { |
---|
5828 | my($self) = shift; |
---|
5829 | my($prop) = $self->{server}; my($sock) = $prop->{client}; |
---|
5830 | local $SIG{CHLD} = 'DEFAULT'; |
---|
5831 | local($1,$2,$3,$4); # Perl bug: $1 and $2 come tainted from Net::Server ! |
---|
5832 | # Net::Server assigns STDIN and STDOUT to the socket |
---|
5833 | binmode(STDIN) or die "Can't set STDIN to binmode: $!"; |
---|
5834 | binmode(STDOUT) or die "Can't set STDOUT to binmode: $!"; |
---|
5835 | binmode($sock) or die "Can't set socket to binmode: $!"; |
---|
5836 | $| = 1; |
---|
5837 | local $SIG{ALRM} = sub { die "timed out\n" }; # do not modify the sig text! |
---|
5838 | eval { |
---|
5839 | # if ($] < 5.006) { # Perl older than 5.6.0 did not set FD_CLOEXEC on sockets |
---|
5840 | # for my $mysock (@{$prop->{sock}}) { cloexec_on($mysock, $mysock) } |
---|
5841 | # } |
---|
5842 | prolong_timer('new request - timer reset', $child_timeout); # timer init |
---|
5843 | if ($extra_code_ldap && !defined $ldap_policy) { |
---|
5844 | # make LDAP lookup object |
---|
5845 | $ldap_policy = Amavis::Lookup::LDAP->new($default_ldap); |
---|
5846 | } |
---|
5847 | if (defined $ldap_policy && !$implicit_maps_inserted) { |
---|
5848 | # make SQL field lookup objects with incorporated field names |
---|
5849 | # fieldtype: B=boolean, N=numeric, S=string, L=list |
---|
5850 | # B-, N-, S-, L- returns undef if field does not exist |
---|
5851 | # B0: boolean, nonexistent field treated as false, |
---|
5852 | # B1: boolean, nonexistent field treated as true |
---|
5853 | my $lf = sub{Amavis::Lookup::LDAPattr->new($ldap_policy,@_)}; |
---|
5854 | unshift(@Amavis::Conf::virus_lovers_maps, $lf->('amavisVirusLover', 'B-')); |
---|
5855 | unshift(@Amavis::Conf::spam_lovers_maps, $lf->('amavisSpamLover', 'B-')); |
---|
5856 | unshift(@Amavis::Conf::banned_files_lovers_maps, $lf->('amavisBannedFilesLover', 'B-')); |
---|
5857 | unshift(@Amavis::Conf::bad_header_lovers_maps, $lf->('amavisBadHeaderLover', 'B-')); |
---|
5858 | unshift(@Amavis::Conf::bypass_virus_checks_maps, $lf->('amavisBypassVirusChecks', 'B-')); |
---|
5859 | unshift(@Amavis::Conf::bypass_spam_checks_maps, $lf->('amavisBypassSpamChecks', 'B-')); |
---|
5860 | unshift(@Amavis::Conf::bypass_banned_checks_maps,$lf->('amavisBypassBannedChecks', 'B-')); |
---|
5861 | unshift(@Amavis::Conf::bypass_header_checks_maps,$lf->('amavisBypassHeaderChecks', 'B-')); |
---|
5862 | unshift(@Amavis::Conf::spam_tag_level_maps, $lf->('amavisSpamTagLevel', 'N')); |
---|
5863 | unshift(@Amavis::Conf::spam_tag2_level_maps, $lf->('amavisSpamTag2Level', 'N')); |
---|
5864 | unshift(@Amavis::Conf::spam_kill_level_maps, $lf->('amavisSpamKillLevel', 'N')); |
---|
5865 | unshift(@Amavis::Conf::spam_modifies_subj_maps, $lf->('amavisSpamModifiesSubj', 'B-')); |
---|
5866 | unshift(@Amavis::Conf::message_size_limit_maps, $lf->('amavisMessageSizeLimit', 'N-')); |
---|
5867 | unshift(@Amavis::Conf::virus_quarantine_to_maps, $lf->('amavisVirusQuarantineTo', 'S-')); |
---|
5868 | unshift(@Amavis::Conf::spam_quarantine_to_maps, $lf->('amavisSpamQuarantineTo', 'S-')); |
---|
5869 | unshift(@Amavis::Conf::banned_quarantine_to_maps, $lf->('amavisBannedQuarantineTo','S-')); |
---|
5870 | unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $lf->('amavisBadHeaderQuarantineTo', 'S-')); |
---|
5871 | unshift(@Amavis::Conf::local_domains_maps, $lf->('amavisLocal', |
---|
5872 | 'B1')); |
---|
5873 | unshift(@Amavis::Conf::warnvirusrecip_maps, $lf->('amavisWarnVirusRecip', 'B-')); |
---|
5874 | unshift(@Amavis::Conf::warnbannedrecip_maps, $lf->('amavisWarnBannedRecip', 'B-')); |
---|
5875 | unshift(@Amavis::Conf::warnbadhrecip_maps, $lf->('amavisWarnBadHeaderRecip', 'B-')); |
---|
5876 | section_time('ldap-prepare'); |
---|
5877 | } |
---|
5878 | if ($extra_code_sql && @lookup_sql_dsn) { |
---|
5879 | # make SQL lookup objects which will hold SELECT and DBI handles |
---|
5880 | $sql_wblist = Amavis::Lookup::SQL->new |
---|
5881 | if !defined $sql_wblist && defined $sql_select_white_black_list; |
---|
5882 | $sql_policy = Amavis::Lookup::SQL->new |
---|
5883 | if !defined $sql_policy && defined $sql_select_policy; |
---|
5884 | } |
---|
5885 | if (defined $sql_policy && !$implicit_maps_inserted) { |
---|
5886 | # make SQL field lookup objects with incorporated field names |
---|
5887 | # fieldtype: B=boolean, N=numeric, S=string, |
---|
5888 | # B-, N-, S- returns undef if field does not exist |
---|
5889 | # B0: boolean, nonexistent field treated as false, |
---|
5890 | # B1: boolean, nonexistent field treated as true |
---|
5891 | my $nf = sub{Amavis::Lookup::SQLfield->new($sql_policy,@_)}; #shorthand |
---|
5892 | $user_id_sql = $nf->('id', 'S'); |
---|
5893 | unshift(@Amavis::Conf::local_domains_maps, $nf->('local', 'B1')); |
---|
5894 | |
---|
5895 | unshift(@Amavis::Conf::virus_lovers_maps, $nf->('virus_lover', 'B0')); |
---|
5896 | unshift(@Amavis::Conf::spam_lovers_maps, $nf->('spam_lover', 'B-')); |
---|
5897 | unshift(@Amavis::Conf::banned_files_lovers_maps, $nf->('banned_files_lover', 'B-')); |
---|
5898 | unshift(@Amavis::Conf::bad_header_lovers_maps, $nf->('bad_header_lover', 'B-')); |
---|
5899 | |
---|
5900 | unshift(@Amavis::Conf::bypass_virus_checks_maps, $nf->('bypass_virus_checks', 'B0')); |
---|
5901 | unshift(@Amavis::Conf::bypass_spam_checks_maps, $nf->('bypass_spam_checks', 'B0')); |
---|
5902 | unshift(@Amavis::Conf::bypass_banned_checks_maps, $nf->('bypass_banned_checks', 'B-')); |
---|
5903 | unshift(@Amavis::Conf::bypass_header_checks_maps, $nf->('bypass_header_checks', 'B-')); |
---|
5904 | |
---|
5905 | unshift(@Amavis::Conf::spam_tag_level_maps, $nf->('spam_tag_level', 'N')); |
---|
5906 | unshift(@Amavis::Conf::spam_tag2_level_maps, $nf->('spam_tag2_level', 'N')); |
---|
5907 | unshift(@Amavis::Conf::spam_kill_level_maps, $nf->('spam_kill_level', 'N')); |
---|
5908 | unshift(@Amavis::Conf::spam_dsn_cutoff_level_maps,$nf->('spam_dsn_cutoff_level','N-')); |
---|
5909 | |
---|
5910 | unshift(@Amavis::Conf::spam_modifies_subj_maps, $nf->('spam_modifies_subj', 'B-')); |
---|
5911 | unshift(@Amavis::Conf::spam_subject_tag_maps, $nf->('spam_subject_tag', 'S-')); |
---|
5912 | unshift(@Amavis::Conf::spam_subject_tag2_maps, $nf->('spam_subject_tag2', 'S-')); |
---|
5913 | |
---|
5914 | unshift(@Amavis::Conf::virus_quarantine_to_maps, $nf->('virus_quarantine_to', 'S-')); |
---|
5915 | unshift(@Amavis::Conf::banned_quarantine_to_maps, $nf->('banned_quarantine_to', 'S-')); |
---|
5916 | unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $nf->('bad_header_quarantine_to','S-')); |
---|
5917 | unshift(@Amavis::Conf::spam_quarantine_to_maps, $nf->('spam_quarantine_to', 'S-')); |
---|
5918 | unshift(@Amavis::Conf::message_size_limit_maps, $nf->('message_size_limit', 'N-')); |
---|
5919 | |
---|
5920 | unshift(@Amavis::Conf::addr_extension_virus_maps, $nf->('addr_extension_virus', 'S-')); |
---|
5921 | unshift(@Amavis::Conf::addr_extension_spam_maps, $nf->('addr_extension_spam', 'S-')); |
---|
5922 | unshift(@Amavis::Conf::addr_extension_banned_maps,$nf->('addr_extension_banned','S-')); |
---|
5923 | unshift(@Amavis::Conf::addr_extension_bad_header_maps,$nf->('addr_extension_bad_header','S-')); |
---|
5924 | |
---|
5925 | unshift(@Amavis::Conf::warnvirusrecip_maps, $nf->('warnvirusrecip', 'B-')); |
---|
5926 | unshift(@Amavis::Conf::warnbannedrecip_maps, $nf->('warnbannedrecip', 'B-')); |
---|
5927 | unshift(@Amavis::Conf::warnbadhrecip_maps, $nf->('warnbadhrecip', 'B-')); |
---|
5928 | |
---|
5929 | unshift(@Amavis::Conf::newvirus_admin_maps, $nf->('newvirus_admin', 'S-')); |
---|
5930 | unshift(@Amavis::Conf::virus_admin_maps, $nf->('virus_admin', 'S-')); |
---|
5931 | unshift(@Amavis::Conf::banned_admin_maps, $nf->('banned_admin', 'S-')); |
---|
5932 | unshift(@Amavis::Conf::bad_header_admin_maps, $nf->('bad_header_admin', 'S-')); |
---|
5933 | unshift(@Amavis::Conf::spam_admin_maps, $nf->('spam_admin', 'S-')); |
---|
5934 | |
---|
5935 | section_time('sql-prepare'); |
---|
5936 | } |
---|
5937 | Amavis::Conf::label_default_maps() if !$implicit_maps_inserted; |
---|
5938 | $implicit_maps_inserted = 1; |
---|
5939 | |
---|
5940 | my($conn) = Amavis::In::Connection->new; |
---|
5941 | $CONN = $conn; # ugly - save in a global |
---|
5942 | $conn->proto($sock->NS_proto); |
---|
5943 | my($suggested_protocol) = c('protocol'); # suggested by the policy bank |
---|
5944 | do_log(5,"process_request: suggested_protocol=\"$suggested_protocol\" on ". |
---|
5945 | $sock->NS_proto); |
---|
5946 | if ($sock->NS_proto eq 'UNIX') { # traditional amavis helper program |
---|
5947 | if ($suggested_protocol eq 'COURIER') { |
---|
5948 | die "unavailable support for protocol: $suggested_protocol"; |
---|
5949 | } elsif ($suggested_protocol eq 'AM.PDP') { |
---|
5950 | $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj; |
---|
5951 | $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0); |
---|
5952 | } else { # default to old amavis helper program protocol |
---|
5953 | $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj; |
---|
5954 | $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 1); |
---|
5955 | } |
---|
5956 | } elsif ($sock->NS_proto eq 'TCP') { |
---|
5957 | $conn->socket_ip($prop->{sockaddr}); |
---|
5958 | $conn->socket_port($prop->{sockport}); |
---|
5959 | $conn->client_ip($prop->{peeraddr}); |
---|
5960 | if ($suggested_protocol eq 'TCP-LOOKUP') { # postfix maps (experimental) |
---|
5961 | process_tcp_lookup_request($sock, $conn); |
---|
5962 | do_log(2, Amavis::Timing::report()); # report elapsed times |
---|
5963 | } elsif ($suggested_protocol eq 'AM.PDP') { |
---|
5964 | # amavis policy delegation protocol (e.g. new milter helper program) |
---|
5965 | $amcl_in_obj = Amavis::In::AMCL->new if !$amcl_in_obj; |
---|
5966 | $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0); |
---|
5967 | } elsif ($suggested_protocol eq 'QMQPqq') { |
---|
5968 | if (!$extra_code_in_qmqpqq) { |
---|
5969 | die "incoming TCP connection, but dynamic QMQPqq code not loaded"; |
---|
5970 | } |
---|
5971 | $qmqpqq_in_obj = Amavis::In::QMQPqq->new if !$qmqpqq_in_obj; |
---|
5972 | $qmqpqq_in_obj->process_qmqpqq_request($sock,$conn,\&check_mail); |
---|
5973 | } else { # defaults to SMTP or LMTP |
---|
5974 | if (!$extra_code_in_smtp) { |
---|
5975 | die "incoming TCP connection, but dynamic SMTP/LMTP code not loaded"; |
---|
5976 | } |
---|
5977 | $smtp_in_obj = Amavis::In::SMTP->new if !$smtp_in_obj; |
---|
5978 | $smtp_in_obj->process_smtp_request( |
---|
5979 | $sock, ($suggested_protocol eq 'LMTP'?1:0), $conn, \&check_mail); |
---|
5980 | } |
---|
5981 | } else { |
---|
5982 | die ("unsupported protocol: $suggested_protocol, " . $sock->NS_proto); |
---|
5983 | } |
---|
5984 | }; # eval |
---|
5985 | alarm(0); # stop the timer |
---|
5986 | if ($@ ne '') { |
---|
5987 | chomp($@); my($timed_out) = $@ eq "timed out"; |
---|
5988 | my($msg) = $timed_out ? "Child task exceeded $child_timeout seconds, abort" |
---|
5989 | : "TROUBLE in process_request: $@"; |
---|
5990 | do_log(-2, $msg); |
---|
5991 | $smtp_in_obj->preserve_evidence(1) if $smtp_in_obj && !$timed_out; |
---|
5992 | # kills a child, hopefully preserving tempdir; does not kill parent |
---|
5993 | do_log(-1, "Requesting process rundown after fatal error"); |
---|
5994 | $self->done(1); |
---|
5995 | # die ("Suicide (" . am_id() . ") " . $msg . "\n"); |
---|
5996 | } |
---|
5997 | |
---|
5998 | my(@modules_extra) = grep {!exists $modules_basic{$_}} keys %INC; |
---|
5999 | # do_log(0, "modules loaded: ".join(", ", sort keys %modules_basic)); |
---|
6000 | do_log(1, "extra modules loaded: ". |
---|
6001 | join(", ", sort @modules_extra)) if @modules_extra; |
---|
6002 | |
---|
6003 | # if ($child_task_count >= $max_requests && |
---|
6004 | # $child_invocation_count < $max_requests) { |
---|
6005 | if ($child_task_count > $max_requests) { |
---|
6006 | # in case of multiple-transaction protocols (e.g. SMTP, LMTP) |
---|
6007 | # we do not like to keep running indefinitely at the MTA's mercy |
---|
6008 | do_log(1, "Requesting process rundown after $child_task_count tasks ". |
---|
6009 | "(and $child_invocation_count sessions)"); |
---|
6010 | $self->done(1); |
---|
6011 | } |
---|
6012 | } |
---|
6013 | |
---|
6014 | ### override Net::Server::PreForkSimple::done |
---|
6015 | ### to be able to rundown the child process prematurely |
---|
6016 | sub done(@) { |
---|
6017 | my($self) = shift; |
---|
6018 | if (@_) { $self->{server}->{done} = shift } |
---|
6019 | elsif (!$self->{server}->{done}) |
---|
6020 | { $self->{server}->{done} = $self->SUPER::done } |
---|
6021 | $self->{server}->{done}; |
---|
6022 | } |
---|
6023 | |
---|
6024 | ### Net::Server hook |
---|
6025 | sub post_process_request_hook { |
---|
6026 | my($self) = @_; |
---|
6027 | local $SIG{CHLD} = 'DEFAULT'; |
---|
6028 | debug_oneshot(0); |
---|
6029 | $0 = sprintf("amavisd (ch%d-avail)", $child_invocation_count); |
---|
6030 | alarm(0); do_log(5,"post_process_request_hook: timer stopped"); |
---|
6031 | $snmp_db->register_proc('') if defined $snmp_db; # process is alive and idle |
---|
6032 | Amavis::Timing::go_idle('bye'); Amavis::Timing::report_load(); |
---|
6033 | } |
---|
6034 | |
---|
6035 | ### Child is about to be terminated |
---|
6036 | ### user customizable Net::Server hook |
---|
6037 | sub child_finish_hook { |
---|
6038 | my($self) = @_; |
---|
6039 | local $SIG{CHLD} = 'DEFAULT'; |
---|
6040 | # for my $m (sort map { s/\.pm\z//; s[/][::]g; $_ } grep { /\.pm\z/ } keys %INC){ |
---|
6041 | # do_log(0, sprintf("Module %-19s %s", $m, $m->VERSION || '?')) |
---|
6042 | # if grep {$m=~/^$_/} qw(Mail::ClamAV Mail::SpamAssassin Razor2 Net::DNS); |
---|
6043 | # } |
---|
6044 | $0 = sprintf("amavisd (ch%d-finish)", $child_invocation_count); |
---|
6045 | do_log(5,"child_finish_hook: invoking DESTROY methods"); |
---|
6046 | $smtp_in_obj = undef; # calls Amavis::In::SMTP::DESTROY |
---|
6047 | $qmqpqq_in_obj = undef; # calls Amavis::In::QMQPqq::DESTROY |
---|
6048 | $amcl_in_obj = undef; # (currently does nothing for Amavis::In::AMCL) |
---|
6049 | $sql_wblist = undef; # calls Amavis::Lookup::SQL::DESTROY |
---|
6050 | $sql_policy = undef; # calls Amavis::Lookup::SQL::DESTROY |
---|
6051 | $ldap_policy = undef; # calls Amavis::Lookup::LDAP::DESTROY |
---|
6052 | $body_digest_cache = undef; # calls Amavis::Cache::DESTROY |
---|
6053 | eval { $snmp_db->register_proc(undef) } if defined $snmp_db; # going away |
---|
6054 | $snmp_db = undef; # calls Amavis::DB::SNMP::DESTROY |
---|
6055 | $db_env = undef; |
---|
6056 | } |
---|
6057 | |
---|
6058 | sub END { # runs before exiting the module |
---|
6059 | do_log(5,"at the END handler: invoking DESTROY methods"); |
---|
6060 | $smtp_in_obj = undef; # at end calls Amavis::In::SMTP::DESTROY |
---|
6061 | $qmqpqq_in_obj = undef; # at end calls Amavis::In::QMQPqq::DESTROY |
---|
6062 | $amcl_in_obj = undef; # (currently does nothing for Amavis::In::AMCL) |
---|
6063 | $sql_wblist = undef; # at end calls Amavis::Lookup::SQL::DESTROY |
---|
6064 | $sql_policy = undef; # at end calls Amavis::Lookup::SQL::DESTROY |
---|
6065 | $ldap_policy = undef; # at end calls Amavis::Lookup::LDAP::DESTROY |
---|
6066 | $body_digest_cache = undef; # at end calls Amavis::Cache::DESTROY |
---|
6067 | eval { $snmp_db->register_proc(undef) } if defined $snmp_db; # going away |
---|
6068 | $snmp_db = undef; # at end calls Amavis::DB::SNMP::DESTROY |
---|
6069 | $db_env = undef; |
---|
6070 | } |
---|
6071 | |
---|
6072 | # implements Postfix TCP lookup server, see tcp_table(5) man page; experimental |
---|
6073 | sub process_tcp_lookup_request($$) { |
---|
6074 | my($sock, $conn) = @_; |
---|
6075 | local($/) = "\012"; # set line terminator to LF (regardless of platform) |
---|
6076 | my($req_cnt); |
---|
6077 | while (<$sock>) { |
---|
6078 | $req_cnt++; my($level) = 0; |
---|
6079 | my($resp_code, $resp_msg) = (400, 'INTERNAL ERROR'); |
---|
6080 | if (/^get (.*?)\015?\012\z/si) { |
---|
6081 | my($key) = tcp_lookup_decode($1); |
---|
6082 | my($sl); $sl = lookup(0,$key, @{ca('spam_lovers_maps')}); |
---|
6083 | $resp_code = 200; $level = 2; |
---|
6084 | $resp_msg = $sl ? "OK Recipient <$key> IS spam lover" |
---|
6085 | : "DUNNO Recipient <$key> is NOT spam lover"; |
---|
6086 | } elsif (/^put ([^ ]*) (.*?)\015?\012\z/si) { |
---|
6087 | $resp_code = 500; $resp_msg = 'request not implemented: ' . $_; |
---|
6088 | } else { |
---|
6089 | $resp_code = 500; $resp_msg = 'illegal request: ' . $_; |
---|
6090 | } |
---|
6091 | do_log($level, "tcp_lookup($req_cnt): $resp_code $resp_msg"); |
---|
6092 | $sock->printf("%03d %s\012", $resp_code, tcp_lookup_encode($resp_msg)) |
---|
6093 | or die "Can't write to tcp_lookup socket: $!"; |
---|
6094 | } |
---|
6095 | do_log(0, "tcp_lookup: RUNDOWN after $req_cnt requests"); |
---|
6096 | } |
---|
6097 | |
---|
6098 | sub tcp_lookup_encode($) { |
---|
6099 | my($str) = @_; |
---|
6100 | $str =~ s/[^\041-\044\046-\176]/sprintf("%%%02x",ord($&))/eg; |
---|
6101 | $str; |
---|
6102 | } |
---|
6103 | |
---|
6104 | sub tcp_lookup_decode($) { |
---|
6105 | my($str) = @_; |
---|
6106 | $str =~ s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg; |
---|
6107 | $str; |
---|
6108 | } |
---|
6109 | |
---|
6110 | # Checks the message stored on a file. File must already |
---|
6111 | # be open on file handle $msginfo->mail_text; it need not be positioned |
---|
6112 | # properly, check_mail must not close the file handle. |
---|
6113 | # |
---|
6114 | sub check_mail($$$$) { |
---|
6115 | my($conn, $msginfo, $dsn_per_recip_capable, $tempdir) = @_; |
---|
6116 | |
---|
6117 | my($am_id) = am_id(); |
---|
6118 | $snmp_db->register_proc($am_id) if defined $snmp_db; |
---|
6119 | my($fh) = $msginfo->mail_text; my(@recips) = @{$msginfo->recips}; |
---|
6120 | |
---|
6121 | $MSGINFO = $msginfo; # ugly - save in a global, to make it accessible |
---|
6122 | # to %builtins |
---|
6123 | # check_mail() may be called several times per child lifetime and/or |
---|
6124 | # per-SMTP session. The variable $child_task_count is mainly used |
---|
6125 | # by AV-scanner interfaces, e.g. to initialize when invoked |
---|
6126 | # for the first time during child process lifetime. |
---|
6127 | $child_task_count++; |
---|
6128 | |
---|
6129 | # reset certain global variables for each task |
---|
6130 | $VIRUSFILE = undef; $av_output = undef; @detecting_scanners = (); |
---|
6131 | @virusname = (); @banned_filename = (); @bad_headers = (); |
---|
6132 | $spam_level = undef; $spam_status = undef; $spam_report = undef; |
---|
6133 | |
---|
6134 | # comment out to retain SQL cache entries for the whole child lifetime: |
---|
6135 | $sql_policy->clear_cache if defined $sql_policy; |
---|
6136 | $sql_wblist->clear_cache if defined $sql_wblist; |
---|
6137 | |
---|
6138 | # comment out to retain LDAP cache entries for the whole child lifetime: |
---|
6139 | $ldap_policy->clear_cache if defined $ldap_policy; |
---|
6140 | |
---|
6141 | # also measures mail size and saves mail header |
---|
6142 | $body_digest = get_body_digest($fh, $msginfo); |
---|
6143 | |
---|
6144 | my($mail_size) = $msginfo->msg_size; # use ESMTP size if available |
---|
6145 | $mail_size = $msginfo->orig_header_size + 1 + $msginfo->orig_body_size |
---|
6146 | if $mail_size <= 0; |
---|
6147 | # $mail_size = -s $msginfo->mail_text_fn; # get it from a file system |
---|
6148 | |
---|
6149 | my($file_generator_object) = # maxfiles 0 disables the $MAXFILES limit |
---|
6150 | Amavis::Unpackers::NewFilename->new($MAXFILES?$MAXFILES:undef, $mail_size); |
---|
6151 | Amavis::Unpackers::Part::init($file_generator_object); # fudge: keep in variable |
---|
6152 | my($parts_root) = Amavis::Unpackers::Part->new; |
---|
6153 | $msginfo->parts_root($parts_root); |
---|
6154 | my($smtp_resp, $exit_code, $preserve_evidence); my($virus_dejavu) = 0; |
---|
6155 | my($banned_filename_checked,$virus_presence_checked,$spam_presence_checked); |
---|
6156 | |
---|
6157 | # matching banned rules suggest DSN to be suppressed? |
---|
6158 | my($banned_dsn_suppress) = 0; |
---|
6159 | |
---|
6160 | # is any mail component password protected or otherwise non-decodable? |
---|
6161 | my($any_undecipherable) = 0; |
---|
6162 | |
---|
6163 | my($cl_ip) = $msginfo->client_addr; my($pbn) = c('policy_bank_path'); |
---|
6164 | do_log(1,sprintf("Checking: %s%s%s -> %s", |
---|
6165 | $pbn eq '' ? '' : "$pbn ", |
---|
6166 | $cl_ip eq '' ? '' : "[$cl_ip] ", |
---|
6167 | qquote_rfc2821_local($msginfo->sender), |
---|
6168 | join(',', qquote_rfc2821_local(@recips)) )); |
---|
6169 | |
---|
6170 | my($mime_err); # undef, or MIME parsing error string as given by MIME::Parser |
---|
6171 | my($hold); # set to some string to cause the message to be |
---|
6172 | # placed on hold (frozen) by MTA. This can be used |
---|
6173 | # in cases when we stumble across some permanent problem |
---|
6174 | # making us unable to decide if the message is to be |
---|
6175 | # really delivered. |
---|
6176 | my($which_section); |
---|
6177 | eval { |
---|
6178 | snmp_count('InMsgs'); |
---|
6179 | snmp_count('InMsgsNullRPath') if $msginfo->sender eq ''; |
---|
6180 | if (@recips == 1) { snmp_count( 'InMsgsRecips' ) } |
---|
6181 | elsif (@recips > 1) { snmp_count( ['InMsgsRecips',scalar(@recips)] ) } |
---|
6182 | |
---|
6183 | $which_section = "creating_partsdir"; |
---|
6184 | if (-d "$tempdir/parts") { |
---|
6185 | # mkdir is a costly operation (must be atomic, flushes buffers). |
---|
6186 | # If we can re-use directory 'parts' from the previous invocation |
---|
6187 | # it saves us precious time. Together with matching rmdir this can |
---|
6188 | # amount to 10-15 % of total elapsed time !!! (no spam checking) |
---|
6189 | } else { |
---|
6190 | mkdir("$tempdir/parts", 0750) |
---|
6191 | or die "Can't create directory $tempdir/parts: $!"; |
---|
6192 | section_time('mkdir parts'); |
---|
6193 | } |
---|
6194 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
6195 | |
---|
6196 | # FIRST: what kind of e-mail did we get? call content scanners |
---|
6197 | |
---|
6198 | # already in cache? |
---|
6199 | $which_section = "cached"; |
---|
6200 | snmp_count('CacheAttempts'); |
---|
6201 | my($cache_entry); my($now) = time; |
---|
6202 | my($cache_entry_ttl) = |
---|
6203 | max($virus_check_negative_ttl, $virus_check_positive_ttl, |
---|
6204 | $spam_check_negative_ttl, $spam_check_positive_ttl); |
---|
6205 | my($now_utc_iso8601) = iso8601_utc_timestamp($now,1); |
---|
6206 | my($expires_utc_iso8601) = iso8601_utc_timestamp($now+$cache_entry_ttl,1); |
---|
6207 | $cache_entry = $body_digest_cache->get($body_digest) |
---|
6208 | if $body_digest_cache && defined $body_digest; |
---|
6209 | if (!defined $cache_entry) { |
---|
6210 | snmp_count('CacheMisses'); |
---|
6211 | $cache_entry->{'ctime'} = $now_utc_iso8601; # create a new cache record |
---|
6212 | } else { |
---|
6213 | snmp_count('CacheHits'); |
---|
6214 | $banned_filename_checked = defined $cache_entry->{'FB'} ? 1 : 0; |
---|
6215 | $virus_presence_checked = defined $cache_entry->{'VN'} ? 1 : 0; |
---|
6216 | |
---|
6217 | # spam level and spam report may be influenced by mail header, not only |
---|
6218 | # by mail body, so caching based on body is only a close approximation; |
---|
6219 | # ignore spam cache if body is too small |
---|
6220 | $spam_presence_checked = defined $cache_entry->{'SL'} ? 1 : 0; |
---|
6221 | if ($msginfo->orig_body_size < 200) { $spam_presence_checked = 0 } |
---|
6222 | |
---|
6223 | if ($virus_presence_checked && defined $cache_entry->{'Vt'}) { |
---|
6224 | # check for expiration of cached virus test results |
---|
6225 | my($ttl) = !@{$cache_entry->{'VN'}} ? $virus_check_negative_ttl |
---|
6226 | : $virus_check_positive_ttl; |
---|
6227 | if ($now > $cache_entry->{'Vt'} + $ttl) { |
---|
6228 | do_log(2,"Cached virus check expired, TTL = $ttl s"); |
---|
6229 | $virus_presence_checked = 0; |
---|
6230 | } |
---|
6231 | } |
---|
6232 | if ($spam_presence_checked && defined $cache_entry->{'St'}) { |
---|
6233 | # check for expiration of cached spam test results |
---|
6234 | # (note: hard-wired spam level 6) |
---|
6235 | my($ttl) = $cache_entry->{'SL'} < 6 ? $spam_check_negative_ttl |
---|
6236 | : $spam_check_positive_ttl; |
---|
6237 | if ($now > $cache_entry->{'St'} + $ttl) { |
---|
6238 | do_log(2,"Cached spam check expired, TTL = $ttl s"); |
---|
6239 | $spam_presence_checked = 0; |
---|
6240 | } |
---|
6241 | } |
---|
6242 | if ($virus_presence_checked) { |
---|
6243 | $av_output = $cache_entry->{'VO'}; |
---|
6244 | @virusname = @{$cache_entry->{'VN'}}; |
---|
6245 | @detecting_scanners = @{$cache_entry->{'VD'}}; |
---|
6246 | $virus_dejavu = 1; |
---|
6247 | } |
---|
6248 | if ($banned_filename_checked) { |
---|
6249 | @banned_filename = @{$cache_entry->{'FB'}}; |
---|
6250 | $banned_dsn_suppress = $cache_entry->{'FS'}; |
---|
6251 | } |
---|
6252 | ($spam_level, $spam_status, $spam_report) = @$cache_entry{'SL','SS','SR'} |
---|
6253 | if $spam_presence_checked; |
---|
6254 | do_log(1,sprintf("cached %s from <%s> (%s,%s,%s)", |
---|
6255 | $body_digest, $msginfo->sender, |
---|
6256 | $banned_filename_checked, $virus_presence_checked, |
---|
6257 | $spam_presence_checked)); |
---|
6258 | snmp_count('CacheHitsVirusCheck') if $virus_presence_checked; |
---|
6259 | snmp_count('CacheHitsVirusMsgs') if @virusname; |
---|
6260 | snmp_count('CacheHitsSpamCheck') if $spam_presence_checked; |
---|
6261 | snmp_count('CacheHitsSpamMsgs') if $spam_level >= 6; # a hack |
---|
6262 | # snmp_count('CacheHitsBannedCheck') if $banned_filename_checked; |
---|
6263 | # snmp_count('CacheHitsBannedMsgs') if @banned_filename; |
---|
6264 | do_log(5,sprintf("cache entry age: %s c=%s a=%s", |
---|
6265 | (@virusname ? 'V' : $spam_level > 5 ? 'S' : '.'), |
---|
6266 | $cache_entry->{'ctime'}, $cache_entry->{'atime'} )); |
---|
6267 | } # if defined $cache_entry |
---|
6268 | |
---|
6269 | my($will_do_virus_scanning) = # virus scanning will be needed? |
---|
6270 | !$virus_presence_checked && $extra_code_antivirus && |
---|
6271 | grep {!lookup(0,$_, @{ca('bypass_virus_checks_maps')})} @recips; |
---|
6272 | |
---|
6273 | my($will_do_banned_checking) = # banned name checking will be needed? |
---|
6274 | !$banned_filename_checked && |
---|
6275 | (@{ca('banned_filename_maps')} || cr('banned_namepath_re')) && |
---|
6276 | grep {!lookup(0,$_, @{ca('bypass_banned_checks_maps')})} @recips; |
---|
6277 | |
---|
6278 | # will do decoding parts as deeply as possible? only if needed |
---|
6279 | my($will_do_parts_decoding) = |
---|
6280 | !c('bypass_decode_parts') && |
---|
6281 | ($will_do_virus_scanning || $will_do_banned_checking); |
---|
6282 | |
---|
6283 | $which_section = "mime_decode-1"; |
---|
6284 | my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root); |
---|
6285 | $msginfo->mime_entity($ent); |
---|
6286 | prolong_timer($which_section); |
---|
6287 | |
---|
6288 | if ($will_do_parts_decoding) { |
---|
6289 | # decoding parts can take a lot of time! |
---|
6290 | snmp_count('OpsDec'); |
---|
6291 | ($hold,$any_undecipherable) = |
---|
6292 | Amavis::Unpackers::decompose_mail($tempdir,$file_generator_object); |
---|
6293 | } |
---|
6294 | if (grep {!lookup(0,$_,@{ca('bypass_header_checks_maps')})} @recips) { |
---|
6295 | push(@bad_headers, "MIME error: ".$mime_err) if $mime_err ne ''; |
---|
6296 | push(@bad_headers, check_header_validity($conn,$msginfo)); |
---|
6297 | } |
---|
6298 | if ($will_do_banned_checking) { # check for banned file contents |
---|
6299 | $which_section = "check-banned"; |
---|
6300 | my($banned_part_descr_ref, $banned_matching_keys_ref, $banned_rhs_ref) = |
---|
6301 | check_for_banned_names($parts_root); |
---|
6302 | for my $j (0..$#{$banned_part_descr_ref}) { |
---|
6303 | if ($banned_rhs_ref->[$j] =~ /^DISCARD/) { |
---|
6304 | $banned_dsn_suppress = 1; |
---|
6305 | do_log(4,sprintf('BANNED:%s: %s', $banned_rhs_ref->[$j], |
---|
6306 | $banned_part_descr_ref->[$j])); |
---|
6307 | } |
---|
6308 | } |
---|
6309 | push(@banned_filename, @$banned_part_descr_ref); |
---|
6310 | } |
---|
6311 | $cache_entry->{'FB'} = \@banned_filename; |
---|
6312 | $cache_entry->{'FS'} = $banned_dsn_suppress; |
---|
6313 | |
---|
6314 | if ($virus_presence_checked) { |
---|
6315 | do_log(5, "virus_presence cached, skipping virus_scan"); |
---|
6316 | } elsif (!$extra_code_antivirus) { |
---|
6317 | do_log(5, "no anti-virus code loaded, skipping virus_scan"); |
---|
6318 | } elsif (!grep {!lookup(0,$_,@{ca('bypass_virus_checks_maps')})} @recips) { |
---|
6319 | do_log(5, "bypassing of virus checks requested"); |
---|
6320 | } elsif ($hold ne '') { # protect virus scanner from mail bombs |
---|
6321 | do_log(0, "NOTICE: Virus scanning skipped: $hold"); |
---|
6322 | $will_do_virus_scanning = 0; |
---|
6323 | } else { |
---|
6324 | if (!$will_do_virus_scanning) |
---|
6325 | { do_log(-1, "NOTICE: will_do_virus_scanning is false???") } |
---|
6326 | if (!defined($msginfo->mime_entity)) { |
---|
6327 | $which_section = "mime_decode-3"; |
---|
6328 | my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root); |
---|
6329 | $msginfo->mime_entity($ent); |
---|
6330 | prolong_timer($which_section); |
---|
6331 | } |
---|
6332 | # special case to make available a complete mail file for inspection |
---|
6333 | if ($mime_err ne '' || |
---|
6334 | lookup(0,'MAIL',@keep_decoded_original_maps) || |
---|
6335 | $any_undecipherable && lookup(0,'MAIL-UNDECIPHERABLE', |
---|
6336 | @keep_decoded_original_maps)) { |
---|
6337 | # keep the original email.txt by making a hard link to it in ./parts/ |
---|
6338 | $which_section = "linking-to-MAIL"; |
---|
6339 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts", |
---|
6340 | $parts_root); |
---|
6341 | my($newpart) = $newpart_obj->full_name; |
---|
6342 | do_log(2, "providing full original message to scanners as $newpart". |
---|
6343 | (!$any_undecipherable ?'' :", $any_undecipherable undecipherable"). |
---|
6344 | ($mime_err eq '' ? '' : ", MIME error: $mime_err") ); |
---|
6345 | link($msginfo->mail_text_fn, $newpart) |
---|
6346 | or die sprintf("Can't create hard link %s to %s: %s", |
---|
6347 | $newpart, $msginfo->mail_text_fn, $!); |
---|
6348 | $newpart_obj->type_short('MAIL'); |
---|
6349 | $newpart_obj->type_declared('message/rfc822'); |
---|
6350 | } |
---|
6351 | $which_section = "virus_scan"; |
---|
6352 | # some virus scanners behave badly if interrupted, |
---|
6353 | # so for now just turn off the timer |
---|
6354 | my($remaining_time) = alarm(0); # check time left, stop timer |
---|
6355 | my($av_ret); |
---|
6356 | eval { |
---|
6357 | my($vn, $ds); |
---|
6358 | ($av_ret, $av_output, $vn, $ds) = |
---|
6359 | Amavis::AV::virus_scan($tempdir, $child_task_count==1, $parts_root); |
---|
6360 | @virusname = @$vn; @detecting_scanners = @$ds; # copy |
---|
6361 | }; |
---|
6362 | prolong_timer($which_section, $remaining_time); # restart timer |
---|
6363 | if ($@ ne '') { |
---|
6364 | chomp($@); |
---|
6365 | if ($@ eq "timed out") { |
---|
6366 | @virusname = (); $av_ret = 0; # assume not a virus! |
---|
6367 | do_log(-1, "virus_scan TIMED OUT, ASSUME NOT A VIRUS !!!"); |
---|
6368 | } else { |
---|
6369 | $hold = "virus_scan: $@"; # request HOLD |
---|
6370 | $av_ret = 0; # pretend it was ok (msg should be held) |
---|
6371 | die "$hold\n"; # die, TEMPFAIL is preferred to HOLD |
---|
6372 | } |
---|
6373 | } |
---|
6374 | snmp_count('OpsVirusCheck'); |
---|
6375 | defined($av_ret) or die "All virus scanners failed!"; |
---|
6376 | @$cache_entry{'Vt','VO','VN','VD'} = |
---|
6377 | ($now, $av_output, \@virusname, \@detecting_scanners); |
---|
6378 | $virus_presence_checked = 1; |
---|
6379 | if (defined $snmp_db && @virusname) { |
---|
6380 | $which_section = "read_counters"; |
---|
6381 | $virus_dejavu = 1 if !grep {$_==0} # none with counter zero or undef |
---|
6382 | @{$snmp_db->read_counters(map {"virus.byname.$_"} @virusname)}; |
---|
6383 | section_time($which_section); |
---|
6384 | } |
---|
6385 | } |
---|
6386 | |
---|
6387 | my($sender_contact,$sender_source); |
---|
6388 | if (!@virusname) { $sender_contact = $sender_source = $msginfo->sender } |
---|
6389 | else { |
---|
6390 | ($sender_contact,$sender_source) = best_try_originator( |
---|
6391 | $msginfo->sender, $msginfo->mime_entity, \@virusname); |
---|
6392 | section_time('best_try_originator'); |
---|
6393 | } |
---|
6394 | $msginfo->sender_contact($sender_contact); # save it |
---|
6395 | $msginfo->sender_source($sender_source); # save it |
---|
6396 | |
---|
6397 | # consider doing spam scanning |
---|
6398 | if (!$extra_code_antispam) { |
---|
6399 | do_log(5, "no anti-spam code loaded, skipping spam_scan"); |
---|
6400 | } elsif (@virusname || @banned_filename) { |
---|
6401 | do_log(5, "infected or banned contents, skipping spam_scan"); |
---|
6402 | } elsif (!grep {!lookup(0,$_,@{ca('bypass_spam_checks_maps')})} @recips) { |
---|
6403 | do_log(5, "bypassing of spam checks requested"); |
---|
6404 | } else { |
---|
6405 | $which_section = "spam-wb-list"; |
---|
6406 | my($any_wbl, $all_wbl) = Amavis::SpamControl::white_black_list( |
---|
6407 | $conn, $msginfo, $sql_wblist, $user_id_sql, $ldap_policy); |
---|
6408 | section_time($which_section); |
---|
6409 | if ($all_wbl) { |
---|
6410 | do_log(5, "sender white/blacklisted, skipping spam_scan"); |
---|
6411 | } elsif ($spam_presence_checked) { |
---|
6412 | do_log(5, "spam_presence cached, skipping spam_scan"); |
---|
6413 | } else { |
---|
6414 | $which_section = "spam_scan"; |
---|
6415 | ($spam_level, $spam_status, $spam_report) = |
---|
6416 | Amavis::SpamControl::spam_scan($conn, $msginfo); |
---|
6417 | prolong_timer($which_section); |
---|
6418 | snmp_count('OpsSpamCheck'); |
---|
6419 | @$cache_entry{'St','SL','SS','SR'} = |
---|
6420 | ($now, $spam_level, $spam_status, $spam_report); |
---|
6421 | $spam_presence_checked = 1; |
---|
6422 | } |
---|
6423 | } |
---|
6424 | |
---|
6425 | # store to cache |
---|
6426 | $cache_entry->{'atime'} = $now_utc_iso8601; # update accessed timestamp |
---|
6427 | $body_digest_cache->set($body_digest,$cache_entry, |
---|
6428 | $now_utc_iso8601,$expires_utc_iso8601) |
---|
6429 | if $body_digest_cache && defined $body_digest; |
---|
6430 | $cache_entry = undef; # discard the object, it is no longer needed |
---|
6431 | section_time('update_cache'); |
---|
6432 | |
---|
6433 | snmp_count("virus.byname.$_") for @virusname; |
---|
6434 | |
---|
6435 | # SECOND: now that we know what we got, decide what to do with it |
---|
6436 | |
---|
6437 | my($considered_spam_by_some_recips,$considered_oversize_by_some_recips); |
---|
6438 | |
---|
6439 | if (@virusname || @banned_filename) { # virus or banned filename found |
---|
6440 | # bad_headers do not enter this section, although code is ready for them; |
---|
6441 | # we'll handle bad headers later, if mail turns out not to be spam |
---|
6442 | $which_section = "deal_with_virus_or_banned"; |
---|
6443 | my($final_destiny) = @virusname ? c('final_virus_destiny') |
---|
6444 | : @banned_filename ? c('final_banned_destiny') |
---|
6445 | : @bad_headers ? c('final_bad_header_destiny') |
---|
6446 | : D_PASS; |
---|
6447 | for my $r (@{$msginfo->per_recip_data}) { |
---|
6448 | next if $r->recip_done; # already dealt with |
---|
6449 | if ($final_destiny == D_PASS) { |
---|
6450 | # recipient wants this message, malicious or not |
---|
6451 | } elsif ((!@virusname || # not a virus or we want it |
---|
6452 | lookup(0,$r->recip_addr, @{ca('virus_lovers_maps')})) && |
---|
6453 | (!@banned_filename || # not banned or we want it |
---|
6454 | lookup(0,$r->recip_addr, @{ca('banned_files_lovers_maps')})) && |
---|
6455 | (!@bad_headers || # not bad header or we want it |
---|
6456 | lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')})) ) |
---|
6457 | { |
---|
6458 | # clean, or recipient wants it |
---|
6459 | } else { # change mail destiny for those not wanting malware |
---|
6460 | $r->recip_destiny($final_destiny); |
---|
6461 | my($reason); |
---|
6462 | if (@virusname) |
---|
6463 | { $reason = "VIRUS: " . join(", ", @virusname) } |
---|
6464 | elsif (@banned_filename) |
---|
6465 | { $reason = "BANNED: " . join(", ", @banned_filename) } |
---|
6466 | elsif (@bad_headers) |
---|
6467 | { $reason = "BAD_HEADER: " . join(", ", @bad_headers) } |
---|
6468 | $reason = substr($reason,0,100)."..." if length($reason) > 100+3; |
---|
6469 | $r->recip_smtp_response( ($final_destiny == D_DISCARD |
---|
6470 | ? "250 2.7.1 Ok, discarded" |
---|
6471 | : "550 5.7.1 Message content rejected") . |
---|
6472 | ", id=$am_id - $reason"); |
---|
6473 | $r->recip_done(1); |
---|
6474 | } |
---|
6475 | } |
---|
6476 | $which_section = "virus_or_banned quar+notif"; |
---|
6477 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
6478 | # send notifications, quarantine it |
---|
6479 | do_virus($conn, $msginfo, $virus_dejavu); |
---|
6480 | |
---|
6481 | } else { # perhaps some recips consider it spam? |
---|
6482 | # spaminess is an individual matter, we must compare spam level |
---|
6483 | # with each recipient setting, there is no global criterium |
---|
6484 | # that the mail is spam |
---|
6485 | $which_section = "deal_with_spam"; |
---|
6486 | my($final_destiny) = c('final_spam_destiny'); |
---|
6487 | for my $r (@{$msginfo->per_recip_data}) { |
---|
6488 | next if $r->recip_done; # already dealt with |
---|
6489 | my($kill_level); |
---|
6490 | $kill_level = lookup(0,$r->recip_addr, @{ca('spam_kill_level_maps')}); |
---|
6491 | my($boost) = $r->recip_score_boost; |
---|
6492 | my($should_be_killed) = |
---|
6493 | !$r->recip_whitelisted_sender && |
---|
6494 | ($r->recip_blacklisted_sender || |
---|
6495 | (defined $spam_level && defined $kill_level ? |
---|
6496 | $spam_level+$boost >= $kill_level : 0) ); |
---|
6497 | next unless $should_be_killed; |
---|
6498 | # message is at or above kill level, or sender is blacklisted |
---|
6499 | $considered_spam_by_some_recips = 1; |
---|
6500 | if ($final_destiny == D_PASS || |
---|
6501 | lookup(0,$r->recip_addr, @{ca('spam_lovers_maps')})) { |
---|
6502 | # do nothing, recipient wants this message, even if spam |
---|
6503 | } else { # change mail destiny for those not wanting spam |
---|
6504 | ll(3) && do_log(3,sprintf( |
---|
6505 | "SPAM-KILL, %s -> %s, hits=%s, kill=%s%s", |
---|
6506 | qquote_rfc2821_local($msginfo->sender, $r->recip_addr), |
---|
6507 | (!defined $spam_level ? 'x' |
---|
6508 | : !defined $boost ? $spam_level |
---|
6509 | : $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost), |
---|
6510 | !defined $kill_level ? 'x' : 0+sprintf("%.3f",$kill_level), |
---|
6511 | $r->recip_blacklisted_sender ? ', BLACKLISTED' : '')); |
---|
6512 | $r->recip_destiny($final_destiny); |
---|
6513 | my($reason) = |
---|
6514 | $r->recip_blacklisted_sender ? 'sender blacklisted' : 'UBE'; |
---|
6515 | $r->recip_smtp_response(($final_destiny == D_DISCARD |
---|
6516 | ? "250 2.7.1 Ok, discarded, $reason" |
---|
6517 | : "550 5.7.1 Message content rejected, $reason" |
---|
6518 | ) . ", id=$am_id"); |
---|
6519 | $r->recip_done(1); |
---|
6520 | } |
---|
6521 | } |
---|
6522 | if ($considered_spam_by_some_recips) { |
---|
6523 | $which_section = "spam quar+notif"; |
---|
6524 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
6525 | do_spam($conn, $msginfo); |
---|
6526 | section_time('post-do_spam'); |
---|
6527 | } |
---|
6528 | } |
---|
6529 | |
---|
6530 | if (@bad_headers) { # invalid mail headers |
---|
6531 | $which_section = "deal_with_bad_headers"; |
---|
6532 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
6533 | my($is_bulk) = $msginfo->mime_entity->head->get('precedence', 0); |
---|
6534 | chomp($is_bulk); |
---|
6535 | do_log(1,sprintf("BAD HEADER from %s<%s>: %s", |
---|
6536 | $is_bulk eq '' ? '' : "($is_bulk) ", $msginfo->sender, |
---|
6537 | $bad_headers[0])); |
---|
6538 | $is_bulk = $is_bulk=~/^(bulk|list|junk)/i ? $1 : undef; |
---|
6539 | if (defined $is_bulk || $msginfo->sender eq '') { |
---|
6540 | # have mercy on mailing lists and DSN |
---|
6541 | } else { |
---|
6542 | my($any_badh); my($final_destiny) = c('final_bad_header_destiny'); |
---|
6543 | for my $r (@{$msginfo->per_recip_data}) { |
---|
6544 | next if $r->recip_done; # already dealt with |
---|
6545 | if ($final_destiny == D_PASS || |
---|
6546 | lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')})) |
---|
6547 | { |
---|
6548 | # recipient wants this message, broken or not |
---|
6549 | } else { # change mail destiny for those not wanting it |
---|
6550 | $r->recip_destiny($final_destiny); |
---|
6551 | my($reason) = (split(/\n/, $bad_headers[0]))[0]; |
---|
6552 | $r->recip_smtp_response(($final_destiny == D_DISCARD |
---|
6553 | ? "250 2.6.0 Ok, message with invalid header discarded" |
---|
6554 | : "554 5.6.0 Message with invalid header rejected" |
---|
6555 | ) . ", id=$am_id - $reason"); |
---|
6556 | $r->recip_done(1); |
---|
6557 | $any_badh++; |
---|
6558 | } |
---|
6559 | } |
---|
6560 | if ($any_badh) { # we use the same code as for viruses or banned |
---|
6561 | # but only if it wasn't already handled as spam |
---|
6562 | do_virus($conn, $msginfo, 0); # send notifications, quarantine it |
---|
6563 | } |
---|
6564 | } |
---|
6565 | section_time($which_section); |
---|
6566 | } |
---|
6567 | |
---|
6568 | my($mslm) = ca('message_size_limit_maps'); |
---|
6569 | if (@$mslm) { |
---|
6570 | $which_section = "deal_with_mail_size"; |
---|
6571 | my($mail_size) = $msginfo->msg_size; |
---|
6572 | for my $r (@{$msginfo->per_recip_data}) { |
---|
6573 | next if $r->recip_done; # already dealt with |
---|
6574 | my($size_limit) = lookup(0,$r->recip_addr, @$mslm); |
---|
6575 | $size_limit = 65536 |
---|
6576 | if $size_limit && $size_limit < 65536; # rfc2821 |
---|
6577 | if ($size_limit && $mail_size > $size_limit) { |
---|
6578 | do_log(1,sprintf("OVERSIZE from <%s> to <%s>: size %s B, limit %s B", |
---|
6579 | $msginfo->sender, $r->recip_addr, $mail_size, $size_limit)) |
---|
6580 | if !$considered_oversize_by_some_recips; |
---|
6581 | $considered_oversize_by_some_recips = 1; |
---|
6582 | $r->recip_destiny(D_BOUNCE); |
---|
6583 | $r->recip_smtp_response("552 5.3.4 Message size ($mail_size B) ". |
---|
6584 | "exceeds fixed maximium message size of $size_limit B, id=$am_id"); |
---|
6585 | $r->recip_done(1); |
---|
6586 | } |
---|
6587 | } |
---|
6588 | section_time($which_section); |
---|
6589 | } |
---|
6590 | |
---|
6591 | $which_section = "snooping_quarantine"; |
---|
6592 | # do_quarantine($conn, $msginfo, Amavis::Out::EditHeader->new, |
---|
6593 | # ['sender-quarantine'], 'local:user-%i-%n' |
---|
6594 | # ) if lookup(0,$msginfo->sender, ['user1@domain','user2@domain']); |
---|
6595 | # do_quarantine($conn, $msginfo, Amavis::Out::EditHeader->new, |
---|
6596 | # ['incoming-quarantine'], 'local:all-%i-%n'); |
---|
6597 | # do_quarantine($conn, $msginfo, Amavis::Out::EditHeader->new, |
---|
6598 | # ['archive@localhost'], 'local:all-%i-%n'); |
---|
6599 | # section_time($which_section); |
---|
6600 | |
---|
6601 | $which_section = "checking_sender_ip"; |
---|
6602 | my(@recips) = @{$msginfo->recips}; |
---|
6603 | if ($considered_spam_by_some_recips && @recips==1 && |
---|
6604 | $recips[0] eq $msginfo->sender && |
---|
6605 | lookup(0,$msginfo->sender, @{ca('local_domains_maps')})) |
---|
6606 | { |
---|
6607 | my($cl_ip) = $msginfo->client_addr; |
---|
6608 | if ($cl_ip eq '') { |
---|
6609 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
6610 | $cl_ip = fish_out_ip_from_received( |
---|
6611 | $msginfo->mime_entity->head->get('received',0)); |
---|
6612 | } |
---|
6613 | if ($cl_ip ne '') { |
---|
6614 | my($is_our_ip) = |
---|
6615 | eval { lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) }; |
---|
6616 | if ($@ ne '' && !$is_our_ip) { |
---|
6617 | do_log(0, "FAKE SENDER, SPAM: $cl_ip, " . $msginfo->sender); |
---|
6618 | $msginfo->sender_contact(undef); # believed to be faked |
---|
6619 | } |
---|
6620 | } |
---|
6621 | } |
---|
6622 | |
---|
6623 | if ($hold ne '') { do_log(-1, "NOTICE: HOLD reason: $hold") } |
---|
6624 | |
---|
6625 | # THIRD: now that we know what to do with it, do it! |
---|
6626 | |
---|
6627 | my($which_content_counter) = |
---|
6628 | @virusname ? 'ContentVirusMsgs' |
---|
6629 | : @banned_filename ? 'ContentBannedMsgs' |
---|
6630 | : $considered_spam_by_some_recips ? 'ContentSpamMsgs' |
---|
6631 | : @bad_headers ? 'ContentBadHdrMsgs' |
---|
6632 | : $considered_oversize_by_some_recips ? 'ContentOversizeMsgs' |
---|
6633 | : 'ContentCleanMsgs'; |
---|
6634 | snmp_count($which_content_counter); |
---|
6635 | |
---|
6636 | my($hdr_edits) = $msginfo->header_edits; |
---|
6637 | if (!$hdr_edits) { |
---|
6638 | $hdr_edits = Amavis::Out::EditHeader->new; |
---|
6639 | $msginfo->header_edits($hdr_edits); |
---|
6640 | } |
---|
6641 | if ($msginfo->delivery_method eq '') { # AM.PDP or AM.CL (milter) |
---|
6642 | $which_section = "AM.PDP headers"; |
---|
6643 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
6644 | $hdr_edits = add_forwarding_header_edits_common( |
---|
6645 | $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable, |
---|
6646 | $virus_presence_checked, $spam_presence_checked, undef); |
---|
6647 | my($done_all); |
---|
6648 | my($recip_cl); # ref to a list of similar recip objects |
---|
6649 | ($hdr_edits, $recip_cl, $done_all) = |
---|
6650 | add_forwarding_header_edits_per_recip( |
---|
6651 | $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable, |
---|
6652 | $virus_presence_checked, $spam_presence_checked, undef, undef); |
---|
6653 | $msginfo->header_edits($hdr_edits); # store edits (redundant?) |
---|
6654 | if (@$recip_cl && !$done_all) { |
---|
6655 | do_log(-1, "AM.PDP: CLIENTS REQUIRE DIFFERENT HEADERS"); |
---|
6656 | }; |
---|
6657 | } elsif (grep { !$_->recip_done } @{$msginfo->per_recip_data}) { # forward |
---|
6658 | # To be delivered explicitly - only to those recipients not yet marked |
---|
6659 | # as 'done' by the above content filtering sections. |
---|
6660 | $which_section = "forwarding"; |
---|
6661 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
6662 | # a quick-fix solution to defang dangerous contents |
---|
6663 | my($mail_defanged); # nonempty indicates mail body is replaced |
---|
6664 | my($explanation); |
---|
6665 | if ($hold ne '') { $explanation = |
---|
6666 | "WARNING: possible mail bomb, NOT CHECKED FOR VIRUSES:\n $hold"; |
---|
6667 | } elsif (@virusname) { $explanation = |
---|
6668 | 'WARNING: contains virus '.join(' ',@virusname) if c('defang_virus'); |
---|
6669 | } elsif (@banned_filename) { $explanation = |
---|
6670 | "WARNING: contains banned part" if c('defang_banned'); |
---|
6671 | } elsif ($any_undecipherable) { $explanation = |
---|
6672 | "WARNING: contains undecipherable part" if c('defang_undecipherable'); |
---|
6673 | } elsif ($considered_spam_by_some_recips) { $explanation = |
---|
6674 | $spam_report if c('defang_spam'); |
---|
6675 | } elsif (@bad_headers) { $explanation = |
---|
6676 | 'WARNING: bad headers '.join(' ',@bad_headers) if c('defang_bad_header'); |
---|
6677 | } else { $explanation = '(clean)' if c('defang_all') } |
---|
6678 | if (defined $explanation) { # malware |
---|
6679 | $explanation .= "\n" if $explanation !~ /\n\z/; |
---|
6680 | my($s) = $explanation; $s=~s/[ \t\n]+\z//; |
---|
6681 | if (length($s) > 100) { $s = substr($s,0,100-3) . "..." } |
---|
6682 | do_log(1, "DEFANGING MAIL: $s"); |
---|
6683 | my($d) = defanged_mime_entity($conn,$msginfo,$explanation); |
---|
6684 | $msginfo->mail_text($d); # substitute mail with rewritten version |
---|
6685 | $msginfo->mail_text_fn(undef); # remove filename information |
---|
6686 | $mail_defanged = 'Original mail wrapped as attachment (defanged)'; |
---|
6687 | section_time('defang'); |
---|
6688 | } |
---|
6689 | $hdr_edits = add_forwarding_header_edits_common( |
---|
6690 | $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable, |
---|
6691 | $virus_presence_checked, $spam_presence_checked, $mail_defanged); |
---|
6692 | for (;;) { # do the delivery |
---|
6693 | my($r_hdr_edits) = Amavis::Out::EditHeader->new; # per-recip edits set |
---|
6694 | $r_hdr_edits->inherit_header_edits($hdr_edits); |
---|
6695 | my($done_all); |
---|
6696 | my($recip_cl); # ref to a list of similar recip objects |
---|
6697 | ($r_hdr_edits, $recip_cl, $done_all) = |
---|
6698 | add_forwarding_header_edits_per_recip( |
---|
6699 | $conn, $msginfo, $r_hdr_edits, $hold, $any_undecipherable, |
---|
6700 | $virus_presence_checked, $spam_presence_checked, |
---|
6701 | $mail_defanged, undef); |
---|
6702 | last if !@$recip_cl; |
---|
6703 | $msginfo->header_edits($r_hdr_edits); # store edits |
---|
6704 | mail_dispatch($conn, $msginfo, 0, |
---|
6705 | sub { my($r) = @_; grep { $_ eq $r } @$recip_cl }); |
---|
6706 | snmp_count('OutForwMsgs'); |
---|
6707 | snmp_count('OutForwHoldMsgs') if $hold ne ''; |
---|
6708 | last if $done_all; |
---|
6709 | } |
---|
6710 | } |
---|
6711 | prolong_timer($which_section); |
---|
6712 | |
---|
6713 | $which_section = "delivery-notification"; |
---|
6714 | my($dsn_needed); |
---|
6715 | ($smtp_resp, $exit_code, $dsn_needed) = |
---|
6716 | one_response_for_all($msginfo, $dsn_per_recip_capable, $am_id); |
---|
6717 | my($warnsender_with_pass) = |
---|
6718 | $smtp_resp =~ /^2/ && !$dsn_needed && |
---|
6719 | (@virusname && c('warnvirussender') || |
---|
6720 | @banned_filename && c('warnbannedsender') || |
---|
6721 | $considered_spam_by_some_recips && c('warnspamsender') || |
---|
6722 | @bad_headers && c('warnbadhsender') ); |
---|
6723 | ll(4) && do_log(4,sprintf( |
---|
6724 | "warnsender_with_pass=%s (%s,%s,%s,%s), dsn_needed=%s, exit=%s, %s", |
---|
6725 | $warnsender_with_pass, |
---|
6726 | c('warnvirussender'),c('warnbannedsender'), |
---|
6727 | c('warnbadhsender'),c('warnspamsender'), |
---|
6728 | $dsn_needed,$exit_code,$smtp_resp)); |
---|
6729 | if ($dsn_needed || $warnsender_with_pass) { |
---|
6730 | ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root); |
---|
6731 | my($what_bad_content) = join(' & ', |
---|
6732 | !@virusname ? () : 'VIRUS', |
---|
6733 | !@banned_filename ? () : 'BANNED', |
---|
6734 | !$considered_spam_by_some_recips ? () : 'SPAM', |
---|
6735 | !@bad_headers ? () : 'BAD HEADER', |
---|
6736 | !$considered_oversize_by_some_recips ? () : 'OVERSIZE'); |
---|
6737 | my($notification); my($dsn_cutoff_level); |
---|
6738 | if ($msginfo->sender eq '') { # don't respond to null reverse path |
---|
6739 | my($msg) = "DSN contains $what_bad_content; bounce is not bouncible"; |
---|
6740 | if (!$dsn_needed) { do_log(4, $msg) } |
---|
6741 | else { do_log(1, "NOTICE: $msg, mail intentionally dropped") } |
---|
6742 | $msginfo->dsn_sent(2); # pretend the message was bounced |
---|
6743 | } elsif ($msginfo->sender_contact eq '') { |
---|
6744 | my($msg) = sprintf("Not sending DSN to believed-to-be-faked " |
---|
6745 | . "sender <%s>, mail containing %s", |
---|
6746 | $msginfo->sender, $what_bad_content); |
---|
6747 | if (!$dsn_needed) { do_log(4, $msg) } |
---|
6748 | else { do_log(2, "NOTICE: $msg intentionally dropped") } |
---|
6749 | $msginfo->dsn_sent(2); # pretend the message was bounced |
---|
6750 | } elsif ($banned_dsn_suppress) { |
---|
6751 | my($msg) = "Not sending DSN, as suggested by banned rule"; |
---|
6752 | if (!$dsn_needed) { do_log(4, $msg) } |
---|
6753 | else { do_log(1, "NOTICE: $msg, mail intentionally dropped") } |
---|
6754 | $msginfo->dsn_sent(2); # pretend the message was bounced |
---|
6755 | } elsif (defined $spam_level && |
---|
6756 | !grep { $spam_level + $_->recip_score_boost < |
---|
6757 | lookup(0,$_->recip_addr, |
---|
6758 | @{ca('spam_dsn_cutoff_level_maps')}) } |
---|
6759 | @{$msginfo->per_recip_data} ) { |
---|
6760 | my($msg) = "Not sending DSN, spam level exceeds DSN cutoff level"; |
---|
6761 | if (!$dsn_needed) { do_log(4, $msg) } |
---|
6762 | else { do_log(1, "NOTICE: $msg, mail intentionally dropped") } |
---|
6763 | $msginfo->dsn_sent(2); # pretend the message was bounced |
---|
6764 | } elsif ((@virusname || @banned_filename || |
---|
6765 | $considered_spam_by_some_recips || @bad_headers || |
---|
6766 | $considered_oversize_by_some_recips) && |
---|
6767 | $msginfo->mime_entity->head->get('precedence',0) |
---|
6768 | =~ /^(bulk|list|junk)/i ) |
---|
6769 | { |
---|
6770 | my($msg) = sprintf("Not sending DSN in response to bulk mail " |
---|
6771 | . "from <%s> containing %s", |
---|
6772 | $msginfo->sender, $what_bad_content); |
---|
6773 | if (!$dsn_needed) { do_log(4, $msg) } |
---|
6774 | else { do_log(1, "NOTICE: $msg, mail intentionally dropped") } |
---|
6775 | $msginfo->dsn_sent(2); # pretend the message was bounced |
---|
6776 | } else { # prepare a notification |
---|
6777 | my($which_dsn_counter,$dsnmsgref); |
---|
6778 | ### TODO: better selection of DSN reason is needed! |
---|
6779 | for my $r (@{$msginfo->per_recip_data}) { |
---|
6780 | next if !$r->recip_done; |
---|
6781 | local($_) = $r->recip_smtp_response; |
---|
6782 | ($which_dsn_counter,$dsnmsgref) = |
---|
6783 | /^5.*\bVIRUS\b/ ? |
---|
6784 | ('OutDsnVirusMsgs', cr('notify_virus_sender_templ')) |
---|
6785 | : /^5.*\bBANNED\b/ ? |
---|
6786 | ('OutDsnBannedMsgs',cr('notify_virus_sender_templ')) |
---|
6787 | : /^5.*\b(?:UBE|blacklisted)\b/ ? |
---|
6788 | ('OutDsnSpamMsgs', cr('notify_spam_sender_templ')) |
---|
6789 | : /^5.*\bheader\b/ ? |
---|
6790 | ('OutDsnBadHdrMsgs',cr('notify_sender_templ')) |
---|
6791 | : ('OutDsnOtherMsgs', cr('notify_sender_templ')); |
---|
6792 | } |
---|
6793 | # generate delivery status notification according to rfc3462 |
---|
6794 | # and rfc3464 if needed |
---|
6795 | $notification = delivery_status_notification($conn, $msginfo, |
---|
6796 | $warnsender_with_pass, \%builtins, $dsnmsgref) if $dsnmsgref; |
---|
6797 | snmp_count($which_dsn_counter) if defined $notification; |
---|
6798 | } |
---|
6799 | if (defined $notification) { # dsn needed, send delivery notification |
---|
6800 | mail_dispatch($conn, $notification, 1); |
---|
6801 | snmp_count('OutDsnMsgs'); |
---|
6802 | my($n_smtp_resp, $n_exit_code, $n_dsn_needed) = |
---|
6803 | one_response_for_all($notification, 0, $am_id); # check status |
---|
6804 | if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # dsn successful? |
---|
6805 | $msginfo->dsn_sent(1); # mark the message as bounced |
---|
6806 | } elsif ($n_smtp_resp =~ /^4/) { |
---|
6807 | snmp_count('OutDsnTempFails'); |
---|
6808 | die sprintf("temporarily unable to send DSN to <%s>: %s", |
---|
6809 | $msginfo->sender_contact, $n_smtp_resp); |
---|
6810 | } else { |
---|
6811 | snmp_count('OutDsnRejects'); |
---|
6812 | do_log(-1,sprintf("NOTICE: UNABLE TO SEND DSN to <%s>: %s", |
---|
6813 | $msginfo->sender, $n_smtp_resp)); |
---|
6814 | # # if dsn can not be sent, try to send it to postmaster |
---|
6815 | # $notification->recips(['postmaster']); |
---|
6816 | # # attempt double bounce |
---|
6817 | # mail_dispatch($conn, $notification, 1); |
---|
6818 | } |
---|
6819 | # $notification->purge; |
---|
6820 | } |
---|
6821 | } |
---|
6822 | prolong_timer($which_section); |
---|
6823 | |
---|
6824 | # generate customized log report at log level 0 - this is usually the |
---|
6825 | # only log entry interesting to administrators during normal operation |
---|
6826 | $which_section = 'main_log_entry'; |
---|
6827 | my(%mybuiltins) = %builtins; # make a local copy |
---|
6828 | { # do a per-mail log entry |
---|
6829 | my($s) = $spam_status; $s =~ s/^tests=//; my(@s) = split(/,/,$s); |
---|
6830 | if (@s > 10) { $#s = 9; push(@s,"...") } |
---|
6831 | $mybuiltins{'T'} = \@s; # macro %T has overloaded semantics, ugly |
---|
6832 | my($strr) = expand(cr('log_templ'), \%mybuiltins); |
---|
6833 | for my $logline (split(/[ \t]*\n/, $$strr)) { |
---|
6834 | do_log(0, $logline) if $logline ne ''; |
---|
6835 | } |
---|
6836 | } |
---|
6837 | if (c('log_recip_templ') ne '') { # do per-recipient log entries |
---|
6838 | # redefine macros with a by-recipient semantics |
---|
6839 | for my $r (@{$msginfo->per_recip_data}) { |
---|
6840 | # recipient counter in macro %. may indicate to the template |
---|
6841 | # that a per-recipient expansion semantics is expected |
---|
6842 | $mybuiltins{'.'}++; |
---|
6843 | my($recip) = $r->recip_addr; |
---|
6844 | my($smtp_resp) = $r->recip_smtp_response; |
---|
6845 | my($qrecip_addr) = scalar(qquote_rfc2821_local($recip)); |
---|
6846 | $mybuiltins{'D'} = $mybuiltins{'O'} = $mybuiltins{'N'} = undef; |
---|
6847 | if ($r->recip_destiny==D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)){ |
---|
6848 | $mybuiltins{'D'} = $qrecip_addr; |
---|
6849 | } else { |
---|
6850 | $mybuiltins{'O'} = $qrecip_addr; |
---|
6851 | my($remote_mta) = $r->recip_remote_mta; |
---|
6852 | $mybuiltins{'N'} = sprintf("%s:%s\n %s", $qrecip_addr, |
---|
6853 | ($remote_mta eq '' ? '' : " $remote_mta said:"), $smtp_resp); |
---|
6854 | } |
---|
6855 | my($blacklisted) = $r->recip_blacklisted_sender; |
---|
6856 | my($whitelisted) = $r->recip_whitelisted_sender; |
---|
6857 | my($boost) = $r->recip_score_boost; |
---|
6858 | my($is_local,$tag_level,$tag2_level,$kill_level); |
---|
6859 | $is_local = lookup(0,$recip, @{ca('local_domains_maps')}); |
---|
6860 | $tag_level = lookup(0,$recip, @{ca('spam_tag_level_maps')}); |
---|
6861 | $tag2_level = lookup(0,$recip, @{ca('spam_tag2_level_maps')}); |
---|
6862 | $kill_level = lookup(0,$recip, @{ca('spam_kill_level_maps')}); |
---|
6863 | my($do_tag) = |
---|
6864 | $blacklisted || !defined $tag_level || |
---|
6865 | (defined $spam_level ? $spam_level+$boost >= $tag_level |
---|
6866 | : $whitelisted ? (-10 >= $tag_level) : 0); |
---|
6867 | my($do_tag2) = !$whitelisted && |
---|
6868 | ( $blacklisted || |
---|
6869 | (defined $spam_level && defined $tag2_level ? |
---|
6870 | $spam_level+$boost >= $tag2_level : 0) ); |
---|
6871 | my($do_kill) = !$whitelisted && |
---|
6872 | ( $blacklisted || |
---|
6873 | (defined $spam_level && defined $kill_level ? |
---|
6874 | $spam_level+$boost >= $kill_level : 0) ); |
---|
6875 | for ($do_tag,$do_tag2,$do_kill) { $_ = $_ ? 'Y' : '0' } # normalize |
---|
6876 | for ($is_local) { $_ = $_ ? 'L' : '0' } # normalize |
---|
6877 | for ($tag_level,$tag2_level,$kill_level) { $_ = 'x' if !defined($_) } |
---|
6878 | $mybuiltins{'R'} = $recip; |
---|
6879 | $mybuiltins{'c'} = !defined $spam_level ? '-' |
---|
6880 | : 0+sprintf("%.3f",$spam_level+$boost); |
---|
6881 | @mybuiltins{('0','1','2','k')} = ($is_local,$do_tag,$do_tag2,$do_kill); |
---|
6882 | # macros %3, %4, %5 are experimental, until a better solution is found |
---|
6883 | @mybuiltins{('3','4','5')} = ($tag_level,$tag2_level,$kill_level); |
---|
6884 | my($strr) = expand(cr('log_recip_templ'), \%mybuiltins); |
---|
6885 | for my $logline (split(/[ \t]*\n/, $$strr)) { |
---|
6886 | do_log(0, $logline) if $logline ne ''; |
---|
6887 | } |
---|
6888 | } |
---|
6889 | } |
---|
6890 | section_time($which_section); |
---|
6891 | |
---|
6892 | $which_section = 'finishing'; |
---|
6893 | $snmp_db->update_counters if defined $snmp_db; |
---|
6894 | section_time('update_snmp'); |
---|
6895 | |
---|
6896 | }; # end eval |
---|
6897 | if ($@ ne '') { |
---|
6898 | chomp($@); |
---|
6899 | $preserve_evidence = 1; |
---|
6900 | my($msg) = "$which_section FAILED: $@"; |
---|
6901 | do_log(-2, "TROUBLE in check_mail: $msg"); |
---|
6902 | $smtp_resp = "451 4.5.0 Error in processing, id=$am_id, $msg"; |
---|
6903 | $exit_code = EX_TEMPFAIL; |
---|
6904 | for my $r (@{$msginfo->per_recip_data}) { |
---|
6905 | next if $r->recip_done; |
---|
6906 | $r->recip_smtp_response($smtp_resp); $r->recip_done(1); |
---|
6907 | } |
---|
6908 | } |
---|
6909 | # if ($hold ne '') { |
---|
6910 | # do_log(-1, "NOTICE: Evidence is to be preserved: $hold"); |
---|
6911 | # $preserve_evidence = 1; |
---|
6912 | # } |
---|
6913 | if (!$preserve_evidence && debug_oneshot()) { |
---|
6914 | do_log(0, "DEBUG_ONESHOT CAUSES EVIDENCE TO BE PRESERVED"); |
---|
6915 | $preserve_evidence = 1; |
---|
6916 | } |
---|
6917 | |
---|
6918 | my($which_counter) = 'InUnknown'; |
---|
6919 | if ($smtp_resp =~ /^4/) { $which_counter = 'InTempFails' } |
---|
6920 | elsif ($smtp_resp =~ /^5/) { $which_counter = 'InRejects' } |
---|
6921 | elsif ($smtp_resp =~ /^2/) { |
---|
6922 | my($dsn_sent) = $msginfo->dsn_sent; |
---|
6923 | if (!$dsn_sent) { $which_counter = $msginfo->delivery_method ne '' |
---|
6924 | ? 'InAccepts' : 'InContinues' } |
---|
6925 | elsif ($dsn_sent==1) { $which_counter = 'InBounces' } |
---|
6926 | elsif ($dsn_sent==2) { $which_counter = 'InDiscards' } |
---|
6927 | } |
---|
6928 | snmp_count($which_counter); |
---|
6929 | $snmp_db->register_proc('.') if defined $snmp_db; # content checking done |
---|
6930 | |
---|
6931 | $MSGINFO = undef; # release global reference to msginfo object |
---|
6932 | ($smtp_resp, $exit_code, $preserve_evidence); |
---|
6933 | } |
---|
6934 | |
---|
6935 | # Ensure we have $msginfo->$entity defined when we expect we'll need it, |
---|
6936 | # e.g. to construct notifications. While at it, also get us some additional |
---|
6937 | # information on sender from the header. |
---|
6938 | # |
---|
6939 | sub ensure_mime_entity($$$$$) { |
---|
6940 | my($msginfo, $fh, $tempdir, $virusname_list, $parts_root) = @_; |
---|
6941 | if (!defined($msginfo->mime_entity)) { |
---|
6942 | # header may not have been parsed yet, e.g. if the result was cached |
---|
6943 | my($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root); |
---|
6944 | $msginfo->mime_entity($ent); |
---|
6945 | prolong_timer("ensure_mime_entity"); |
---|
6946 | } |
---|
6947 | } |
---|
6948 | |
---|
6949 | sub add_forwarding_header_edits_common($$$$$$) { |
---|
6950 | my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable, |
---|
6951 | $virus_presence_checked, $spam_presence_checked, $mail_defanged) = @_; |
---|
6952 | |
---|
6953 | $hdr_edits->prepend_header('Received', |
---|
6954 | received_line($conn,$msginfo,am_id(),1), 1) |
---|
6955 | if $insert_received_line && $msginfo->delivery_method ne ''; |
---|
6956 | # discard existing X-Amavis-Hold header field, only allow our own |
---|
6957 | $hdr_edits->delete_header('X-Amavis-Hold'); |
---|
6958 | if ($hold ne '') { |
---|
6959 | $hdr_edits->append_header('X-Amavis-Hold', $hold); |
---|
6960 | do_log(-1, "Inserting header field: X-Amavis-Hold: $hold"); |
---|
6961 | } |
---|
6962 | if ($mail_defanged ne '') { |
---|
6963 | # prepend Resent-* header fields, they must precede |
---|
6964 | # corresponding Received header field (pushed in reverse order) |
---|
6965 | $hdr_edits->prepend_header('Resent-Message-ID', |
---|
6966 | sprintf('<RE%s@%s>',am_id(),$myhostname) ); |
---|
6967 | $hdr_edits->prepend_header('Resent-Date', |
---|
6968 | rfc2822_timestamp($msginfo->rx_time)); |
---|
6969 | $hdr_edits->prepend_header('Resent-From', c('hdrfrom_notify_recip')); |
---|
6970 | # append X-Amavis-Modified |
---|
6971 | my($msg) = "$mail_defanged by $myhostname"; |
---|
6972 | $hdr_edits->append_header('X-Amavis-Modified', $msg); |
---|
6973 | do_log(1, "Inserting header field: X-Amavis-Modified: $msg"); |
---|
6974 | } |
---|
6975 | if ($extra_code_antivirus) { |
---|
6976 | $hdr_edits->delete_header('X-Amavis-Alert'); |
---|
6977 | $hdr_edits->delete_header(c('X_HEADER_TAG')) |
---|
6978 | if c('remove_existing_x_scanned_headers') && |
---|
6979 | (c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/); |
---|
6980 | } |
---|
6981 | if ($extra_code_antispam) { |
---|
6982 | if (c('remove_existing_spam_headers')) { |
---|
6983 | my(@which_headers) = qw( |
---|
6984 | X-Spam-Status X-Spam-Level X-Spam-Flag X-Spam-Score |
---|
6985 | X-Spam-Report X-Spam-Checker-Version X-Spam-Tests); |
---|
6986 | push(@which_headers, qw(X-DSPAM-Result X-DSPAM-Signature X-DSPAM-User |
---|
6987 | X-DSPAM-Probability) ) if defined $dspam; |
---|
6988 | for my $h (@which_headers) { $hdr_edits->delete_header($h) } |
---|
6989 | } |
---|
6990 | # $hdr_edits->append_header('X-Spam-Checker-Version', |
---|
6991 | # sprintf("SpamAssassin %s (%s) on %s", Mail::SpamAssassin::Version(), |
---|
6992 | # $Mail::SpamAssassin::SUB_VERSION, $myhostname)); |
---|
6993 | } |
---|
6994 | $hdr_edits; |
---|
6995 | } |
---|
6996 | |
---|
6997 | # Prepare header edits for the first not-yet-done recipient. |
---|
6998 | # Inspect remaining recipients, returning the list of recipient objects |
---|
6999 | # that are receiving the same set of header edits (so the message may be |
---|
7000 | # delivered to them in one SMTP transaction). |
---|
7001 | # |
---|
7002 | sub add_forwarding_header_edits_per_recip($$$$$$$$$) { |
---|
7003 | my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable, |
---|
7004 | $virus_presence_checked, $spam_presence_checked, |
---|
7005 | $mail_defanged, $filter) = @_; |
---|
7006 | my(@recip_cluster); |
---|
7007 | my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) } |
---|
7008 | @{$msginfo->per_recip_data}; |
---|
7009 | my($per_recip_data_len) = scalar(@per_recip_data); |
---|
7010 | my($first) = 1; my($cluster_key); my($cluster_full_spam_status); |
---|
7011 | for my $r (@per_recip_data) { |
---|
7012 | my($recip) = $r->recip_addr; |
---|
7013 | my($is_local,$blacklisted,$whitelisted,$boost,$tag_level,$tag2_level, |
---|
7014 | $do_tag_virus_checked,$do_tag_virus,$do_tag_banned,$do_tag_badh, |
---|
7015 | $do_tag,$do_tag2,$do_subj,$do_subj_u,$subject_tag,$subject_tag2); |
---|
7016 | $is_local = lookup(0,$recip, @{ca('local_domains_maps')}); |
---|
7017 | $do_tag_badh = @bad_headers && |
---|
7018 | !lookup(0,$recip,@{ca('bypass_header_checks_maps')}); |
---|
7019 | $do_tag_banned= @banned_filename && |
---|
7020 | !lookup(0,$recip,@{ca('bypass_banned_checks_maps')}); |
---|
7021 | $do_tag_virus = @virusname && |
---|
7022 | !lookup(0,$recip,@{ca('bypass_virus_checks_maps')}); |
---|
7023 | $do_tag_virus_checked = |
---|
7024 | $virus_presence_checked && |
---|
7025 | (c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/) && |
---|
7026 | !lookup(0,$recip,@{ca('bypass_virus_checks_maps')}); |
---|
7027 | if ($extra_code_antispam) { |
---|
7028 | my($bypassed); |
---|
7029 | $blacklisted = $r->recip_blacklisted_sender; |
---|
7030 | $whitelisted = $r->recip_whitelisted_sender; |
---|
7031 | $boost = $r->recip_score_boost; |
---|
7032 | $bypassed = lookup(0,$recip, @{ca('bypass_spam_checks_maps')}); |
---|
7033 | $tag_level = lookup(0,$recip, @{ca('spam_tag_level_maps')}); |
---|
7034 | $tag2_level = lookup(0,$recip, @{ca('spam_tag2_level_maps')}); |
---|
7035 | # spam-related headers should _not_ be inserted for: |
---|
7036 | # - nonlocal recipients (outgoing mail), as a matter of courtesy |
---|
7037 | # to our users; |
---|
7038 | # - recipients matching bypass_spam_checks: even though spam checking |
---|
7039 | # may have been done for other reasons, these recipients do not |
---|
7040 | # expect such headers, so let's pretend the check has not been done |
---|
7041 | # and not insert spam-related headers for them |
---|
7042 | $do_tag = $is_local && !$bypassed && |
---|
7043 | ( $blacklisted || !defined $tag_level || |
---|
7044 | (defined $spam_level ? $spam_level+$boost >= $tag_level |
---|
7045 | : $whitelisted ? (-10 >= $tag_level) : 0) ); |
---|
7046 | $do_tag2 = $is_local && !$bypassed && !$whitelisted && |
---|
7047 | ( $blacklisted || |
---|
7048 | (defined $spam_level && defined $tag2_level ? |
---|
7049 | $spam_level+$boost >= $tag2_level : 0) ); |
---|
7050 | $subject_tag2 = !$do_tag2 ? undef |
---|
7051 | : lookup(0,$recip, @{ca('spam_subject_tag2_maps')}); |
---|
7052 | $subject_tag = !($do_tag||$do_tag2) ? undef |
---|
7053 | : lookup(0,$recip, @{ca('spam_subject_tag_maps')}); |
---|
7054 | $do_subj = ($subject_tag2 ne '' || $subject_tag ne '') && |
---|
7055 | lookup(0,$recip, @{ca('spam_modifies_subj_maps')}); |
---|
7056 | } |
---|
7057 | if ($hold ne '' || $any_undecipherable) { # adding *UNCHECKED* subject tag? |
---|
7058 | $do_subj_u = $is_local && c('undecipherable_subject_tag') ne '' && |
---|
7059 | !(@virusname && !lookup(0,$recip, |
---|
7060 | @{ca('bypass_virus_checks_maps')}) ); |
---|
7061 | } |
---|
7062 | # normalize |
---|
7063 | for ($do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh, |
---|
7064 | $do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local) { $_ = $_?1:0 } |
---|
7065 | my($spam_level_bar, $full_spam_status); |
---|
7066 | if ($do_tag || $do_tag2) { |
---|
7067 | my($slc) = c('sa_spam_level_char'); |
---|
7068 | $spam_level_bar = |
---|
7069 | $slc x min($blacklisted ? 64 : $spam_level+$boost, 64) if $slc ne ''; |
---|
7070 | my($s) = $spam_status; $s =~ s/,/,\n /g; # allow header field wrapping |
---|
7071 | $full_spam_status = sprintf("%s,\n hits=%s\n%s%s %s%s", |
---|
7072 | $do_tag2 ? 'Yes' : 'No', |
---|
7073 | !defined $spam_level ? 'x' : 0+sprintf("%.3f",$spam_level+$boost), |
---|
7074 | !defined $tag_level ? '' : sprintf(" tagged_above=%s\n",$tag_level), |
---|
7075 | !defined $tag2_level ? '' : sprintf(" required=%s\n", $tag2_level), |
---|
7076 | join('', $blacklisted ? "BLACKLISTED\n " : (), |
---|
7077 | $whitelisted ? "WHITELISTED\n " : ()), |
---|
7078 | $s); |
---|
7079 | } |
---|
7080 | my($key) = join("\000", |
---|
7081 | $do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh, |
---|
7082 | $do_tag, $do_tag2, $do_subj, $do_subj_u, $spam_level_bar, |
---|
7083 | $full_spam_status); |
---|
7084 | if ($first) { |
---|
7085 | ll(4) && do_log(4,sprintf( |
---|
7086 | "headers CLUSTERING: NEW CLUSTER <%s>: ". |
---|
7087 | "hits=%s, tag=%s, tag2=%s, subj=%s, subj_u=%s, local=%s, bl=%s", |
---|
7088 | $recip, |
---|
7089 | (!defined $spam_level ? 'x' |
---|
7090 | : !defined $boost ? $spam_level |
---|
7091 | : $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost), |
---|
7092 | $do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local, $blacklisted)); |
---|
7093 | $cluster_key = $key; $cluster_full_spam_status = $full_spam_status; |
---|
7094 | } elsif ($key eq $cluster_key) { |
---|
7095 | do_log(5,"headers CLUSTERING: <$recip> joining cluster"); |
---|
7096 | } else { |
---|
7097 | do_log(5,"headers CLUSTERING: skipping <$recip> (tag=$do_tag, tag2=$do_tag2)"); |
---|
7098 | next; # this recipient will be handled in some later pass |
---|
7099 | } |
---|
7100 | |
---|
7101 | if ($first) { # insert headers required for the new cluster |
---|
7102 | if ($do_tag_virus_checked) { |
---|
7103 | $hdr_edits->append_header(c('X_HEADER_TAG'), c('X_HEADER_LINE')); |
---|
7104 | } |
---|
7105 | if ($do_tag_virus) { |
---|
7106 | $hdr_edits->append_header('X-Amavis-Alert', |
---|
7107 | "INFECTED, message contains virus:\n " . join(",\n ",@virusname), 1); |
---|
7108 | } |
---|
7109 | if ($do_tag_banned) { |
---|
7110 | my(@b) = @banned_filename>3 ? @banned_filename[0..2] :@banned_filename; |
---|
7111 | my($msg) = "BANNED, message contains " |
---|
7112 | . (@banned_filename==1 ? 'part' : 'parts') . ":\n " |
---|
7113 | . join(",\n ", @b) . (@banned_filename > @b ? ", ..." : ""); |
---|
7114 | $hdr_edits->append_header('X-Amavis-Alert', $msg, 1); |
---|
7115 | } |
---|
7116 | if ($do_tag_badh) { |
---|
7117 | $hdr_edits->append_header('X-Amavis-Alert', |
---|
7118 | 'BAD HEADER '.$bad_headers[0], 1); |
---|
7119 | } |
---|
7120 | if ($do_tag) { |
---|
7121 | $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1); |
---|
7122 | # $hdr_edits->append_header('X-Spam-Score', |
---|
7123 | # !defined $spam_level ? '-' : 0+sprintf("%.3f",$spam_level+$boost) ); |
---|
7124 | $hdr_edits->append_header('X-Spam-Level', |
---|
7125 | $spam_level_bar) if defined $spam_level_bar; |
---|
7126 | } |
---|
7127 | if ($do_tag2) { |
---|
7128 | $hdr_edits->append_header('X-Spam-Flag', 'YES'); |
---|
7129 | $hdr_edits->append_header('X-Spam-Report', $spam_report,1) |
---|
7130 | if $spam_report ne '' && c('sa_spam_report_header'); |
---|
7131 | } |
---|
7132 | if ($do_subj || $do_subj_u) { |
---|
7133 | my($s) = ''; |
---|
7134 | if ($do_subj_u) { |
---|
7135 | $s = c('undecipherable_subject_tag'); |
---|
7136 | do_log(3,"adding $s, $any_undecipherable, $hold"); |
---|
7137 | } |
---|
7138 | if ($do_subj) { |
---|
7139 | $s .= $do_tag2 && $subject_tag2 ne '' ? $subject_tag2 : $subject_tag; |
---|
7140 | } |
---|
7141 | my($entity) = $msginfo->mime_entity; |
---|
7142 | if (defined $entity && defined $entity->head->get('Subject',0)) { |
---|
7143 | $hdr_edits->edit_header('Subject', |
---|
7144 | sub { $_[1]=~/^([ \t]?)(.*)\z/s; ' '.$s.$2 }); |
---|
7145 | } else { # no Subject header field present, insert one |
---|
7146 | $s =~ s/[ \t]+\z//; # trim |
---|
7147 | $hdr_edits->append_header('Subject', $s); |
---|
7148 | if (!defined $entity) { |
---|
7149 | do_log(-1,"WARN: no MIME entity!? Inserting 'Subject'"); |
---|
7150 | } else { |
---|
7151 | do_log(0,"INFO: no existing header field 'Subject', inserting it"); |
---|
7152 | } |
---|
7153 | } |
---|
7154 | } |
---|
7155 | } |
---|
7156 | push(@recip_cluster,$r); $first = 0; |
---|
7157 | |
---|
7158 | my($delim) = c('recipient_delimiter'); |
---|
7159 | if ($delim ne '' && $is_local) { |
---|
7160 | # append address extensions to mailbox names if desired |
---|
7161 | my($ext_map) = $do_tag_virus ? ca('addr_extension_virus_maps') |
---|
7162 | : $do_tag_banned ? ca('addr_extension_banned_maps') |
---|
7163 | : $do_tag2 ? ca('addr_extension_spam_maps') |
---|
7164 | : $do_tag_badh ? ca('addr_extension_bad_header_maps') |
---|
7165 | : undef; |
---|
7166 | my($ext) = !ref($ext_map) ? undef : lookup(0,$recip, @$ext_map); |
---|
7167 | if ($ext ne '') { |
---|
7168 | my($localpart,$domain) = split_address($recip); |
---|
7169 | if (c('replace_existing_extension')) # strip existing address extensions |
---|
7170 | { $localpart =~ s/^(.*?)\Q$delim\E.*\z/$1/s } |
---|
7171 | do_log(5,"adding address extension $delim$ext to $localpart$domain"); |
---|
7172 | $r->recip_addr_modified($localpart.$delim.$ext.$domain); |
---|
7173 | } |
---|
7174 | } |
---|
7175 | } |
---|
7176 | my($done_all); |
---|
7177 | if (@recip_cluster == $per_recip_data_len) { |
---|
7178 | do_log(5,"headers CLUSTERING: " . |
---|
7179 | "done all $per_recip_data_len recips in one go"); |
---|
7180 | $done_all = 1; |
---|
7181 | } else { |
---|
7182 | ll(4) && do_log(4,sprintf( |
---|
7183 | "headers CLUSTERING: got %d recips out of %d: %s", |
---|
7184 | scalar(@recip_cluster), $per_recip_data_len, |
---|
7185 | join(", ", map { "<" . $_->recip_addr . ">" } @recip_cluster) )); |
---|
7186 | } |
---|
7187 | if (defined($cluster_full_spam_status) && @recip_cluster) { |
---|
7188 | my($s) = $cluster_full_spam_status; $s =~ s/\n[ \t]/ /g; |
---|
7189 | ll(2) && do_log(2,sprintf("SPAM-TAG, %s -> %s, %s", |
---|
7190 | qquote_rfc2821_local($msginfo->sender), |
---|
7191 | join(',', qquote_rfc2821_local( |
---|
7192 | map { $_->recip_addr } @recip_cluster)), $s)); |
---|
7193 | } |
---|
7194 | ($hdr_edits, \@recip_cluster, $done_all); |
---|
7195 | } |
---|
7196 | |
---|
7197 | sub do_quarantine($$$$$;$) { |
---|
7198 | my($conn,$msginfo,$hdr_edits,$recips_ref,$quarantine_method,$snmp_id) = @_; |
---|
7199 | if ($quarantine_method eq '') { do_log(5, "quarantine disabled") } |
---|
7200 | else { |
---|
7201 | # fudge to make %b access the body_digest of $msginfo, not of $quar_msg |
---|
7202 | $quarantine_method =~ s/%b/$msginfo->body_digest/eg; |
---|
7203 | my($sender) = $msginfo->sender; |
---|
7204 | my($quar_msg) = Amavis::In::Message->new; |
---|
7205 | $quar_msg->rx_time($msginfo->rx_time); # copy the reception time |
---|
7206 | $quar_msg->delivery_method($quarantine_method); |
---|
7207 | if ($quarantine_method =~ /^bsmtp:/i) { |
---|
7208 | $quar_msg->sender($sender); # original sender & recipients |
---|
7209 | $quar_msg->recips($msginfo->recips); |
---|
7210 | } else { |
---|
7211 | my($mftq) = c('mailfrom_to_quarantine'); |
---|
7212 | $quar_msg->sender(defined $mftq ? $mftq : $sender); |
---|
7213 | $quar_msg->recips($recips_ref); # e.g. per-recip quarantine |
---|
7214 | # NOTE: RFC2821 mentions possible headers X-SMTP-MAIL and X-SMTP-RCPT |
---|
7215 | # Exim uses: Envelope-To, Sendmail uses X-Envelope-To; |
---|
7216 | # No need with bsmtp, which already carries addresses in the envelope |
---|
7217 | $hdr_edits->prepend_header('X-Envelope-To', |
---|
7218 | join(",\n ", qquote_rfc2821_local(@{$msginfo->recips})), 1); |
---|
7219 | $hdr_edits->prepend_header('X-Envelope-From', |
---|
7220 | qquote_rfc2821_local($sender)); |
---|
7221 | } |
---|
7222 | do_log(5, "DO_QUARANTINE, sender: " . $quar_msg->sender); |
---|
7223 | $quar_msg->auth_submitter(qquote_rfc2821_local($quar_msg->sender)); |
---|
7224 | $quar_msg->auth_user(c('amavis_auth_user')); |
---|
7225 | $quar_msg->auth_pass(c('amavis_auth_pass')); |
---|
7226 | $quar_msg->header_edits($hdr_edits); |
---|
7227 | $quar_msg->mail_text($msginfo->mail_text); # use the same mail contents |
---|
7228 | |
---|
7229 | snmp_count('QuarMsgs'); |
---|
7230 | mail_dispatch($conn, $quar_msg, 1); |
---|
7231 | my($n_smtp_resp, $n_exit_code, $n_dsn_needed) = |
---|
7232 | one_response_for_all($quar_msg, 0, am_id()); # check status |
---|
7233 | if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok |
---|
7234 | snmp_count($snmp_id eq '' ? 'QuarOther' : $snmp_id); |
---|
7235 | } elsif ($n_smtp_resp =~ /^4/) { |
---|
7236 | snmp_count('QuarAttemptTempFails'); |
---|
7237 | die "temporarily unable to quarantine: $n_smtp_resp"; |
---|
7238 | } else { # abort if quarantining not successful |
---|
7239 | snmp_count('QuarAttemptFails'); |
---|
7240 | die "Can not quarantine: $n_smtp_resp"; |
---|
7241 | } |
---|
7242 | my(@qa); my(%seen); # collect unique quarantine mailboxes or addresses |
---|
7243 | for my $r (@{$quar_msg->per_recip_data}) { |
---|
7244 | my($mbxname) = $r->recip_mbxname; |
---|
7245 | push(@qa,$mbxname) if $mbxname ne '' && !$seen{$mbxname}++; |
---|
7246 | } |
---|
7247 | $msginfo->quarantined_to(\@qa); # remember where it was quarantined to |
---|
7248 | do_log(5, "DO_QUARANTINE done"); |
---|
7249 | } |
---|
7250 | } |
---|
7251 | |
---|
7252 | # If virus/banned/bad-header found - quarantine it and send notifications |
---|
7253 | sub do_virus($$$) { |
---|
7254 | my($conn, $msginfo, $virus_dejavu) = @_; |
---|
7255 | my($q_method, $quarantine_to_maps_ref, |
---|
7256 | $bypass_checks_maps_ref, $admin_maps_ref) = |
---|
7257 | @virusname ? |
---|
7258 | (c('virus_quarantine_method'), |
---|
7259 | ca('virus_quarantine_to_maps'), |
---|
7260 | ca('bypass_virus_checks_maps'), |
---|
7261 | ca('virus_admin_maps') ) |
---|
7262 | : @banned_filename ? |
---|
7263 | (c('banned_files_quarantine_method'), |
---|
7264 | ca('banned_quarantine_to_maps'), |
---|
7265 | ca('bypass_banned_checks_maps'), |
---|
7266 | ca('banned_admin_maps') ) |
---|
7267 | : @bad_headers ? |
---|
7268 | (c('bad_header_quarantine_method'), |
---|
7269 | ca('bad_header_quarantine_to_maps'), |
---|
7270 | ca('bypass_header_checks_maps'), |
---|
7271 | ca('bad_header_admin_maps') ) |
---|
7272 | : (undef, undef, undef, undef); |
---|
7273 | # suggest a name to be used as 'X-Quarantine-Id:' or file name |
---|
7274 | $VIRUSFILE = $q_method =~ /^(?:local|bsmtp):(.*)\z/si ? $1 : "virus-%i-%n"; |
---|
7275 | $VIRUSFILE =~ s{%(.)} |
---|
7276 | { $1 eq 'b' ? $msginfo->body_digest |
---|
7277 | : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time)) |
---|
7278 | : $1 eq 'n' ? am_id() |
---|
7279 | : $1 eq '%' ? '%' : '%'.$1 }egs; |
---|
7280 | do_log(5, "do_virus: looking for per-recipient quarantine and admins"); |
---|
7281 | my($newvirus_admin_maps_ref) = |
---|
7282 | @virusname && !$virus_dejavu ? ca('newvirus_admin_maps') : undef; |
---|
7283 | my(@q_addr,@a_addr); # get per-recipient quarantine address(es) and admins |
---|
7284 | for my $r (@{$msginfo->per_recip_data}) { |
---|
7285 | my($rec) = $r->recip_addr; |
---|
7286 | my($q); # quarantine (pseudo) address associated with the recipient |
---|
7287 | my($a); # administrator's e-mail address |
---|
7288 | ($q) = lookup(0,$rec,@$quarantine_to_maps_ref) if $quarantine_to_maps_ref; |
---|
7289 | $q = $rec if $q ne '' && $q_method =~ /^bsmtp:/i; # orig.recip when BSMTP |
---|
7290 | ($a) = lookup(0,$rec,@$admin_maps_ref) if $admin_maps_ref; |
---|
7291 | push(@q_addr, $q) if $q ne '' && !grep {$_ eq $q} @q_addr; |
---|
7292 | push(@a_addr, $a) if $a ne '' && !grep {$_ eq $a} @a_addr; |
---|
7293 | if ($newvirus_admin_maps_ref) { |
---|
7294 | ($a) = lookup(0,$rec,@$newvirus_admin_maps_ref); |
---|
7295 | push(@a_addr, $a) if $a ne '' && !grep {$_ eq $a} @a_addr; |
---|
7296 | } |
---|
7297 | } |
---|
7298 | if (@q_addr) { # do the quarantining |
---|
7299 | # prepare header edits for the quarantined message |
---|
7300 | my($hdr_edits) = Amavis::Out::EditHeader->new; |
---|
7301 | $hdr_edits->prepend_header('X-Quarantine-Id', "<$VIRUSFILE>"); |
---|
7302 | if (@virusname) { |
---|
7303 | $hdr_edits->append_header('X-Amavis-Alert', |
---|
7304 | "INFECTED, message contains virus:\n " . join(",\n ", @virusname), 1); |
---|
7305 | } |
---|
7306 | if (@banned_filename) { |
---|
7307 | my(@b) = @banned_filename>3 ? @banned_filename[0..2] : @banned_filename; |
---|
7308 | my($msg) = "BANNED, message contains " |
---|
7309 | . (@banned_filename==1 ? 'part' : 'parts') . ":\n " |
---|
7310 | . join(",\n ", @b) . (@banned_filename > @b ? ", ..." : ""); |
---|
7311 | $hdr_edits->append_header('X-Amavis-Alert', $msg, 1); |
---|
7312 | } |
---|
7313 | if (@bad_headers) { |
---|
7314 | $hdr_edits->append_header('X-Amavis-Alert', |
---|
7315 | 'BAD HEADER '.$bad_headers[0], 1); |
---|
7316 | } |
---|
7317 | do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method, |
---|
7318 | @virusname ? 'QuarVirusMsgs' : |
---|
7319 | @banned_filename ? 'QuarBannedMsgs' : |
---|
7320 | @bad_headers ? 'QuarBadHMsgs' : 'QuarOther'); |
---|
7321 | } |
---|
7322 | my($hdr_edits) = Amavis::Out::EditHeader->new; |
---|
7323 | if (!@a_addr) { |
---|
7324 | do_log(4, "Skip admin notification, no administrators"); |
---|
7325 | } else { # notify per-recipient virus administrators |
---|
7326 | ll(5) && do_log(5, sprintf("DO_VIRUS - NOTIFICATIONS to %s; sender: %s", |
---|
7327 | join(",",qquote_rfc2821_local(@a_addr)), $msginfo->sender)); |
---|
7328 | my($notification) = Amavis::In::Message->new; |
---|
7329 | $notification->rx_time($msginfo->rx_time); # copy the reception time |
---|
7330 | $notification->delivery_method(c('notify_method')); |
---|
7331 | $notification->sender(c('mailfrom_notify_admin')); |
---|
7332 | $notification->auth_submitter( |
---|
7333 | qquote_rfc2821_local(c('mailfrom_notify_admin'))); |
---|
7334 | $notification->auth_user(c('amavis_auth_user')); |
---|
7335 | $notification->auth_pass(c('amavis_auth_pass')); |
---|
7336 | $notification->recips([@a_addr]); |
---|
7337 | my(%mybuiltins) = %builtins; # make a local copy |
---|
7338 | $mybuiltins{'T'} = \@a_addr; # used in 'To:' |
---|
7339 | $mybuiltins{'f'} = c('hdrfrom_notify_admin'); # From: |
---|
7340 | $notification->mail_text( |
---|
7341 | string_to_mime_entity(expand(cr('notify_virus_admin_templ'), |
---|
7342 | \%mybuiltins))); |
---|
7343 | $notification->header_edits($hdr_edits); |
---|
7344 | mail_dispatch($conn, $notification, 1); |
---|
7345 | my($n_smtp_resp, $n_exit_code, $n_dsn_needed) = |
---|
7346 | one_response_for_all($notification, 0, am_id()); # check status |
---|
7347 | if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok |
---|
7348 | } elsif ($n_smtp_resp =~ /^4/) { |
---|
7349 | die "temporarily unable to notify virus admin: $n_smtp_resp"; |
---|
7350 | } else { |
---|
7351 | do_log(-1, "FAILED to notify virus admin: $n_smtp_resp"); |
---|
7352 | } |
---|
7353 | # $notification->purge; |
---|
7354 | } |
---|
7355 | |
---|
7356 | # if (! defined($msginfo->sender_contact) ) { |
---|
7357 | # do_log(5,"do_virus: skip recipient notifications for unknown senders"); |
---|
7358 | # } else { # send notification to recipients |
---|
7359 | my(@recips_to_notify) = |
---|
7360 | grep { |
---|
7361 | @virusname && !lookup(0,$_,@{ca('bypass_virus_checks_maps')}) ? |
---|
7362 | scalar(lookup(0,$_,@{ca('warnvirusrecip_maps')})) |
---|
7363 | : @banned_filename && !lookup(0,$_,@{ca('bypass_banned_checks_maps')}) ? |
---|
7364 | scalar(lookup(0,$_,@{ca('warnbannedrecip_maps')})) |
---|
7365 | : @bad_headers && !lookup(0,$_,@{ca('bypass_header_checks_maps')}) ? |
---|
7366 | scalar(lookup(0,$_,@{ca('warnbadhrecip_maps')})) |
---|
7367 | : 0 } |
---|
7368 | grep { c('warn_offsite') || lookup(0,$_,@{ca('local_domains_maps')}) } |
---|
7369 | @{$msginfo->recips}; |
---|
7370 | if (!@recips_to_notify) { |
---|
7371 | do_log(5,"do_virus: recipient notifications not required"); |
---|
7372 | } else { |
---|
7373 | my($notification) = Amavis::In::Message->new; |
---|
7374 | $notification->rx_time($msginfo->rx_time); # copy the reception time |
---|
7375 | $notification->delivery_method(c('notify_method')); |
---|
7376 | $notification->sender(c('mailfrom_notify_recip')); |
---|
7377 | $notification->auth_submitter( |
---|
7378 | qquote_rfc2821_local(c('mailfrom_notify_recip'))); |
---|
7379 | $notification->auth_user(c('amavis_auth_user')); |
---|
7380 | $notification->auth_pass(c('amavis_auth_pass')); |
---|
7381 | $notification->recips(\@recips_to_notify); |
---|
7382 | my(%mybuiltins) = %builtins; # make a local copy |
---|
7383 | $mybuiltins{'f'} = c('hdrfrom_notify_recip'); # 'From:' |
---|
7384 | $mybuiltins{'T'} = (@recips_to_notify==1 && $recips_to_notify[0] ne '') ? # 'To:' |
---|
7385 | [quote_rfc2821_local($recips_to_notify[0])] : undef; |
---|
7386 | $notification->mail_text( |
---|
7387 | string_to_mime_entity(expand(cr('notify_virus_recips_templ'), |
---|
7388 | \%mybuiltins)) ); |
---|
7389 | $notification->header_edits($hdr_edits); |
---|
7390 | mail_dispatch($conn, $notification, 1); |
---|
7391 | my($n_smtp_resp, $n_exit_code, $n_dsn_needed) = |
---|
7392 | one_response_for_all($notification, 0, am_id()); # check status |
---|
7393 | if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok |
---|
7394 | } elsif ($n_smtp_resp =~ /^4/) { |
---|
7395 | die "temporarily unable to notify recipients: $n_smtp_resp"; |
---|
7396 | } else { |
---|
7397 | do_log(-1, "FAILED to notify recipients: $n_smtp_resp"); |
---|
7398 | } |
---|
7399 | # $notification->purge; |
---|
7400 | } |
---|
7401 | # } |
---|
7402 | do_log(5, "DO_VIRUS - DONE"); |
---|
7403 | } |
---|
7404 | |
---|
7405 | # |
---|
7406 | # If Spam found - quarantine it and log report |
---|
7407 | sub do_spam($$) { |
---|
7408 | my($conn, $msginfo) = @_; |
---|
7409 | # suggest a name to be used as 'X-Quarantine-Id:' or file name |
---|
7410 | my($q_method) = c('spam_quarantine_method'); |
---|
7411 | $VIRUSFILE = $q_method =~ /^(?:local|bsmtp):(.*)\z/si ? $1 : "spam-%b-%i-%n"; |
---|
7412 | $VIRUSFILE =~ s{%(.)} |
---|
7413 | { $1 eq 'b' ? $msginfo->body_digest |
---|
7414 | : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time)) |
---|
7415 | : $1 eq 'n' ? am_id() |
---|
7416 | : $1 eq '%' ? '%' : '%'.$1 }egs; |
---|
7417 | # use the smallest value as the level reported in quarantined headers! |
---|
7418 | my($tag_level) = |
---|
7419 | min(map { scalar(lookup(0,$_,@{ca('spam_tag_level_maps')})) } @{$msginfo->recips}); |
---|
7420 | my($tag2_level) = |
---|
7421 | min(map { scalar(lookup(0,$_,@{ca('spam_tag2_level_maps')})) } @{$msginfo->recips}); |
---|
7422 | my($kill_level) = |
---|
7423 | min(map { scalar(lookup(0,$_,@{ca('spam_kill_level_maps')})) } @{$msginfo->recips}); |
---|
7424 | my($blacklisted) = |
---|
7425 | scalar(grep { $_->recip_blacklisted_sender } @{$msginfo->per_recip_data}); |
---|
7426 | my($whitelisted) = |
---|
7427 | scalar(grep { $_->recip_whitelisted_sender } @{$msginfo->per_recip_data}); |
---|
7428 | my($s) = $spam_status; $s =~ s/,/,\n /g; # allow header field wrapping |
---|
7429 | my($full_spam_status) = sprintf( |
---|
7430 | "%s,\n hits=%s\n tag=%s\n tag2=%s\n kill=%s\n %s%s", |
---|
7431 | (defined $spam_level && defined $tag2_level && $spam_level>=$tag2_level ? |
---|
7432 | 'Yes' : 'No'), |
---|
7433 | (map { !defined $_ ? 'x' : 0+sprintf("%.3f",$_) } |
---|
7434 | ($spam_level, $tag_level, $tag2_level, $kill_level)), |
---|
7435 | join('', $blacklisted ? "BLACKLISTED\n " : (), |
---|
7436 | $whitelisted ? "WHITELISTED\n " : ()), |
---|
7437 | $s); |
---|
7438 | |
---|
7439 | do_log(5, "do_spam: looking for a quarantine address"); |
---|
7440 | my(@q_addr,@a_addr); # quarantine address(es) and administrators |
---|
7441 | my($sqbsm) = ca('spam_quarantine_bysender_to_maps'); |
---|
7442 | if (@$sqbsm) { # by-sender quarantine |
---|
7443 | my($a); $a = lookup(0,$msginfo->sender, @$sqbsm); |
---|
7444 | push(@q_addr, $a) if $a ne ''; |
---|
7445 | } |
---|
7446 | # get per-recipient quarantine address(es) and admins |
---|
7447 | for my $r (@{$msginfo->per_recip_data}) { |
---|
7448 | my($rec) = $r->recip_addr; |
---|
7449 | my($q); # quarantine (pseudo) address associated with the recipient |
---|
7450 | ($q) = lookup(0,$rec, @{ca('spam_quarantine_to_maps')}); |
---|
7451 | $q = $rec if $q ne '' && $q_method =~ /^bsmtp:/i; # orig.recip when BSMTP |
---|
7452 | my($a) = lookup(0,$rec, @{ca('spam_admin_maps')}); |
---|
7453 | push(@q_addr, $q) if $q ne '' && !grep {$_ eq $q} @q_addr; |
---|
7454 | push(@a_addr, $a) if $a ne '' && !grep {$_ eq $a} @a_addr; |
---|
7455 | } |
---|
7456 | if (@q_addr) { # do the quarantining |
---|
7457 | # prepare header edits for the quarantined message |
---|
7458 | my($hdr_edits) = Amavis::Out::EditHeader->new; |
---|
7459 | $hdr_edits->prepend_header('X-Quarantine-Id', "<$VIRUSFILE>"); |
---|
7460 | $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1); |
---|
7461 | # $hdr_edits->append_header('X-Spam-Score', |
---|
7462 | # !defined $spam_level ? 'x' : 0+sprintf("%.3f",$spam_level) ); |
---|
7463 | my($slc) = c('sa_spam_level_char'); |
---|
7464 | $hdr_edits->append_header('X-Spam-Level', |
---|
7465 | $slc x min(0+$spam_level,64)) if $slc ne ''; |
---|
7466 | $hdr_edits->append_header('X-Spam-Report', $spam_report,1) |
---|
7467 | if c('sa_spam_report_header') && $spam_report ne ''; |
---|
7468 | do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method,'QuarSpamMsgs'); |
---|
7469 | } |
---|
7470 | $s = $full_spam_status; $s =~ s/\n[ \t]/ /g; |
---|
7471 | do_log(2,sprintf("SPAM, <%s> -> %s, %s%s", $msginfo->sender_source, |
---|
7472 | join(',', qquote_rfc2821_local(@{$msginfo->recips})), $s, |
---|
7473 | !@q_addr ? '' : sprintf(", quarantine %s (%s)", |
---|
7474 | $VIRUSFILE, join(',', @q_addr)) )); |
---|
7475 | if (!@a_addr) { |
---|
7476 | do_log(4, "Skip spam admin notification, no administrators"); |
---|
7477 | } else { # notify per-recipient spam administrators |
---|
7478 | ll(5) && do_log(5, sprintf("DO_SPAM - NOTIFICATIONS to %s; sender: %s", |
---|
7479 | join(",",qquote_rfc2821_local(@a_addr)), $msginfo->sender)); |
---|
7480 | my($notification) = Amavis::In::Message->new; |
---|
7481 | $notification->rx_time($msginfo->rx_time); # copy the reception time |
---|
7482 | $notification->delivery_method(c('notify_method')); |
---|
7483 | $notification->sender(c('mailfrom_notify_spamadmin')); |
---|
7484 | $notification->auth_submitter( |
---|
7485 | qquote_rfc2821_local(c('mailfrom_notify_spamadmin'))); |
---|
7486 | $notification->auth_user(c('amavis_auth_user')); |
---|
7487 | $notification->auth_pass(c('amavis_auth_pass')); |
---|
7488 | $notification->recips([@a_addr]); |
---|
7489 | my(%mybuiltins) = %builtins; # make a local copy |
---|
7490 | $mybuiltins{'T'} = \@a_addr; # used in 'To:' |
---|
7491 | $mybuiltins{'f'} = c('hdrfrom_notify_spamadmin'); |
---|
7492 | $notification->mail_text( |
---|
7493 | string_to_mime_entity(expand(cr('notify_spam_admin_templ'), |
---|
7494 | \%mybuiltins))); |
---|
7495 | my($hdr_edits) = Amavis::Out::EditHeader->new; |
---|
7496 | $notification->header_edits($hdr_edits); |
---|
7497 | mail_dispatch($conn, $notification, 1); |
---|
7498 | my($n_smtp_resp, $n_exit_code, $n_dsn_needed) = |
---|
7499 | one_response_for_all($notification, 0, am_id()); # check status |
---|
7500 | if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) { # ok |
---|
7501 | } elsif ($n_smtp_resp =~ /^4/) { |
---|
7502 | die "temporarily unable to notify spam admin: $n_smtp_resp"; |
---|
7503 | } else { |
---|
7504 | do_log(-1, "FAILED to notify spam admin: $n_smtp_resp"); |
---|
7505 | } |
---|
7506 | # $notification->purge; |
---|
7507 | } |
---|
7508 | do_log(5, "DO_SPAM DONE"); |
---|
7509 | } |
---|
7510 | |
---|
7511 | # Calculate message digest; |
---|
7512 | # While at it, also get the message size and store original header, |
---|
7513 | # since we need it for the %H macro, and MIME::Tools may modify it. |
---|
7514 | |
---|
7515 | sub get_body_digest($$) { |
---|
7516 | my($fh, $msginfo) = @_; |
---|
7517 | $fh->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
7518 | local($_); |
---|
7519 | |
---|
7520 | # choose message digest method: |
---|
7521 | my($ctx) = Digest::MD5->new; # 128 bits (32 hex digits) |
---|
7522 | # my($ctx) = Digest::SHA1->new; # 160 bits (40 hex digits), slightly slower |
---|
7523 | |
---|
7524 | my(@orig_header); |
---|
7525 | my($header_size) = 0; |
---|
7526 | my($body_size) = 0; |
---|
7527 | while (<$fh>) { # skip mail header |
---|
7528 | last if $_ eq $eol; |
---|
7529 | $header_size += length($_); |
---|
7530 | push(@orig_header, $_); # with trailing EOL |
---|
7531 | } |
---|
7532 | my($len); |
---|
7533 | while (($len = read($fh,$_,16384)) > 0) |
---|
7534 | { $ctx->add($_); $body_size += $len } |
---|
7535 | my($signature) = $ctx->hexdigest; |
---|
7536 | # my($signature) = $ctx->b64digest; |
---|
7537 | $signature = untaint($signature) # checked (either 32 or 40 char) |
---|
7538 | if $signature =~ /^ [0-9a-fA-F]{32} (?: [0-9a-fA-F]{8} )? \z/x; |
---|
7539 | # store information obtained |
---|
7540 | $msginfo->orig_header(\@orig_header); |
---|
7541 | $msginfo->orig_header_size($header_size); |
---|
7542 | $msginfo->orig_body_size($body_size); |
---|
7543 | $msginfo->body_digest($signature); |
---|
7544 | |
---|
7545 | section_time('body_hash'); |
---|
7546 | do_log(3, "body hash: $signature"); |
---|
7547 | $signature; |
---|
7548 | } |
---|
7549 | |
---|
7550 | sub find_program_path($$$) { |
---|
7551 | my($fv_list, $path_list_ref, $may_log) = @_; |
---|
7552 | $fv_list = [$fv_list] if !ref $fv_list; |
---|
7553 | my($found); |
---|
7554 | for my $fv (@$fv_list) { |
---|
7555 | my(@fv_cmd) = split(' ',$fv); |
---|
7556 | if (!@fv_cmd) { # empty, not available |
---|
7557 | } elsif ($fv_cmd[0] =~ /^\//) { # absolute path |
---|
7558 | my($errn) = stat($fv_cmd[0]) ? 0 : 0+$!; |
---|
7559 | if ($errn == ENOENT) { } |
---|
7560 | elsif ($errn) { |
---|
7561 | do_log(-1, "find_program_path: " . "$fv_cmd[0] inaccessible: $!") |
---|
7562 | if $may_log; |
---|
7563 | } elsif (-x _ && !-d _) { $found = join(' ', @fv_cmd) } |
---|
7564 | } elsif ($fv_cmd[0] =~ /\//) { # relative path |
---|
7565 | die "find_program_path: relative paths not implemented: @fv_cmd\n"; |
---|
7566 | } else { # walk through the specified PATH |
---|
7567 | for my $p (@$path_list_ref) { |
---|
7568 | my($errn) = stat("$p/$fv_cmd[0]") ? 0 : 0+$!; |
---|
7569 | if ($errn == ENOENT) { } |
---|
7570 | elsif ($errn) { |
---|
7571 | do_log(-1, "find_program_path: " . "$p/$fv_cmd[0] inaccessible: $!") |
---|
7572 | if $may_log; |
---|
7573 | } elsif (-x _ && !-d _) { |
---|
7574 | $found = $p . '/' . join(' ', @fv_cmd); |
---|
7575 | last; |
---|
7576 | } |
---|
7577 | } |
---|
7578 | } |
---|
7579 | last if defined $found; |
---|
7580 | } |
---|
7581 | $found; |
---|
7582 | } |
---|
7583 | |
---|
7584 | sub find_external_programs($) { |
---|
7585 | my($path_list_ref) = @_; |
---|
7586 | for my $f (qw($file $arc $gzip $bzip2 $lzop $lha $unarj $uncompress |
---|
7587 | $unfreeze $unrar $zoo $pax $cpio $ar $rpm2cpio $cabextract |
---|
7588 | $ripole $dspam)) |
---|
7589 | { |
---|
7590 | my($g) = $f; |
---|
7591 | $g =~ s/\$/Amavis::Conf::/; |
---|
7592 | my($fv_list) = eval('$' . $g); |
---|
7593 | my($found) = find_program_path($fv_list, $path_list_ref, 1); |
---|
7594 | { no strict 'refs'; $$g = $found } # NOTE: a symbolic reference |
---|
7595 | if (!defined $found) { |
---|
7596 | do_log(-1, sprintf("No %-14s not using it", "$f,")); |
---|
7597 | } else { |
---|
7598 | do_log(0,sprintf("Found %-11s at %s%s", $f, |
---|
7599 | $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '', |
---|
7600 | $found)); |
---|
7601 | } |
---|
7602 | } |
---|
7603 | # map program name hints to full paths |
---|
7604 | my($tier) = 'primary'; # primary, secondary, ... av scanners |
---|
7605 | for my $f (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) { |
---|
7606 | if ($f eq "\000") { # next tier |
---|
7607 | $tier = 'secondary'; |
---|
7608 | } elsif (!defined $f || !ref $f) { # empty, skip |
---|
7609 | } elsif (ref($f->[1]) eq 'CODE') { |
---|
7610 | do_log(0, "Using internal av scanner code for ($tier) " . $f->[0]); |
---|
7611 | } else { |
---|
7612 | my($found) = $f->[1] = find_program_path($f->[1], $path_list_ref, 1); |
---|
7613 | if (!defined $found) { |
---|
7614 | do_log(3, "No $tier av scanner: " . $f->[0]); |
---|
7615 | $f = undef; # release its storage |
---|
7616 | } else { |
---|
7617 | do_log(0,sprintf("Found $tier av scanner %-11s at %s%s", $f->[0], |
---|
7618 | $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '', |
---|
7619 | $found)); |
---|
7620 | } |
---|
7621 | } |
---|
7622 | } |
---|
7623 | } |
---|
7624 | |
---|
7625 | # Fetch all remaining modules. |
---|
7626 | sub fetch_modules_extra() { |
---|
7627 | my(@modules); |
---|
7628 | if ($extra_code_sql) { |
---|
7629 | push(@modules, 'DBI'); |
---|
7630 | for (@lookup_sql_dsn) { |
---|
7631 | my(@dsn) = split(/:/,$_->[0],-1); |
---|
7632 | push(@modules, 'DBD::'.$dsn[1]) if uc($dsn[0]) eq 'DBI'; |
---|
7633 | } |
---|
7634 | } |
---|
7635 | push(@modules, qw(Net::LDAP Net::LDAP::Util Net::LDAP::Search)) |
---|
7636 | if $extra_code_ldap; |
---|
7637 | if (c('bypass_decode_parts') && |
---|
7638 | !grep {exists $policy_bank{$_}{'bypass_decode_parts'} && |
---|
7639 | !$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) { |
---|
7640 | } else { |
---|
7641 | push(@modules, qw(Compress::Zlib Convert::TNEF Convert::UUlib |
---|
7642 | Archive::Zip Archive::Tar)); |
---|
7643 | } |
---|
7644 | push(@modules, 'Mail::SpamAssassin') if $extra_code_antispam; |
---|
7645 | push(@modules, 'Authen::SASL') if c('auth_required_out'); |
---|
7646 | Amavis::Boot::fetch_modules('REQUIRED ADDITIONAL MODULES', 1, @modules); |
---|
7647 | my($sa_version); |
---|
7648 | $sa_version = Mail::SpamAssassin::Version() if $extra_code_antispam; |
---|
7649 | @modules = (); # now start collecting optional modules |
---|
7650 | if ($unicode_aware) { |
---|
7651 | push(@modules, qw( |
---|
7652 | bytes bytes_heavy.pl utf8 utf8_heavy.pl Encode Encode::Byte |
---|
7653 | unicore::Canonical.pl unicore::Exact.pl unicore::PVA.pl |
---|
7654 | unicore::To::Fold.pl unicore::To::Title.pl |
---|
7655 | unicore::To::Lower.pl unicore::To::Upper.pl |
---|
7656 | )); |
---|
7657 | } |
---|
7658 | if ($extra_code_antispam) { # must be loaded before chroot takes place |
---|
7659 | push(@modules, qw( |
---|
7660 | Mail::SpamAssassin::Locker::Flock |
---|
7661 | Mail::SpamAssassin::Locker::UnixNFSSafe |
---|
7662 | Mail::SpamAssassin::DBBasedAddrList |
---|
7663 | Mail::SpamAssassin::SQLBasedAddrList |
---|
7664 | Mail::SpamAssassin::BayesStore::DBM |
---|
7665 | Mail::SpamAssassin::BayesStore::SQL |
---|
7666 | Mail::SpamAssassin::Plugin::SPF |
---|
7667 | Mail::SpamAssassin::Plugin::URIDNSBL |
---|
7668 | Mail::SpamAssassin::Plugin::Hashcash |
---|
7669 | Mail::SpamAssassin::PerMsgLearner |
---|
7670 | |
---|
7671 | DBD::mysql Sys::Hostname::Long |
---|
7672 | Mail::SPF::Query Razor2::Client Net::CIDR::Lite |
---|
7673 | Net::DNS::RR::SOA Net::DNS::RR::NS Net::DNS::RR::MX |
---|
7674 | Net::DNS::RR::A Net::DNS::RR::AAAA Net::DNS::RR::PTR |
---|
7675 | Net::DNS::RR::CNAME Net::DNS::RR::TXT Net::Ping |
---|
7676 | )); |
---|
7677 | # ??? ArchiveIterator Reporter Plugin::RelayCountry |
---|
7678 | # ??? Mail::SpamAssassin::Plugin::Razor2 |
---|
7679 | } |
---|
7680 | # *** note that $sa_version could be 3.0.1, which is not really numeric! |
---|
7681 | if ($extra_code_antispam && defined $sa_version && $sa_version < 3) { |
---|
7682 | push(@modules, qw( |
---|
7683 | Mail::SpamAssassin::UnixLocker Mail::SpamAssassin::BayesStoreDBM |
---|
7684 | Mail::SpamAssassin::SpamCopURI |
---|
7685 | URI URI::Escape URI::Heuristic URI::QueryParam URI::Split URI::URL |
---|
7686 | URI::WithBase URI::_foreign URI::_generic URI::_ldap URI::_login |
---|
7687 | URI::_query URI::_segment URI::_server URI::_userpass URI::data URI::ftp |
---|
7688 | URI::gopher URI::http URI::https URI::ldap URI::ldapi URI::ldaps |
---|
7689 | URI::mailto URI::mms URI::news URI::nntp URI::pop URI::rlogin URI::rsync |
---|
7690 | URI::rtsp URI::rtspu URI::sip URI::sips URI::snews URI::ssh URI::telnet |
---|
7691 | URI::tn3270 URI::urn URI::urn::isbn URI::urn::oid |
---|
7692 | URI::file URI::file::Base URI::file::Unix URI::file::Win32 |
---|
7693 | )); |
---|
7694 | } |
---|
7695 | my($missing); |
---|
7696 | $missing = Amavis::Boot::fetch_modules('PRE-COMPILE OPTIONAL MODULES', 0, |
---|
7697 | @modules) if @modules; |
---|
7698 | do_log(2, 'INFO: no optional modules: '.join(' ',@$missing)) |
---|
7699 | if ref $missing && @$missing; |
---|
7700 | # load optional modules SAVI and Mail::ClamAV if available and requested |
---|
7701 | if ($extra_code_antivirus) { |
---|
7702 | my($savi_obj, $savi_module_ok, $clamav_module_ok); |
---|
7703 | for my $entry (@{ca('av_scanners')}, @{ca('av_scanners_backup')}) { |
---|
7704 | if (ref($entry) ne 'ARRAY') { # none |
---|
7705 | } elsif ($entry->[1] eq \&ask_sophos_savi || |
---|
7706 | $entry->[1] eq \&sophos_savi || |
---|
7707 | $entry->[0] eq 'Sophos SAVI') { |
---|
7708 | if (!defined($savi_module_ok)) { |
---|
7709 | $savi_module_ok = eval { require SAVI }; |
---|
7710 | $savi_module_ok = 0 if !defined $savi_module_ok; |
---|
7711 | # |
---|
7712 | # if the Amavis::AV::sophos_savi_init() in the following line _is_ |
---|
7713 | # called from here, SAVI-Perl will only initialize once during startup; |
---|
7714 | # otherwise (if left uninitialized here) each child process will do the |
---|
7715 | # SAVI-Perl initialization at its birth, which costs some time, but avoids |
---|
7716 | # the need to restart amavisd when IDE database changes: |
---|
7717 | ### $savi_obj = Amavis::AV::sophos_savi_init(@$entry) if $savi_module_ok; |
---|
7718 | |
---|
7719 | } |
---|
7720 | if (!$savi_module_ok) { $entry->[1] = undef } # disable entry |
---|
7721 | else { $entry->[2] = $savi_obj if defined $savi_obj } # save as args |
---|
7722 | } elsif ($entry->[1] eq \&ask_clamav || |
---|
7723 | $entry->[0] =~ /^Mail::ClamAV/) { |
---|
7724 | if (!defined($clamav_module_ok)) { |
---|
7725 | $clamav_module_ok = eval { require Mail::ClamAV }; |
---|
7726 | $clamav_module_ok = 0 if !defined $clamav_module_ok; |
---|
7727 | } |
---|
7728 | $entry->[1] = undef if !$clamav_module_ok; # disable entry |
---|
7729 | } |
---|
7730 | } |
---|
7731 | } |
---|
7732 | } |
---|
7733 | |
---|
7734 | # |
---|
7735 | # Main program starts here |
---|
7736 | # |
---|
7737 | |
---|
7738 | # Read dynamic source code, and logging and notification message templates |
---|
7739 | # from the end of this file (pseudo file handle DATA) |
---|
7740 | # |
---|
7741 | $Amavis::Conf::notify_spam_admin_templ = ''; # not used |
---|
7742 | $Amavis::Conf::notify_spam_recips_templ = ''; # not used |
---|
7743 | do { local($/) = "__DATA__\n"; # set line terminator to this string |
---|
7744 | chomp($_ = <Amavis::DATA>) for ( |
---|
7745 | $extra_code_db, $extra_code_cache, |
---|
7746 | $extra_code_sql, $extra_code_ldap, |
---|
7747 | $extra_code_in_amcl, $extra_code_in_smtp, $extra_code_in_qmqpqq, |
---|
7748 | $extra_code_antivirus, $extra_code_antispam, $extra_code_unpackers, |
---|
7749 | $Amavis::Conf::log_templ, $Amavis::Conf::log_recip_templ); |
---|
7750 | if ($unicode_aware) { |
---|
7751 | # binmode(\*Amavis::DATA, ":encoding(utf8)") # :encoding(iso-8859-1) |
---|
7752 | # or die "Can't set \*DATA encoding: $!"; |
---|
7753 | } |
---|
7754 | chomp($_ = <Amavis::DATA>) for ( |
---|
7755 | $Amavis::Conf::notify_sender_templ, |
---|
7756 | $Amavis::Conf::notify_virus_sender_templ, |
---|
7757 | $Amavis::Conf::notify_virus_admin_templ, |
---|
7758 | $Amavis::Conf::notify_virus_recips_templ, |
---|
7759 | $Amavis::Conf::notify_spam_sender_templ, |
---|
7760 | $Amavis::Conf::notify_spam_admin_templ ); |
---|
7761 | }; # restore line terminator |
---|
7762 | close(\*Amavis::DATA) or die "Can't close *Amavis::DATA: $!"; |
---|
7763 | # close(STDIN) or die "Can't close STDIN: $!"; |
---|
7764 | # note: don't close STDIN just yet to prevent some other file taking up fd 0 |
---|
7765 | |
---|
7766 | # discard trailing NL |
---|
7767 | $Amavis::Conf::log_templ = $1 |
---|
7768 | if $Amavis::Conf::log_templ=~/^(.*?)[\r\n]+\z/s; |
---|
7769 | $Amavis::Conf::log_recip_templ = $1 |
---|
7770 | if $Amavis::Conf::log_recip_templ=~/^(.*?)[\r\n]+\z/s; |
---|
7771 | |
---|
7772 | # Consider droping privileges early, before reading config file. |
---|
7773 | # This is only possible if running under chroot will not be needed. |
---|
7774 | # |
---|
7775 | my($desired_group); # defaults to $desired_user's group |
---|
7776 | my($desired_user); # username or UID |
---|
7777 | if ($> != 0) { $desired_user = $> } # use effective UID if not root |
---|
7778 | #else { |
---|
7779 | # for my $u ('amavis', 'vscan') { # try to guess a good default username |
---|
7780 | # my($username,$passwd,$uid,$gid) = getpwnam($u); |
---|
7781 | # if (defined $uid && $uid != 0) { $desired_user = $u; last } |
---|
7782 | # } |
---|
7783 | #} |
---|
7784 | |
---|
7785 | # collect and parse command line options |
---|
7786 | while (@ARGV >= 2 && $ARGV[0] =~ /^-[ugc]\z/) { |
---|
7787 | my($opt) = shift @ARGV; |
---|
7788 | if ($opt eq '-u') { # -u username |
---|
7789 | my($val) = shift @ARGV; |
---|
7790 | if ($> == 0) { $desired_user = $val } |
---|
7791 | else { print STDERR "Ignoring option -u when not running as root\n" } |
---|
7792 | } elsif ($opt eq '-g') { # -g group |
---|
7793 | my($val) = shift @ARGV; |
---|
7794 | if ($> == 0) { $desired_group = $val } |
---|
7795 | else { print STDERR "Ignoring option -g when not running as root\n" } |
---|
7796 | } elsif ($opt eq '-c') { # -c config_file |
---|
7797 | push(@config_files, untaint(shift @ARGV)); |
---|
7798 | } |
---|
7799 | } |
---|
7800 | |
---|
7801 | if (defined $desired_user && ($> == 0 || $< == 0)) { # drop privileges early |
---|
7802 | my($username,$passwd,$uid,$gid) = |
---|
7803 | $desired_user=~/^(\d+)$/ ? (undef,undef,$1,undef) :getpwnam($desired_user); |
---|
7804 | defined $uid or die "No such username: $desired_user\n"; |
---|
7805 | if ($desired_group eq '') { $desired_group = $gid } # for logging purposes |
---|
7806 | else { $gid = $desired_group=~/^(\d+)$/ ? $1 : getgrnam($desired_group) } |
---|
7807 | defined $gid or die "No such group: $desired_group\n"; |
---|
7808 | $( = $gid; # real GID |
---|
7809 | $) = "$gid $gid"; # effective GID |
---|
7810 | POSIX::setuid($uid) or die "Can't setuid to $uid: $!"; |
---|
7811 | $> = $uid; $< = $uid; # just in case |
---|
7812 | # print STDERR "desired user=$desired_user ($uid), current: EUID: $> ($<)\n"; |
---|
7813 | # print STDERR "desired group=$desired_group, current: EGID: $) ($()\n"; |
---|
7814 | $> != 0 or die "Still running as root, aborting\n"; |
---|
7815 | $< != 0 or die "Effective UID changed, but Real UID is 0\n"; |
---|
7816 | } |
---|
7817 | |
---|
7818 | umask(0027); |
---|
7819 | |
---|
7820 | # default location of the config file if none specified |
---|
7821 | push(@config_files, '/etc/amavisd.conf') if !@config_files; |
---|
7822 | |
---|
7823 | # Read config file, which may override default settings |
---|
7824 | Amavis::Conf::build_default_maps(); |
---|
7825 | Amavis::Conf::read_config(@config_files); |
---|
7826 | # chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
7827 | |
---|
7828 | if (defined $desired_user && $daemon_user ne '') { |
---|
7829 | # compare the config file settings to current UID |
---|
7830 | my($username,$passwd,$uid,$gid) = |
---|
7831 | $daemon_user=~/^(\d+)$/ ? (undef,undef,$1,undef) : getpwnam($daemon_user); |
---|
7832 | $uid == $> or warn sprintf( |
---|
7833 | "WARN: running under user '%s' (UID=%s), the config file". |
---|
7834 | " specifies \$daemon_user='%s' (UID=%s)\n", |
---|
7835 | $desired_user, $>, $daemon_user, defined $uid ? $uid : '?'); |
---|
7836 | } |
---|
7837 | |
---|
7838 | # compile optional modules if needed |
---|
7839 | # %modules_basic = %INC; # helps to track missing modules in chroot |
---|
7840 | if (!$enable_db) { $extra_code_db = undef } |
---|
7841 | else { |
---|
7842 | eval $extra_code_db or die "Problem in Amavis::DB or Amavis::DB::SNMP code: $@"; |
---|
7843 | $extra_code_db = 1; # release memory occupied by the source code |
---|
7844 | } |
---|
7845 | if (!$enable_global_cache || !$extra_code_db) { $extra_code_cache = undef } |
---|
7846 | else { |
---|
7847 | eval $extra_code_cache or die "Problem in the Amavis::Cache code: $@"; |
---|
7848 | $extra_code_cache = 1; # release memory occupied by the source code |
---|
7849 | } |
---|
7850 | if (!@lookup_sql_dsn) { $extra_code_sql = undef } |
---|
7851 | else { |
---|
7852 | eval $extra_code_sql or die "Problem in the Lookup::SQL code: $@"; |
---|
7853 | $extra_code_sql = 1; # release memory occupied by the source code |
---|
7854 | } |
---|
7855 | if (!$enable_ldap) { $extra_code_ldap = undef } |
---|
7856 | else { |
---|
7857 | eval $extra_code_ldap or die "Problem in the Lookup::LDAP code: $@"; |
---|
7858 | $extra_code_ldap = 1; # release memory occupied by the source code |
---|
7859 | } |
---|
7860 | |
---|
7861 | if (c('protocol') eq 'COURIER') { |
---|
7862 | die "In::Courier code not available"; |
---|
7863 | } elsif (c('protocol') eq 'AM.PDP' || $unix_socketname ne '') { |
---|
7864 | eval $extra_code_in_amcl or die "Problem in the In::AMCL code: $@"; |
---|
7865 | $extra_code_in_amcl = 1; # release memory occupied by the source code |
---|
7866 | } else { |
---|
7867 | $extra_code_in_amcl = undef; |
---|
7868 | } |
---|
7869 | |
---|
7870 | if (c('protocol') eq 'QMQPqq') { # simpleminded, not checking all policy banks |
---|
7871 | eval $extra_code_in_qmqpqq or die "Problem in the In::QMQPqq code: $@"; |
---|
7872 | $extra_code_in_qmqpqq = 1; # release memory occupied by the source code |
---|
7873 | $extra_code_in_smtp = undef; |
---|
7874 | } elsif (c('protocol') =~ /^(SMTP|LMTP)\z/ || |
---|
7875 | $inet_socket_port ne '' && |
---|
7876 | (!ref $inet_socket_port || @$inet_socket_port)) { # assume SMTP/LMTP |
---|
7877 | eval $extra_code_in_smtp or die "Problem in the In::SMTP code: $@"; |
---|
7878 | $extra_code_in_smtp = 1; # release memory occupied by the source code |
---|
7879 | $extra_code_in_qmqpqq = undef; |
---|
7880 | } else { |
---|
7881 | $extra_code_in_smtp = undef; |
---|
7882 | $extra_code_in_qmqpqq = undef; |
---|
7883 | } |
---|
7884 | |
---|
7885 | my($bpvcm) = ca('bypass_virus_checks_maps'); |
---|
7886 | if (!@{ca('av_scanners')} && !@{ca('av_scanners_backup')}) { |
---|
7887 | $extra_code_antivirus = undef; |
---|
7888 | } elsif (@$bpvcm && !ref($bpvcm->[0]) && $bpvcm->[0]) { |
---|
7889 | # do a simple-minded test to make it easy to turn off virus checks |
---|
7890 | $extra_code_antivirus = undef; |
---|
7891 | } else { |
---|
7892 | eval $extra_code_antivirus or die "Problem in the antivirus code: $@"; |
---|
7893 | $extra_code_antivirus = 1; # release memory occupied by the source code |
---|
7894 | } |
---|
7895 | if (!$extra_code_antivirus) # release storage |
---|
7896 | { @Amavis::Conf::av_scanners = @Amavis::Conf::av_scanners_backup = () } |
---|
7897 | |
---|
7898 | my($bpscm) = ca('bypass_spam_checks_maps'); |
---|
7899 | if (@$bpscm && !ref($bpscm->[0]) && $bpscm->[0]) { |
---|
7900 | # do a simple-minded test to make it easy to turn off spam checks |
---|
7901 | $extra_code_antispam = undef; |
---|
7902 | } else { |
---|
7903 | eval $extra_code_antispam or die "Problem in the antispam code: $@"; |
---|
7904 | $extra_code_antispam = 1; # release memory occupied by the source code |
---|
7905 | } |
---|
7906 | |
---|
7907 | if (c('bypass_decode_parts') && |
---|
7908 | !grep {exists $policy_bank{$_}{'bypass_decode_parts'} && |
---|
7909 | !$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) { |
---|
7910 | $extra_code_unpackers = undef; |
---|
7911 | } else { |
---|
7912 | eval $extra_code_unpackers or die "Problem in the Amavis::Unpackers code: $@"; |
---|
7913 | $extra_code_unpackers = 1; # release memory occupied by the source code |
---|
7914 | } |
---|
7915 | |
---|
7916 | # act on command line parameters |
---|
7917 | my($cmd) = lc($ARGV[0]); |
---|
7918 | if ($cmd =~ /^(start|debug|debug-sa|foreground)?\z/) { |
---|
7919 | $DEBUG=1 if $cmd eq 'debug'; |
---|
7920 | $daemonize=0 if $cmd eq 'foreground'; |
---|
7921 | $daemonize=0, $sa_debug=1 if $cmd eq 'debug-sa'; |
---|
7922 | } elsif ($cmd !~ /^(reload|stop)\z/) { |
---|
7923 | die "$myversion: Unknown argument. Usage:\n $0 [-u user] [-g group] [-c config-file] ( [start] | stop | reload | debug | debug-sa | foreground )\n"; |
---|
7924 | } else { # stop or reload |
---|
7925 | eval { # first stop a running daemon |
---|
7926 | $pid_file ne '' or die "Config parameter \$pid_file not defined"; |
---|
7927 | my($errn) = stat($pid_file) ? 0 : 0+$!; |
---|
7928 | $errn != ENOENT or die "No PID file $pid_file\n"; |
---|
7929 | $errn == 0 or die "PID file $pid_file inaccessible: $!"; |
---|
7930 | my($amavisd_pid); |
---|
7931 | open(PID_FILE, "< $pid_file\0") or die "Can't read file $pid_file: $!"; |
---|
7932 | while (<PID_FILE>) { chomp; $amavisd_pid = $_ if /^\d+\z/ } |
---|
7933 | close(PID_FILE) or die "Can't close file $pid_file: $!"; |
---|
7934 | defined($amavisd_pid) or die "Invalid PID in the $pid_file"; |
---|
7935 | $amavisd_pid = untaint($amavisd_pid); |
---|
7936 | kill('TERM',$amavisd_pid) or die "Can't SIGTERM amavisd[$amavisd_pid]: $!"; |
---|
7937 | my($delay) = 1; # seconds |
---|
7938 | for (;;) { |
---|
7939 | sleep($delay); $delay = 5; |
---|
7940 | last if !kill(0,$amavisd_pid); # is the old daemon still there? |
---|
7941 | print STDERR "Waiting for the process $amavisd_pid to terminate\n"; |
---|
7942 | } |
---|
7943 | }; |
---|
7944 | if ($@ ne '') { chomp($@); die "$@, can't $cmd the process\n" } |
---|
7945 | exit 0 if $cmd eq 'stop'; |
---|
7946 | print STDERR "daemon terminated, waiting for the dust to settle...\n"; |
---|
7947 | sleep 5; # wait for the TCP socket to be released |
---|
7948 | print STDERR "becoming a new daemon...\n"; |
---|
7949 | } |
---|
7950 | $daemonize = 0 if $DEBUG; |
---|
7951 | |
---|
7952 | # Set path, home and term explictly. Don't trust environment |
---|
7953 | $ENV{PATH} = $path if $path ne ''; |
---|
7954 | $ENV{HOME} = $helpers_home if $helpers_home ne ''; |
---|
7955 | $ENV{TERM} = 'dumb'; $ENV{COLUMNS} = '80'; $ENV{LINES} = '100'; |
---|
7956 | |
---|
7957 | Amavis::Log::init("amavis", !$daemonize, $DO_SYSLOG, $SYSLOG_LEVEL, $LOGFILE); |
---|
7958 | |
---|
7959 | # report version of Perl and process UID |
---|
7960 | do_log(1, "user=$desired_user, EUID: $> ($<); group=$desired_group, EGID: $) ($()"); |
---|
7961 | do_log(0, "Perl version $]"); |
---|
7962 | |
---|
7963 | # $SIG{USR2} = sub { |
---|
7964 | # my($msg) = Carp::longmess("SIG$_[0] received, backtrace:"); |
---|
7965 | # print STDERR "\n",$msg,"\n"; do_log(-1,$msg); |
---|
7966 | # }; |
---|
7967 | |
---|
7968 | fetch_modules_extra(); # bring additional modules into memory and compile them |
---|
7969 | |
---|
7970 | # set up Net::Server configuration |
---|
7971 | my $server = bless { |
---|
7972 | server => { |
---|
7973 | # command args to be used after HUP must be untainted, deflt: [$0,@ARGV] |
---|
7974 | # commandline => ['/usr/local/sbin/amavisd','-c',$config_file[0] ], |
---|
7975 | commandline => [], # disable |
---|
7976 | |
---|
7977 | # listen on the following sockets (one or more): |
---|
7978 | port => [ ($unix_socketname eq '' ? () : "$unix_socketname|unix"), # helper |
---|
7979 | map { "$_/tcp" } # accept SMTP on this port(s) |
---|
7980 | (ref $inet_socket_port ? @$inet_socket_port |
---|
7981 | : $inet_socket_port ne '' ? $inet_socket_port : () ), |
---|
7982 | ], |
---|
7983 | # limit socket bind (e.g. to the loopback interface) |
---|
7984 | host => ($inet_socket_bind eq '' ? '*' : $inet_socket_bind), |
---|
7985 | |
---|
7986 | max_servers => $max_servers, # number of pre-forked children |
---|
7987 | max_requests => $max_requests, # restart child after that many accept's |
---|
7988 | user => (($> == 0 || $< == 0) ? $daemon_user : undef), |
---|
7989 | group => (($> == 0 || $< == 0) ? $daemon_group : undef), |
---|
7990 | pid_file => $pid_file, |
---|
7991 | lock_file => $lock_file, # serialization lockfile |
---|
7992 | # serialize => 'flock', # flock, semaphore, pipe |
---|
7993 | background => $daemonize ? 1 : undef, |
---|
7994 | setsid => $daemonize ? 1 : undef, |
---|
7995 | chroot => $daemon_chroot_dir ne '' ? $daemon_chroot_dir : undef, |
---|
7996 | no_close_by_child => 1, |
---|
7997 | |
---|
7998 | # controls log level for Net::Server internal log messages: |
---|
7999 | # 0=err, 1=warning, 2=notice, 3=info, 4=debug |
---|
8000 | log_level => ($DEBUG ? 4 : 2), |
---|
8001 | log_file => undef, # will be overridden to call do_log() |
---|
8002 | }, |
---|
8003 | }, 'Amavis'; |
---|
8004 | |
---|
8005 | $0 = 'amavisd (master)'; |
---|
8006 | $server->run; # transfer control to Net::Server |
---|
8007 | |
---|
8008 | # shouldn't get here |
---|
8009 | exit 1; |
---|
8010 | |
---|
8011 | # we read text (especially notification templates) from DATA sections |
---|
8012 | # to avoid any interpretations of special characters (e.g. \ or ') by Perl |
---|
8013 | # |
---|
8014 | |
---|
8015 | __DATA__ |
---|
8016 | # |
---|
8017 | package Amavis::DB::SNMP; |
---|
8018 | use strict; |
---|
8019 | use re 'taint'; |
---|
8020 | |
---|
8021 | BEGIN { |
---|
8022 | import Amavis::Conf qw($myversion $myhostname); |
---|
8023 | import Amavis::Util qw(ll do_log snmp_counters_get); |
---|
8024 | } |
---|
8025 | |
---|
8026 | use BerkeleyDB; |
---|
8027 | |
---|
8028 | BEGIN { |
---|
8029 | use Exporter (); |
---|
8030 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
8031 | $VERSION = '2.034'; |
---|
8032 | @ISA = qw(Exporter); |
---|
8033 | } |
---|
8034 | |
---|
8035 | # open existing databases (called by each child process) |
---|
8036 | sub new { |
---|
8037 | my($class,$db_env) = @_; my($env) = $db_env->get_db_env; |
---|
8038 | defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!."; |
---|
8039 | my($dbs) = BerkeleyDB::Hash->new(-Filename=>'snmp.db', -Env=>$env); |
---|
8040 | defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!."; |
---|
8041 | my($dbn) = BerkeleyDB::Hash->new(-Filename=>'nanny.db', -Env=>$env); |
---|
8042 | defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!."; |
---|
8043 | bless { 'db_snmp'=>$dbs, 'db_nanny'=>$dbn }, $class; |
---|
8044 | } |
---|
8045 | |
---|
8046 | sub DESTROY { |
---|
8047 | my($self) = shift; |
---|
8048 | eval { do_log(5,"Amavis::DB::SNMP called") }; |
---|
8049 | for my $db ($self->{'db_snmp'}, $self->{'db_nanny'}) { |
---|
8050 | if (defined $db) { |
---|
8051 | eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." }; |
---|
8052 | if ($@ ne '') { warn "BDB S+N DESTROY $@" } |
---|
8053 | $db = undef; |
---|
8054 | } |
---|
8055 | } |
---|
8056 | } |
---|
8057 | |
---|
8058 | #sub lock_stat($) { |
---|
8059 | # my($label) = @_; |
---|
8060 | # my($s) = qx'/usr/local/bin/db_stat-4.2 -c -h /var/amavis/db | /usr/local/bin/perl -ne \'$a{$2}=$1 if /^(\d+)\s+Total number of locks (requested|released)/; END {printf("%d, %d\n",$a{requested}, $a{requested}-$a{released})}\''; |
---|
8061 | # do_log(0, "lock_stat $label: $s"); |
---|
8062 | #} |
---|
8063 | |
---|
8064 | # insert startup time SNMP entry, called from the master process at startup |
---|
8065 | # (a classical subroutine, not a method) |
---|
8066 | sub put_initial_snmp_data($) { |
---|
8067 | my($db) = @_; |
---|
8068 | my($cursor) = $db->db_cursor(DB_WRITECURSOR); |
---|
8069 | defined $cursor or die "BDB S db_cursor: $BerkeleyDB::Error, $!."; |
---|
8070 | for my $obj (['sysDescr', 'STR', $myversion], |
---|
8071 | ['sysObjectID', 'OID', '1.3.6.1.4.1.15312.2.1'], |
---|
8072 | # iso.org.dod.internet.private.enterprise.ijs.amavisd-new.snmp |
---|
8073 | ['sysUpTime', 'INT', int(time)], |
---|
8074 | # later it must be converted to timeticks (10ms since start) |
---|
8075 | ['sysContact', 'STR', ''], |
---|
8076 | ['sysName', 'STR', $myhostname], |
---|
8077 | ['sysLocation', 'STR', ''], |
---|
8078 | ['sysServices', 'INT', 64], # application |
---|
8079 | ) { |
---|
8080 | my($key,$type,$val) = @$obj; |
---|
8081 | $cursor->c_put($key, sprintf("%s %s",$type,$val), DB_KEYLAST) == 0 |
---|
8082 | or die "BDB S c_put: $BerkeleyDB::Error, $!."; |
---|
8083 | }; |
---|
8084 | $cursor->c_close==0 or die "BDB S c_close: $BerkeleyDB::Error, $!."; |
---|
8085 | } |
---|
8086 | |
---|
8087 | sub update_counters { |
---|
8088 | my($self) = @_; |
---|
8089 | my($counter_names_ref) = snmp_counters_get(); |
---|
8090 | my($eval_stat,$interrupt); $interrupt = ''; |
---|
8091 | if (defined $counter_names_ref && @$counter_names_ref) { |
---|
8092 | my($db) = $self->{'db_snmp'}; my($cursor); |
---|
8093 | my($h1) = sub { $interrupt = $_[0] }; |
---|
8094 | local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8; |
---|
8095 | eval { # ensure cursor will be unlocked even in case of errors or signals |
---|
8096 | $cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock |
---|
8097 | defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!."; |
---|
8098 | for my $key (@$counter_names_ref) { |
---|
8099 | my($counter_name,$counter_incr) = ref($key) ? @$key : ($key,1); |
---|
8100 | my($val,$flags); my($type) = 'C32'; |
---|
8101 | my($stat) = $cursor->c_get($counter_name,$val,DB_SET); |
---|
8102 | if ($stat==0) { # exists, update it |
---|
8103 | if ($val =~ /^\Q$type\E (\d+)\z/o) { $val = $1 } |
---|
8104 | else { do_log(-2,"WARN: counter syntax? $val, clearing"); $val = 0 } |
---|
8105 | $flags = DB_CURRENT; $val = $val+$counter_incr; |
---|
8106 | } else { # create new entry |
---|
8107 | $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!."; |
---|
8108 | $flags = DB_KEYLAST; $val = $counter_incr; |
---|
8109 | } |
---|
8110 | $cursor->c_put($counter_name, sprintf("%s %010d",$type,$val),$flags)==0 |
---|
8111 | or die "c_put: $BerkeleyDB::Error, $!."; |
---|
8112 | } |
---|
8113 | $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!."; |
---|
8114 | $cursor = undef; |
---|
8115 | }; |
---|
8116 | $eval_stat = $@; |
---|
8117 | if (defined $db) { |
---|
8118 | $cursor->c_close if defined $cursor; # unlock, ignoring status |
---|
8119 | $cursor = undef; |
---|
8120 | if ($eval_stat eq '') { |
---|
8121 | # my($stat); $db->db_sync(); # not really needed |
---|
8122 | # $stat==0 or warn "BDB S db_sync, status $stat: $BerkeleyDB::Error, $!."; |
---|
8123 | } |
---|
8124 | } |
---|
8125 | } |
---|
8126 | delete $self->{'cnt'}; |
---|
8127 | if ($interrupt ne '') { kill($interrupt,$$) } # resignal |
---|
8128 | elsif ($eval_stat ne '') |
---|
8129 | { chomp($eval_stat); die "update_counters: BDB S $eval_stat\n" } |
---|
8130 | } |
---|
8131 | |
---|
8132 | sub read_counters { |
---|
8133 | my($self,@counter_names) = @_; |
---|
8134 | my($eval_stat,$interrupt); $interrupt = ''; |
---|
8135 | my($db) = $self->{'db_snmp'}; my($cursor); my(@values); |
---|
8136 | my($h1) = sub { $interrupt = $_[0] }; |
---|
8137 | local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8; |
---|
8138 | eval { # ensure cursor will be unlocked even in case of errors or signals |
---|
8139 | $cursor = $db->db_cursor; # obtain read lock |
---|
8140 | defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!."; |
---|
8141 | for my $cname (@counter_names) { |
---|
8142 | my($val); my($stat) = $cursor->c_get($cname,$val,DB_SET); |
---|
8143 | push(@values, $stat==0 ? $val : undef); |
---|
8144 | $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!."; |
---|
8145 | } |
---|
8146 | $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!."; |
---|
8147 | $cursor = undef; |
---|
8148 | }; |
---|
8149 | $eval_stat = $@; |
---|
8150 | if (defined $db) { |
---|
8151 | $cursor->c_close if defined $cursor; # unlock, ignoring status |
---|
8152 | $cursor = undef; |
---|
8153 | } |
---|
8154 | if ($interrupt ne '') { kill($interrupt,$$) } # resignal |
---|
8155 | elsif ($eval_stat ne '') |
---|
8156 | { chomp($eval_stat); die "read_counters: BDB S $eval_stat\n" } |
---|
8157 | my($type) = 'C32'; |
---|
8158 | for my $val (@values) { |
---|
8159 | if (!defined($val)) {} # keep undefined |
---|
8160 | elsif ($val =~ /^\Q$type\E (\d+)\z/o) { $val = 0+$1 } |
---|
8161 | else { do_log(-2,"WARN: counter syntax? $val"); $val = undef } |
---|
8162 | } |
---|
8163 | \@values; |
---|
8164 | } |
---|
8165 | |
---|
8166 | sub register_proc { |
---|
8167 | my($self,$task_id) = @_; |
---|
8168 | my($db) = $self->{'db_nanny'}; my($cursor); |
---|
8169 | my($val,$new_val); my($key) = sprintf("%05d",$$); |
---|
8170 | $new_val = sprintf("%010d %-12s", time, $task_id) if defined $task_id; |
---|
8171 | my($eval_stat,$interrupt); $interrupt = ''; |
---|
8172 | my($h1) = sub { $interrupt = $_[0] }; |
---|
8173 | local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8; |
---|
8174 | eval { # ensure cursor will be unlocked even in case of errors or signals |
---|
8175 | $cursor = $db->db_cursor(DB_WRITECURSOR); # obtain write lock |
---|
8176 | defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!."; |
---|
8177 | my($stat) = $cursor->c_get($key,$val,DB_SET); |
---|
8178 | $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!."; |
---|
8179 | if ($stat==0 && !defined $task_id) { # remove existing entry |
---|
8180 | $cursor->c_del==0 or die "c_del: $BerkeleyDB::Error, $!."; |
---|
8181 | } elsif (defined $task_id && !($stat==0 && $new_val eq $val)) { |
---|
8182 | # add new, or update existing entry if different |
---|
8183 | $cursor->c_put($key, $new_val, |
---|
8184 | $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0 |
---|
8185 | or die "c_put: $BerkeleyDB::Error, $!."; |
---|
8186 | } |
---|
8187 | $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!."; |
---|
8188 | $cursor = undef; |
---|
8189 | }; |
---|
8190 | $eval_stat = $@; |
---|
8191 | if (defined $db) { |
---|
8192 | $cursor->c_close if defined $cursor; # unlock, ignoring status |
---|
8193 | $cursor = undef; |
---|
8194 | if ($eval_stat eq '') { |
---|
8195 | # my($stat) = $db->db_sync(); # not really needed |
---|
8196 | # $stat==0 or warn "BDB N db_sync, status $stat: $BerkeleyDB::Error, $!."; |
---|
8197 | } |
---|
8198 | } |
---|
8199 | if ($interrupt ne '') { kill($interrupt,$$) } # resignal |
---|
8200 | elsif ($eval_stat ne '') |
---|
8201 | { chomp($eval_stat); die "register_proc: BDB N $eval_stat\n" } |
---|
8202 | } |
---|
8203 | |
---|
8204 | 1; |
---|
8205 | |
---|
8206 | # |
---|
8207 | package Amavis::DB; |
---|
8208 | use strict; |
---|
8209 | use re 'taint'; |
---|
8210 | |
---|
8211 | BEGIN { |
---|
8212 | import Amavis::Conf qw($db_home $daemon_chroot_dir); |
---|
8213 | import Amavis::Util qw(untaint ll do_log); |
---|
8214 | } |
---|
8215 | |
---|
8216 | use BerkeleyDB; |
---|
8217 | |
---|
8218 | BEGIN { |
---|
8219 | use Exporter (); |
---|
8220 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
8221 | $VERSION = '2.034'; |
---|
8222 | @ISA = qw(Exporter); |
---|
8223 | } |
---|
8224 | |
---|
8225 | # create new databases, then close them (called by the parent process) |
---|
8226 | # (called only if $db_home is nonempty) |
---|
8227 | sub init($) { |
---|
8228 | my($predelete) = @_; # delete existing db files first? |
---|
8229 | my($name) = $db_home; |
---|
8230 | $name = "$daemon_chroot_dir $name" if $daemon_chroot_dir ne ''; |
---|
8231 | if ($predelete) { # delete old database files |
---|
8232 | local(*DIR); my($f); |
---|
8233 | opendir(DIR,$db_home) or die "Can't open directory $name: $!"; |
---|
8234 | while (defined($f = readdir(DIR))) { |
---|
8235 | next if ($f eq '.' || $f eq '..') && -d _; |
---|
8236 | if ($f =~ /^(__db\.\d+|(cache-expiry|cache|snmp|nanny)\.db)\z/s) { |
---|
8237 | $f = untaint($f); |
---|
8238 | unlink("$db_home/$f") or die "Can't delete file $name/$f: $!"; |
---|
8239 | } |
---|
8240 | } |
---|
8241 | closedir(DIR) or die "Can't close directory $name: $!"; |
---|
8242 | } |
---|
8243 | my($env) = BerkeleyDB::Env->new(-Home=>$db_home, -Mode=>0640, |
---|
8244 | -Flags=> DB_CREATE | DB_INIT_CDB | DB_INIT_MPOOL); |
---|
8245 | defined $env or die "BDB bad db env. at $db_home: $BerkeleyDB::Error, $!."; |
---|
8246 | do_log(0, sprintf("Creating db in %s/; BerkeleyDB %s, libdb %s", |
---|
8247 | $name, BerkeleyDB->VERSION, $BerkeleyDB::db_version)); |
---|
8248 | my($dbc) = BerkeleyDB::Hash->new( |
---|
8249 | -Filename=>'cache.db', -Flags=>DB_CREATE, -Env=>$env ); |
---|
8250 | defined $dbc or die "BDB no dbC: $BerkeleyDB::Error, $!."; |
---|
8251 | my($dbq) = BerkeleyDB::Queue->new( |
---|
8252 | -Filename=>'cache-expiry.db', -Flags=>DB_CREATE, -Env=>$env, |
---|
8253 | -Len=>15+1+32 ); # '-ExtentSize' needs DB 3.2.x, e.g. -ExtentSize=>2 |
---|
8254 | defined $dbq or die "BDB no dbQ: $BerkeleyDB::Error, $!."; |
---|
8255 | my($dbs) = BerkeleyDB::Hash->new( |
---|
8256 | -Filename=>'snmp.db', -Flags=>DB_CREATE, -Env=>$env ); |
---|
8257 | defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!."; |
---|
8258 | my($dbn) = BerkeleyDB::Hash->new( |
---|
8259 | -Filename=>'nanny.db', -Flags=>DB_CREATE, -Env=>$env ); |
---|
8260 | defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!."; |
---|
8261 | |
---|
8262 | Amavis::DB::SNMP::put_initial_snmp_data($dbs); |
---|
8263 | for my $db ($dbc, $dbq, $dbs, $dbn) |
---|
8264 | { $db->db_close==0 or die "BDB db_close: $BerkeleyDB::Error, $!." } |
---|
8265 | } |
---|
8266 | |
---|
8267 | # open an existing databases environment (called by each child process) |
---|
8268 | sub new { |
---|
8269 | my($class) = @_; my($env); |
---|
8270 | if (defined $db_home) { |
---|
8271 | $env = BerkeleyDB::Env->new( |
---|
8272 | -Home=>$db_home, -Mode=>0640, -Flags=> DB_INIT_CDB | DB_INIT_MPOOL); |
---|
8273 | defined $env or die "BDB bad db env. at $db_home: $BerkeleyDB::Error, $!."; |
---|
8274 | } |
---|
8275 | bless \$env, $class; |
---|
8276 | } |
---|
8277 | sub get_db_env { my($self) = shift; $$self } |
---|
8278 | |
---|
8279 | 1; |
---|
8280 | |
---|
8281 | __DATA__ |
---|
8282 | # |
---|
8283 | package Amavis::Cache; |
---|
8284 | # offer an 'IPC::Cache'-compatible interface to a BerkeleyDB-based cache. |
---|
8285 | # Replaces methods new,get,set of the memory-based cache. |
---|
8286 | use strict; |
---|
8287 | use re 'taint'; |
---|
8288 | |
---|
8289 | BEGIN { |
---|
8290 | import Amavis::Util qw(ll do_log); |
---|
8291 | } |
---|
8292 | |
---|
8293 | use BerkeleyDB; |
---|
8294 | |
---|
8295 | BEGIN { |
---|
8296 | use Exporter (); |
---|
8297 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
8298 | $VERSION = '2.0342'; |
---|
8299 | @ISA = qw(Exporter); |
---|
8300 | } |
---|
8301 | |
---|
8302 | # open existing databases (called by each child process); |
---|
8303 | # if $db_env is undef a memory-based cache is created, otherwise use BerkeleyDB |
---|
8304 | sub new { |
---|
8305 | my($class,$db_env) = @_; |
---|
8306 | my($dbc,$dbq,$mem_cache); |
---|
8307 | if (!defined($db_env)) { |
---|
8308 | do_log(1,"BerkeleyDB not available, using memory-based local cache"); |
---|
8309 | $mem_cache = {}; |
---|
8310 | } else { |
---|
8311 | my($env) = $db_env->get_db_env; |
---|
8312 | defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!."; |
---|
8313 | $dbc = BerkeleyDB::Hash->new(-Filename=>'cache.db', -Env=>$env); |
---|
8314 | defined $dbc or die "BDB no dbC: $BerkeleyDB::Error, $!."; |
---|
8315 | $dbq = BerkeleyDB::Queue->new(-Filename=>'cache-expiry.db', -Env=>$env, |
---|
8316 | -Len=>15+1+32); # '-ExtentSize' needs DB 3.2.x, e.g. -ExtentSize=>2 |
---|
8317 | defined $dbq or die "BDB no dbQ: $BerkeleyDB::Error, $!."; |
---|
8318 | } |
---|
8319 | bless {'db_cache'=>$dbc, 'db_queue'=>$dbq, 'mem_cache'=>$mem_cache}, $class; |
---|
8320 | } |
---|
8321 | |
---|
8322 | sub DESTROY { |
---|
8323 | my($self) = shift; |
---|
8324 | eval { do_log(5,"Amavis::Cache called") }; |
---|
8325 | for my $db ($self->{'db_cache'}, $self->{'db_queue'}) { |
---|
8326 | if (defined $db) { |
---|
8327 | eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." }; |
---|
8328 | if ($@ ne '') { warn "BDB C+Q DESTROY $@" } |
---|
8329 | $db = undef; |
---|
8330 | } |
---|
8331 | } |
---|
8332 | } |
---|
8333 | |
---|
8334 | # purge expired entries from the queue head and enqueue new entry at the tail |
---|
8335 | sub enqueue { |
---|
8336 | my($self,$str,$now_utc_iso8601,$expires_utc_iso8601) = @_; |
---|
8337 | my($db) = $self->{'db_cache'}; my($dbq) = $self->{'db_queue'}; |
---|
8338 | local($1,$2); my($stat,$key,$val); $key = ''; |
---|
8339 | my($qcursor) = $dbq->db_cursor(DB_WRITECURSOR); |
---|
8340 | defined $qcursor or die "BDB Q db_cursor: $BerkeleyDB::Error, $!."; |
---|
8341 | while ( ($stat=$qcursor->c_get($key,$val,DB_NEXT)) == 0 ) { |
---|
8342 | if ($val !~ /^([^ ]+) (.*)\z/s) { |
---|
8343 | do_log(-2,"WARN: queue head invalid, deleting: $val"); |
---|
8344 | } else { |
---|
8345 | my($t,$digest) = ($1,$2); |
---|
8346 | last if $t ge $now_utc_iso8601; |
---|
8347 | my($cursor) = $db->db_cursor(DB_WRITECURSOR); |
---|
8348 | defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!."; |
---|
8349 | my($v); my($st1) = $cursor->c_get($digest,$v,DB_SET); |
---|
8350 | $st1==0 || $st1==DB_NOTFOUND or die "BDB C c_get: $BerkeleyDB::Error, $!."; |
---|
8351 | if ($st1==0 && $v=~/^([^ ]+) /s) { # record exists and appears valid |
---|
8352 | if ($1 ne $t) { |
---|
8353 | do_log(5,"enqueue: not deleting: $digest, was refreshed since"); |
---|
8354 | } else { # its expiration time correspond to timestamp in the queue |
---|
8355 | do_log(5,"enqueue: deleting: $digest"); |
---|
8356 | my($st2) = $cursor->c_del; # delete expired entry from the cache |
---|
8357 | $st2==0 || $st2==DB_KEYEMPTY |
---|
8358 | or die "BDB C c_del: $BerkeleyDB::Error, $!."; |
---|
8359 | } |
---|
8360 | } |
---|
8361 | $cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!."; |
---|
8362 | } |
---|
8363 | my($st3) = $qcursor->c_del; |
---|
8364 | $st3==0 || $st3==DB_KEYEMPTY or die "BDB Q c_del: $BerkeleyDB::Error, $!."; |
---|
8365 | } |
---|
8366 | $stat==0 || $stat==DB_NOTFOUND or die "BDB Q c_get: $BerkeleyDB::Error, $!."; |
---|
8367 | $qcursor->c_close==0 or die "BDB Q c_close: $BerkeleyDB::Error, $!."; |
---|
8368 | # insert new expiration request in the queue |
---|
8369 | $dbq->db_put($key, "$expires_utc_iso8601 $str", DB_APPEND) == 0 |
---|
8370 | or die "BDB Q db_put: $BerkeleyDB::Error, $!."; |
---|
8371 | # syncing would only be worth doing if we would want the cache to persist |
---|
8372 | # across restarts - but we scratch the databases to avoid rebuild worries |
---|
8373 | # $stat = $dbq->db_sync(); |
---|
8374 | # $stat==0 or warn "BDB Q db_sync, status $stat: $BerkeleyDB::Error, $!."; |
---|
8375 | # $stat = $db->db_sync(); |
---|
8376 | # $stat==0 or warn "BDB C db_sync, status $stat: $BerkeleyDB::Error, $!."; |
---|
8377 | } |
---|
8378 | |
---|
8379 | sub get { |
---|
8380 | my($self,$key) = @_; |
---|
8381 | my($val); my($db) = $self->{'db_cache'}; |
---|
8382 | if (!defined($db)) { |
---|
8383 | $val = $self->{'mem_cache'}{$key}; # simple local memory-based cache |
---|
8384 | } else { |
---|
8385 | my($stat) = $db->db_get($key,$val); |
---|
8386 | $stat==0 || $stat==DB_NOTFOUND |
---|
8387 | or die "BDB C c_get: $BerkeleyDB::Error, $!."; |
---|
8388 | local($1,$2); |
---|
8389 | if ($stat==0 && $val=~/^([^ ]+) (.*)/s) { $val = $2 } else { $val = undef } |
---|
8390 | } |
---|
8391 | thaw($val); |
---|
8392 | } |
---|
8393 | |
---|
8394 | sub set { |
---|
8395 | my($self,$key,$obj,$now_utc_iso8601,$expires_utc_iso8601) = @_; |
---|
8396 | my($db) = $self->{'db_cache'}; |
---|
8397 | if (!defined($db)) { |
---|
8398 | $self->{'mem_cache'}{$key} = freeze($obj); |
---|
8399 | } else { |
---|
8400 | my($cursor) = $db->db_cursor(DB_WRITECURSOR); |
---|
8401 | defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!."; |
---|
8402 | my($val); my($stat) = $cursor->c_get($key,$val,DB_SET); |
---|
8403 | $stat==0 || $stat==DB_NOTFOUND |
---|
8404 | or die "BDB C c_get: $BerkeleyDB::Error, $!."; |
---|
8405 | $cursor->c_put($key, $expires_utc_iso8601.' '.freeze($obj), |
---|
8406 | $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0 |
---|
8407 | or die "BDB C c_put: $BerkeleyDB::Error, $!."; |
---|
8408 | $cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!."; |
---|
8409 | # $stat = $db->db_sync(); # only worth doing if cache were persistent |
---|
8410 | # $stat==0 or warn "BDB C db_sync, status $stat: $BerkeleyDB::Error, $!."; |
---|
8411 | $self->enqueue($key,$now_utc_iso8601,$expires_utc_iso8601); |
---|
8412 | } |
---|
8413 | $obj; |
---|
8414 | } |
---|
8415 | |
---|
8416 | 1; |
---|
8417 | |
---|
8418 | __DATA__ |
---|
8419 | # |
---|
8420 | package Amavis::Lookup::SQLfield; |
---|
8421 | use strict; |
---|
8422 | use re 'taint'; |
---|
8423 | |
---|
8424 | BEGIN { |
---|
8425 | use Exporter (); |
---|
8426 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
8427 | $VERSION = '2.034'; |
---|
8428 | @ISA = qw(Exporter); |
---|
8429 | } |
---|
8430 | BEGIN { import Amavis::Util qw(ll do_log) } |
---|
8431 | |
---|
8432 | sub new($$$;$$) { |
---|
8433 | my($class, $sql_query,$fieldname, $fieldtype,$implied_args) = @_; |
---|
8434 | # fieldtype: B=boolean, N=numeric, S=string, |
---|
8435 | # N-: numeric, nonexistent field returns undef without complaint |
---|
8436 | # S-: string, nonexistent field returns undef without complaint |
---|
8437 | # B-: boolean, nonexistent field returns undef without complaint |
---|
8438 | # B0: boolean, nonexistent field treated as false |
---|
8439 | # B1: boolean, nonexistent field treated as true |
---|
8440 | return undef if !defined($sql_query); |
---|
8441 | my($self) = bless {}, $class; |
---|
8442 | $self->{sql_query} = $sql_query; |
---|
8443 | $self->{fieldname} = lc($fieldname); |
---|
8444 | $self->{fieldtype} = uc($fieldtype); |
---|
8445 | $self->{args} = ref($implied_args) eq 'ARRAY' ? [@$implied_args] # copy |
---|
8446 | : [$implied_args] if defined $implied_args; |
---|
8447 | $self; |
---|
8448 | } |
---|
8449 | |
---|
8450 | sub lookup_sql_field($$$) { |
---|
8451 | my($self,$addr,$get_all) = @_; |
---|
8452 | my(@result,@matchingkey); |
---|
8453 | if (!defined($self)) { |
---|
8454 | do_log(5, "lookup_sql_field - undefined, \"$addr\" no match"); |
---|
8455 | } elsif (!defined($self->{sql_query})) { |
---|
8456 | do_log(5, sprintf("lookup_sql_field(%s) - null query, \"%s\" no match", |
---|
8457 | $self->{fieldname}, $addr)); |
---|
8458 | } else { |
---|
8459 | my($field) = $self->{fieldname}; |
---|
8460 | my($res_ref,$mk_ref) = $self->{sql_query}->lookup_sql($addr,1, |
---|
8461 | !exists($self->{args}) ? () : $self->{args}); |
---|
8462 | do_log(5, "lookup_sql_field($field), \"$addr\" no matching record") |
---|
8463 | if !@$res_ref; |
---|
8464 | for my $ind (0..$#$res_ref) { |
---|
8465 | my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind]; |
---|
8466 | if (!exists($h_ref->{$field})) { |
---|
8467 | # record found, but no field with that name in the table |
---|
8468 | # fieldtype: B0: boolean, nonexistent field treated as false, |
---|
8469 | # B1: boolean, nonexistent field treated as true |
---|
8470 | if ( $self->{fieldtype} =~ /^B0/) { # boolean, defaults to false |
---|
8471 | $match = 0; # nonexistent field treated as 0 |
---|
8472 | do_log(5, "lookup_sql_field($field), no field, \"$addr\" result=$match"); |
---|
8473 | } elsif ($self->{fieldtype} =~ /^B1/) { # defaults to true |
---|
8474 | $match = 1; # nonexistent field treated as 1 |
---|
8475 | do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=$match"); |
---|
8476 | } elsif ($self->{fieldtype}=~/^.-/s) { # allowed to not exist |
---|
8477 | do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=undef"); |
---|
8478 | } else { # treated as 'no match', issue a warning |
---|
8479 | do_log(1,"lookup_sql_field($field) ". |
---|
8480 | "(WARN: no such field in the SQL table), ". |
---|
8481 | "\"$addr\" result=undef"); |
---|
8482 | } |
---|
8483 | } else { # field exists |
---|
8484 | # fieldtype: B=boolean, N=numeric, S=string |
---|
8485 | $match = $h_ref->{$field}; |
---|
8486 | if (!defined($match)) { # NULL field values represented as undef |
---|
8487 | } elsif ($self->{fieldtype} =~ /^B/) { # boolean |
---|
8488 | # convert values 'N', 'F', '0', ' ' and "\000" to 0 |
---|
8489 | # to allow value to be used directly as a Perl boolean |
---|
8490 | $match = 0 if $match =~ /^([NnFf ]|0+|\000+)[ ]*\z/; |
---|
8491 | } elsif ($self->{fieldtype} =~ /^N/) { # numeric |
---|
8492 | $match = $match + 0; # unify different numeric forms |
---|
8493 | } elsif ($self->{fieldtype} =~ /^S/) { # string |
---|
8494 | $match =~ s/ +\z//; # trim trailing spaces |
---|
8495 | } |
---|
8496 | do_log(5, "lookup_sql_field($field) \"$addr\" result=" . |
---|
8497 | (defined $match ? $match : 'undef') ); |
---|
8498 | } |
---|
8499 | if (defined $match) { |
---|
8500 | push(@result,$match); push(@matchingkey,$mk); |
---|
8501 | last if !$get_all; |
---|
8502 | } |
---|
8503 | } |
---|
8504 | } |
---|
8505 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
8506 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
8507 | } |
---|
8508 | |
---|
8509 | 1; |
---|
8510 | |
---|
8511 | # |
---|
8512 | package Amavis::Lookup::SQL; |
---|
8513 | use strict; |
---|
8514 | use re 'taint'; |
---|
8515 | |
---|
8516 | BEGIN { |
---|
8517 | use Exporter (); |
---|
8518 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
8519 | $VERSION = '2.034'; |
---|
8520 | @ISA = qw(Exporter); |
---|
8521 | } |
---|
8522 | |
---|
8523 | use DBI; |
---|
8524 | |
---|
8525 | BEGIN { |
---|
8526 | import Amavis::Conf qw(:platform :confvars c cr ca); |
---|
8527 | import Amavis::Timing qw(section_time); |
---|
8528 | import Amavis::Util qw(untaint snmp_count ll do_log); |
---|
8529 | import Amavis::rfc2821_2822_Tools qw(make_query_keys); |
---|
8530 | } |
---|
8531 | |
---|
8532 | use vars qw($sql_connected); |
---|
8533 | |
---|
8534 | # Connect to a database. Take a list of database connection |
---|
8535 | # parameters and try each until one succeeds. |
---|
8536 | # -- based on code from Ben Ransford <amavis@uce.ransford.org> 2002-09-22 |
---|
8537 | sub connect_to_sql(@) { |
---|
8538 | my(@dsns) = @_; # a list of DSNs to try connecting to sequentially |
---|
8539 | my($dbh); |
---|
8540 | do_log(3,"Connecting to SQL database server"); |
---|
8541 | for my $tmpdsn (@dsns) { |
---|
8542 | my($dsn, $username, $password) = @$tmpdsn; |
---|
8543 | do_log(4, "connect_to_sql: trying '$dsn'"); |
---|
8544 | $dbh = DBI->connect($dsn, $username, $password, |
---|
8545 | {PrintError => 0, RaiseError => 0, Taint => 1} ); |
---|
8546 | if ($dbh) { do_log(3,"connect_to_sql: '$dsn' succeeded"); last } |
---|
8547 | do_log(-1,"connect_to_sql: unable to connect to DSN '$dsn': ".$DBI::errstr); |
---|
8548 | } |
---|
8549 | do_log(-2,"connect_to_sql: unable to connect to any DSN at all!") |
---|
8550 | if !$dbh && @dsns > 1; |
---|
8551 | $sql_connected = 1 if $dbh; |
---|
8552 | $dbh; |
---|
8553 | } |
---|
8554 | |
---|
8555 | # return a new Lookup::SQL object to contain DBI handle and prepared selects |
---|
8556 | sub new { |
---|
8557 | my($class) = @_; bless {}, $class; |
---|
8558 | } |
---|
8559 | |
---|
8560 | # explicitly disconnect from SQL server |
---|
8561 | sub DESTROY { |
---|
8562 | my($self) = shift; |
---|
8563 | eval { do_log(5,"Amavis::Lookup::SQL called") }; |
---|
8564 | if (defined $self && $self->{dbh} && $sql_connected) { |
---|
8565 | $sql_connected = 0; |
---|
8566 | eval { $self->{dbh}->disconnect }; $self->{dbh} = undef; |
---|
8567 | } |
---|
8568 | } |
---|
8569 | |
---|
8570 | # store DBI handle and prepared selects into existing Lookup::SQL obj |
---|
8571 | sub store_dbh($$$) { |
---|
8572 | my($self, $dbh, $select_clause) = @_; |
---|
8573 | $self->{dbh} = $dbh; # save DBI handle |
---|
8574 | $self->{select_clause} = $select_clause; |
---|
8575 | $self->clear_cache; # let's start afresh just in case |
---|
8576 | $self; |
---|
8577 | } |
---|
8578 | |
---|
8579 | sub clear_cache { |
---|
8580 | my($self) = @_; |
---|
8581 | delete $self->{cache}; |
---|
8582 | } |
---|
8583 | |
---|
8584 | # lookup_sql() performs a lookup for an e-mail address against a SQL map. |
---|
8585 | # If a match is found it returns whatever the map returns (a reference |
---|
8586 | # to a hash containing values of requested fields), otherwise returns undef. |
---|
8587 | # A match aborts further fetching sequence, unless $get_all is true. |
---|
8588 | # |
---|
8589 | # SQL lookups (e.g. for user+foo@example.com) are performed in order |
---|
8590 | # which can be requested by 'ORDER BY' in the SELECT statement, otherwise |
---|
8591 | # the order is unspecified, which is only useful if only specific entries |
---|
8592 | # exist in a database (e.g. only full addresses, not domains). |
---|
8593 | # |
---|
8594 | # The following order is recommended, going from specific to more general: |
---|
8595 | # - lookup for user+foo@example.com |
---|
8596 | # - lookup for user@example.com (only if $recipient_delimiter nonempty) |
---|
8597 | # - lookup for user+foo ('naked lookup': only if local) |
---|
8598 | # - lookup for user ('naked lookup': local and $recipient_delimiter nonempty) |
---|
8599 | # - lookup for @sub.example.com |
---|
8600 | # - lookup for @.sub.example.com |
---|
8601 | # - lookup for @.example.com |
---|
8602 | # - lookup for @.com |
---|
8603 | # - lookup for @. (catchall) |
---|
8604 | # NOTE: |
---|
8605 | # this is different from hash and ACL lookups in two important aspects: |
---|
8606 | # - a key without '@' implies mailbox (=user) name, not domain name; |
---|
8607 | # - the naked mailbox name lookups are only performed when the e-mail addr |
---|
8608 | # (usually its domain part) matches the static local_domains* lookups. |
---|
8609 | # |
---|
8610 | # The domain part is always lowercased when constructing a key, |
---|
8611 | # the localpart is lowercased unless $localpart_is_case_sensitive is true. |
---|
8612 | # |
---|
8613 | sub lookup_sql($$$;$) { |
---|
8614 | my($self, $addr,$get_all,$extra_args) = @_; |
---|
8615 | my(@matchingkey,@result); |
---|
8616 | if (!defined $extra_args && |
---|
8617 | exists $self->{cache} && exists $self->{cache}->{$addr}) |
---|
8618 | { # cached ? |
---|
8619 | my($c) = $self->{cache}->{$addr}; @result = @$c if ref $c; |
---|
8620 | @matchingkey = map {'/cached/'} @result; #will do for now, improve some day |
---|
8621 | if (!ll(5)) { |
---|
8622 | # don't bother preparing log report which will not be printed |
---|
8623 | } elsif (!@result) { |
---|
8624 | do_log(5,"lookup_sql (cached): \"$addr\" no match"); |
---|
8625 | } else { |
---|
8626 | for my $m (@result) { |
---|
8627 | do_log(5, sprintf("lookup_sql (cached): \"%s\" matches, result=(%s)", |
---|
8628 | $addr, join(", ", map { sprintf("%s=>%s", $_, |
---|
8629 | !defined($m->{$_})?'-':'"'.$m->{$_}.'"' |
---|
8630 | ) } sort keys(%$m) ) )); |
---|
8631 | } |
---|
8632 | } |
---|
8633 | if (!$get_all) { |
---|
8634 | return(!wantarray ? $result[0] : ($result[0], $matchingkey[0])); |
---|
8635 | } else { |
---|
8636 | return(!wantarray ? \@result : (\@result, \@matchingkey)); |
---|
8637 | } |
---|
8638 | } |
---|
8639 | if (!$sql_connected) { |
---|
8640 | my($sql_dbh) = connect_to_sql(@lookup_sql_dsn); |
---|
8641 | section_time('sql-connect'); |
---|
8642 | defined($sql_dbh) or die "SQL server(s) not reachable"; |
---|
8643 | $sql_dbh->{'RaiseError'} = 1; |
---|
8644 | $Amavis::sql_policy->store_dbh($sql_dbh, $sql_select_policy) |
---|
8645 | if defined $sql_select_policy; |
---|
8646 | $Amavis::sql_wblist->store_dbh($sql_dbh, $sql_select_white_black_list) |
---|
8647 | if defined $sql_select_white_black_list; |
---|
8648 | } |
---|
8649 | my($is_local); # $local_domains_sql is not looked up to avoid recursion! |
---|
8650 | $is_local = Amavis::Lookup::lookup(0,$addr, |
---|
8651 | grep {ref ne 'Amavis::Lookup::SQL' && |
---|
8652 | ref ne 'Amavis::Lookup::SQLfield' && |
---|
8653 | ref ne 'Amavis::Lookup::LDAP' && |
---|
8654 | ref ne 'Amavis::Lookup::LDAPattr'} |
---|
8655 | @{ca('local_domains_maps')}); |
---|
8656 | my($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local); |
---|
8657 | my($n) = sprintf("%d",scalar(@$keys_ref)); # number of keys |
---|
8658 | my($sel) = $self->{select_clause}; |
---|
8659 | my(@pos_args); my(@extras_tmp) = !ref $extra_args ? () : @$extra_args; |
---|
8660 | $sel =~ s{ ( %k | \? ) } # substitute %k for keys and ? for each extra arg |
---|
8661 | { push(@pos_args, map { untaint($_) } |
---|
8662 | $1 eq '%k' ? @$keys_ref : shift @extras_tmp), |
---|
8663 | $1 eq '%k' ? join(',', ('?') x $n) : '?' }gxe; |
---|
8664 | if (!exists $self->{"sth$n"}) { |
---|
8665 | # 'prepare' appropriate query only when needed, and save it for reuse |
---|
8666 | do_log(5,"SQL prepare($n): $sel"); |
---|
8667 | $self->{"sth$n"} = $self->{dbh}->prepare($sel); |
---|
8668 | } |
---|
8669 | my($sth) = $self->{"sth$n"}; |
---|
8670 | do_log(4,"lookup_sql \"$addr\", query args: ". |
---|
8671 | join(', ', map{"\"$_\""} @pos_args)); |
---|
8672 | do_log(4,"lookup_sql select: $sel"); |
---|
8673 | my($a_ref,$found); my($match) = {}; |
---|
8674 | eval { |
---|
8675 | snmp_count('OpsSqlSelect'); |
---|
8676 | $sth->execute(@pos_args); # do the query |
---|
8677 | while ( defined($a_ref=$sth->fetchrow_arrayref) ) { # fetch query results |
---|
8678 | my(@names) = @{$sth->{NAME_lc}}; |
---|
8679 | $match = {}; @$match{@names} = @$a_ref; |
---|
8680 | if (!exists $match->{'local'} && $match->{'email'} eq '@.') { |
---|
8681 | # UGLY HACK to let a catchall (@.) imply that field 'local' has |
---|
8682 | # a value undef (NULL) when that field is not present in the |
---|
8683 | # database. This overrides B1 fieldtype default by an explicit |
---|
8684 | # undef for '@.', causing a fallback to static lookup tables. |
---|
8685 | # The purpose is to provide a useful default for local_domains |
---|
8686 | # lookup if the field 'local' is not present in the SQL table. |
---|
8687 | # NOTE: field names 'local' and 'email' are hardwired here!!! |
---|
8688 | push(@names,'local'); $match->{'local'} = undef; |
---|
8689 | do_log(5, "lookup_sql: \"$addr\" matches catchall, local=>undef"); |
---|
8690 | } |
---|
8691 | push(@result, {%$match}); # copy hash |
---|
8692 | push(@matchingkey, join(", ", map { sprintf("%s=>%s", $_, |
---|
8693 | !defined($match->{$_})?'-':'"'.$match->{$_}.'"' |
---|
8694 | ) } @names)); |
---|
8695 | last if !$get_all; |
---|
8696 | } |
---|
8697 | $sth->finish(); |
---|
8698 | }; # eval |
---|
8699 | if ($@ ne '') { |
---|
8700 | my($err) = $@; |
---|
8701 | do_log(-1, "lookup_sql: $DBI::err, $DBI::errstr"); |
---|
8702 | if (!$sth) {} |
---|
8703 | elsif ($sth->err eq '2006' || $sth->errstr =~ /\bserver has gone away\b/ || |
---|
8704 | $sth->err eq '2013' || $sth->errstr =~ /\bLost connection to\b/) { |
---|
8705 | do_log(-1,"NOTICE: Disconnected from SQL server"); |
---|
8706 | $sql_connected = 0; $self->{dbh}->disconnect; $self->{dbh} = undef; |
---|
8707 | } |
---|
8708 | die $err; |
---|
8709 | } |
---|
8710 | if (!ll(4)) { |
---|
8711 | # don't bother preparing log report which will not be printed |
---|
8712 | } elsif (!@result) { |
---|
8713 | do_log(4, "lookup_sql, \"$addr\" no match") |
---|
8714 | } else { |
---|
8715 | do_log(4, "lookup_sql($addr) matches, result=($_)") for @matchingkey; |
---|
8716 | } |
---|
8717 | # save for future use, but only within processing of this message |
---|
8718 | $self->{cache}->{$addr} = \@result; |
---|
8719 | section_time('lookup_sql'); |
---|
8720 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
8721 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
8722 | } |
---|
8723 | |
---|
8724 | 1; |
---|
8725 | |
---|
8726 | __DATA__ |
---|
8727 | #^L |
---|
8728 | package Amavis::Lookup::LDAPattr; |
---|
8729 | |
---|
8730 | use strict; |
---|
8731 | use re 'taint'; |
---|
8732 | |
---|
8733 | BEGIN { |
---|
8734 | use Exporter (); |
---|
8735 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
8736 | $VERSION = '2.034'; |
---|
8737 | @ISA = qw(Exporter); |
---|
8738 | |
---|
8739 | import Amavis::Util qw(ll do_log) |
---|
8740 | } |
---|
8741 | |
---|
8742 | # attrtype: B=boolean, N=numeric, S=string, L=list |
---|
8743 | # N-: numeric, nonexistent field returns undef without complaint |
---|
8744 | # S-: string, nonexistent field returns undef without complaint |
---|
8745 | # L-: list, nonexistent field returns undef without complaint |
---|
8746 | # B-: boolean, nonexistent field returns undef without complaint |
---|
8747 | # B0: boolean, nonexistent field treated as false |
---|
8748 | # B1: boolean, nonexistent field treated as true |
---|
8749 | |
---|
8750 | sub new($$$;$) { |
---|
8751 | my($class,$ldap_query,$attrname,$attrtype) = @_; |
---|
8752 | return undef if !defined($ldap_query); |
---|
8753 | my($self) = bless {}, $class; |
---|
8754 | $self->{ldap_query} = $ldap_query; |
---|
8755 | $self->{attrname} = lc($attrname); |
---|
8756 | $self->{attrtype} = uc($attrtype); |
---|
8757 | $self; |
---|
8758 | } |
---|
8759 | |
---|
8760 | sub lookup_ldap_attr($$$) { |
---|
8761 | my($self,$addr,$get_all) = @_; |
---|
8762 | my(@result,@matchingkey); |
---|
8763 | if (!defined($self)) { |
---|
8764 | do_log(5,"lookup_ldap_attr - undefined, \"$addr\" no match"); |
---|
8765 | } elsif (!defined($self->{ldap_query})) { |
---|
8766 | do_log(5,sprintf("lookup_ldap_attr(%s) - null query, \"%s\" no match", |
---|
8767 | $self->{attrname}, $addr)); |
---|
8768 | } else { |
---|
8769 | my($attr) = $self->{attrname}; |
---|
8770 | my($res_ref,$mk_ref) = $self->{ldap_query}->lookup_ldap($addr,1); |
---|
8771 | do_log(5,"lookup_ldap_attr($attr), \"$addr\" no matching record") |
---|
8772 | if !@$res_ref; |
---|
8773 | for my $ind (0..$#$res_ref) { |
---|
8774 | my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind]; |
---|
8775 | if (!exists($h_ref->{$attr})) { |
---|
8776 | # record found, but no attribute with that name in the table |
---|
8777 | if ( $self->{attrtype} =~ /^B0/) { # boolean, defaults to false |
---|
8778 | $match = 0; # nonexistent attribute treated as 0 |
---|
8779 | do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match"); |
---|
8780 | } elsif ($self->{attrtype} =~ /^B1/) { # boolean, defaults to true |
---|
8781 | $match = 1; # nonexistent attribute treated as 1 |
---|
8782 | do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match"); |
---|
8783 | } elsif ($self->{attrtype}=~/^.-/s) { # allowed to not exist |
---|
8784 | do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=undef"); |
---|
8785 | } else { # treated as 'no match', issue a warning |
---|
8786 | do_log(1,"lookup_ldap_attr($attr) ". |
---|
8787 | "(WARN: no such attribute in LDAP entry), ". |
---|
8788 | "\"$addr\" result=undef"); |
---|
8789 | } |
---|
8790 | } else { # attribute exists |
---|
8791 | $match = $h_ref->{$attr}; |
---|
8792 | if (!defined($match)) { # NULL attribute values represented as undef |
---|
8793 | } elsif ($self->{attrtype} =~ /^B/) { # boolean |
---|
8794 | $match = $match eq "TRUE" ? 1 : 0; # convert TRUE|FALSE to 1|0 |
---|
8795 | } elsif ($self->{attrtype} =~ /^N/) { # numeric |
---|
8796 | $match = $match + 0; # unify different numeric forms |
---|
8797 | } elsif ($self->{attrtype} =~ /^S/) { # string |
---|
8798 | $match =~ s/ +\z//; # trim trailing spaces |
---|
8799 | } elsif ($self->{attrtype} =~ /^L/) { # list |
---|
8800 | #$match = join(", ",@$match); |
---|
8801 | } |
---|
8802 | do_log(5,sprintf("lookup_ldap_attr(%s) \"%s\" result=(%s)", |
---|
8803 | $attr, $addr, defined($match) ? $match : 'undef')); |
---|
8804 | } |
---|
8805 | if (defined $match) { |
---|
8806 | push(@result,$match); push(@matchingkey,$mk); |
---|
8807 | last if !$get_all; |
---|
8808 | } |
---|
8809 | } |
---|
8810 | } |
---|
8811 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
8812 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
8813 | } |
---|
8814 | |
---|
8815 | 1; |
---|
8816 | |
---|
8817 | # |
---|
8818 | package Amavis::Lookup::LDAP; |
---|
8819 | |
---|
8820 | use strict; |
---|
8821 | use re 'taint'; |
---|
8822 | |
---|
8823 | BEGIN { |
---|
8824 | use Exporter (); |
---|
8825 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION |
---|
8826 | $ldap_sys_default @ldap_attrs); |
---|
8827 | $VERSION = '2.034'; |
---|
8828 | @ISA = qw(Exporter); |
---|
8829 | |
---|
8830 | import Amavis::Conf qw(:platform :confvars c cr ca); |
---|
8831 | import Amavis::Timing qw(section_time); |
---|
8832 | import Amavis::Util qw(untaint snmp_count ll do_log); |
---|
8833 | import Amavis::rfc2821_2822_Tools qw(make_query_keys); |
---|
8834 | |
---|
8835 | $ldap_sys_default = { |
---|
8836 | hostname => 'localhost', |
---|
8837 | port => 389, |
---|
8838 | version => 3, |
---|
8839 | timeout => 120, |
---|
8840 | tls => 0, |
---|
8841 | base => undef, |
---|
8842 | scope => 'sub', |
---|
8843 | query_filter => '(&(objectClass=amavisAccount)(mail=%m))', |
---|
8844 | bind_dn => undef, |
---|
8845 | bind_password => undef, |
---|
8846 | }; |
---|
8847 | |
---|
8848 | @ldap_attrs = qw(amavisVirusLover amavisSpamLover amavisBannedFilesLover |
---|
8849 | amavisBadHeaderLover amavisBypassVirusChecks amavisBypassSpamChecks |
---|
8850 | amavisBypassBannedChecks amavisBypassHeaderChecks amavisSpamTagLevel |
---|
8851 | amavisSpamTag2Level amavisSpamKillLevel amavisSpamModifiesSubj |
---|
8852 | amavisVirusQuarantineTo amavisSpamQuarantineTo amavisBannedQuarantineTo |
---|
8853 | amavisBadHeaderQuarantineTo amavisBlacklistSender amavisWhitelistSender |
---|
8854 | amavisLocal amavisMessageSizeLimit mail |
---|
8855 | amavisWarnVirusRecip amavisWarnBannedRecip amavisWarnBadHeaderRecip |
---|
8856 | ); |
---|
8857 | } |
---|
8858 | |
---|
8859 | use vars qw($ldap_connected); |
---|
8860 | |
---|
8861 | sub new { |
---|
8862 | my($class,$default) = @_; |
---|
8863 | my($self) = bless {}, $class; |
---|
8864 | for (qw(hostname port timeout tls base scope query_filter |
---|
8865 | bind_dn bind_password)) { |
---|
8866 | # replace undefined attributes with defaults |
---|
8867 | $self->{$_} = $default->{$_} unless defined($self->{$_}); |
---|
8868 | $self->{$_} = $ldap_sys_default->{$_} unless defined($self->{$_}); |
---|
8869 | } |
---|
8870 | $self; |
---|
8871 | } |
---|
8872 | |
---|
8873 | # explicitly disconnect from LDAP server |
---|
8874 | sub DESTROY { |
---|
8875 | my($self) = shift; |
---|
8876 | eval { do_log(5,"Amavis::Lookup::LDAP called") }; |
---|
8877 | if (defined $self && $self->{ldap} && $ldap_connected) { |
---|
8878 | $ldap_connected = 0; |
---|
8879 | eval { $self->{ldap}->disconnect }; $self->{ldap} = undef; |
---|
8880 | } |
---|
8881 | } |
---|
8882 | |
---|
8883 | sub connect_to_ldap { |
---|
8884 | my($self) = @_; |
---|
8885 | my($ldap); |
---|
8886 | do_log(3,"Connecting to LDAP host"); |
---|
8887 | my $hostlist = ref $self->{hostname} eq 'ARRAY' ? |
---|
8888 | join(", ",@{$self->{hostname}}) : $self->{hostname}; |
---|
8889 | do_log(4,"connect_to_ldap: trying $hostlist"); |
---|
8890 | $ldap = Net::LDAP->new($self->{hostname}, |
---|
8891 | port => $self->{port}, |
---|
8892 | version => $self->{version}, |
---|
8893 | timeout => $self->{timeout}, |
---|
8894 | onerror => 'undef'); |
---|
8895 | if ($ldap) { |
---|
8896 | do_log(3,"connect_to_ldap: connected to $hostlist"); |
---|
8897 | } else { |
---|
8898 | do_log(-1,"connect_to_ldap: unable to connect to host $hostlist"); |
---|
8899 | return undef; |
---|
8900 | } |
---|
8901 | if ($self->{tls}) { # TLS required |
---|
8902 | my $tlsVer = $ldap->start_tls(verify=>'none'); |
---|
8903 | do_log(3,"connect_to_ldap: TLS version $tlsVer enabled"); |
---|
8904 | } |
---|
8905 | if ($self->{bind_dn}) { # binding required |
---|
8906 | if ($ldap->bind($self->{bind_dn}, password => $self->{bind_password})) { |
---|
8907 | do_log(3,"connect_to_ldap: bind $self->{bind_dn} succeeded"); |
---|
8908 | } else { |
---|
8909 | do_log(-1,"connect_to_ldap: bind $self->{bind_dn} failed"); |
---|
8910 | return undef; |
---|
8911 | } |
---|
8912 | } |
---|
8913 | $ldap_connected = 1 if $ldap; |
---|
8914 | $ldap; |
---|
8915 | } |
---|
8916 | |
---|
8917 | sub store_ldap($$) { |
---|
8918 | my($self,$ldap) = @_; |
---|
8919 | $self->{ldap} = $ldap; # save LDAP handle |
---|
8920 | $self->clear_cache; # let's start afresh just in case |
---|
8921 | $self; |
---|
8922 | } |
---|
8923 | |
---|
8924 | sub clear_cache { |
---|
8925 | my($self) = @_; |
---|
8926 | delete $self->{cache}; |
---|
8927 | } |
---|
8928 | |
---|
8929 | sub lookup_ldap($$$) { |
---|
8930 | my($self,$addr,$get_all) = @_; |
---|
8931 | my(@matchingkey,@result); |
---|
8932 | if (exists $self->{cache} && exists $self->{cache}->{$addr}) { # cached? |
---|
8933 | my($c) = $self->{cache}->{$addr}; @result = @$c if ref $c; |
---|
8934 | @matchingkey = map {'/cached/'} @result; # will do for now, improve some day |
---|
8935 | if (!ll(5)) { |
---|
8936 | # don't bother preparing log report which will not be printed |
---|
8937 | } elsif (!@result) { |
---|
8938 | do_log(5,"lookup_ldap (cached): \"$addr\" no match"); |
---|
8939 | } else { |
---|
8940 | for my $m (@result) { |
---|
8941 | do_log(5, sprintf("lookup_ldap (cached): \"%s\" matches, result=(%s)", |
---|
8942 | $addr, join(", ", map { sprintf("%s=>%s", $_, |
---|
8943 | !defined($m->{$_})?'-':'"'.$m->{$_}.'"' |
---|
8944 | ) } sort keys(%$m) ) )); |
---|
8945 | } |
---|
8946 | } |
---|
8947 | if (!$get_all) { |
---|
8948 | return(!wantarray ? $result[0] : ($result[0], $matchingkey[0])); |
---|
8949 | } else { |
---|
8950 | return(!wantarray ? \@result : (\@result, \@matchingkey)); |
---|
8951 | } |
---|
8952 | } |
---|
8953 | if (!$ldap_connected) { |
---|
8954 | my($ldap) = $self->connect_to_ldap; |
---|
8955 | defined($ldap) or die "LDAP server(s) not reachable"; |
---|
8956 | $self->store_ldap($ldap); |
---|
8957 | section_time('ldap-connect'); |
---|
8958 | } |
---|
8959 | my($is_local); # LDAP is not looked up to avoid recursion! |
---|
8960 | $is_local = Amavis::Lookup::lookup(0,$addr, |
---|
8961 | grep {ref ne 'Amavis::Lookup::SQL' && |
---|
8962 | ref ne 'Amavis::Lookup::SQLfield' && |
---|
8963 | ref ne 'Amavis::Lookup::LDAP' && |
---|
8964 | ref ne 'Amavis::Lookup::LDAPattr'} |
---|
8965 | @{ca('local_domains_maps')}); |
---|
8966 | my($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local); |
---|
8967 | my(@keys) = @$keys_ref; |
---|
8968 | $_ = untaint($_) for @keys; # untaint keys |
---|
8969 | do_log(4,sprintf("lookup_ldap \"%s\", query keys: %s, %s", |
---|
8970 | $addr, join(', ',map{"\"$_\""}@keys), $self->{query_filter})); |
---|
8971 | my($result); |
---|
8972 | eval { |
---|
8973 | snmp_count('OpsLDAPSearch'); |
---|
8974 | for my $key (@keys) { |
---|
8975 | my($match) = {}; |
---|
8976 | (my $filter = $self->{query_filter}) =~ s/%m/$key/g; |
---|
8977 | do_log(9,sprintf( |
---|
8978 | "lookup_ldap: searching base=\"%s\", scope=\"%s\", filter=\"%s\"", |
---|
8979 | $self->{base}, $self->{scope}, $filter)); |
---|
8980 | $result = $self->{ldap}->search(base => $self->{base}, |
---|
8981 | scope => $self->{scope}, |
---|
8982 | filter => $filter, |
---|
8983 | attrs => [@ldap_attrs],); |
---|
8984 | my $entry = $result->entry(0); # only use first record returned |
---|
8985 | next unless $entry; # no entry found, try next key |
---|
8986 | for my $attr (@ldap_attrs) { |
---|
8987 | my($value); |
---|
8988 | $attr = lc($attr); |
---|
8989 | do_log(9,"lookup_ldap: reading attribute \"$attr\" from object"); |
---|
8990 | if ($attr =~ /^amavis(white|black)listsender\z/) { # multivalued (list) |
---|
8991 | $value = $entry->get_value($attr, asref => 1); |
---|
8992 | } else { |
---|
8993 | $value = $entry->get_value($attr); |
---|
8994 | } |
---|
8995 | $match->{$attr} = $value if $value; |
---|
8996 | } |
---|
8997 | if (!exists $match->{'amavislocal'} && $match->{'mail'} eq '@.') { |
---|
8998 | # NOTE: see lookup_sql |
---|
8999 | $match->{'amavislocal'} = undef; |
---|
9000 | do_log(5,"lookup_ldap: \"$addr\" matches catchall, amavislocal=>undef"); |
---|
9001 | } |
---|
9002 | push(@result, {%$match}); # copy hash |
---|
9003 | push(@matchingkey, join(", ", map { sprintf("%s=>%s", $_, |
---|
9004 | !defined($match->{$_})?'-':'"'.$match->{$_}.'"' ) } keys(%$match))); |
---|
9005 | last if !$get_all; |
---|
9006 | } |
---|
9007 | }; # eval |
---|
9008 | if ($@ ne '') { |
---|
9009 | my($err) = $@; |
---|
9010 | do_log(-1,"lookup_ldap: $err"); |
---|
9011 | if (!$result || $result->code() != 'LDAP_SUCCESS') { |
---|
9012 | my $code = $result?$result->code():-1; |
---|
9013 | my $errname = Net::LDAP::Util::ldap_error_name->($code); |
---|
9014 | if ($errname eq 'LDAP_PARAM_ERROR') { |
---|
9015 | do_log(-1,"NOTICE: LDAP error - LDAP_PARAM_ERROR"); |
---|
9016 | } |
---|
9017 | else { # probably lost connection to server |
---|
9018 | do_log(-1,"NOTICE: Check LDAP server, lost connection?"); |
---|
9019 | $ldap_connected = 0; $self->{ldap}->disconnect; $self->{ldap} = undef; |
---|
9020 | } |
---|
9021 | } |
---|
9022 | die $err; |
---|
9023 | } |
---|
9024 | if (!ll(4)) { |
---|
9025 | # don't bother preparing log report which will not be printed |
---|
9026 | } elsif (!@result) { |
---|
9027 | do_log(4,"lookup_ldap, \"$addr\" no match") |
---|
9028 | } else { |
---|
9029 | do_log(4,"lookup_ldap($addr) matches, result=($_)") for @matchingkey; |
---|
9030 | } |
---|
9031 | # save for future use, but only within processing of this message |
---|
9032 | $self->{cache}->{$addr} = \@result; |
---|
9033 | section_time('lookup_ldap'); |
---|
9034 | if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) } |
---|
9035 | else { !wantarray ? \@result : (\@result, \@matchingkey) } |
---|
9036 | } |
---|
9037 | |
---|
9038 | 1; |
---|
9039 | |
---|
9040 | __DATA__ |
---|
9041 | # |
---|
9042 | package Amavis::In::AMCL; |
---|
9043 | use strict; |
---|
9044 | use re 'taint'; |
---|
9045 | |
---|
9046 | BEGIN { |
---|
9047 | use Exporter (); |
---|
9048 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
9049 | $VERSION = '2.034'; |
---|
9050 | @ISA = qw(Exporter); |
---|
9051 | } |
---|
9052 | |
---|
9053 | use subs @EXPORT; |
---|
9054 | use Errno qw(ENOENT EACCES); |
---|
9055 | use IO::File (); |
---|
9056 | |
---|
9057 | BEGIN { |
---|
9058 | import Amavis::Conf qw(:platform :confvars c cr ca); |
---|
9059 | import Amavis::Util qw(ll do_log debug_oneshot snmp_counters_init snmp_count |
---|
9060 | am_id new_am_id untaint rmdir_recursively); |
---|
9061 | import Amavis::Lookup qw(lookup); |
---|
9062 | import Amavis::Timing qw(section_time); |
---|
9063 | import Amavis::rfc2821_2822_Tools; |
---|
9064 | import Amavis::In::Message; |
---|
9065 | import Amavis::In::Connection; |
---|
9066 | import Amavis::Out::EditHeader qw(hdr); |
---|
9067 | import Amavis::rfc2821_2822_Tools qw(/^EX_/); |
---|
9068 | } |
---|
9069 | |
---|
9070 | sub new($) { my($class) = @_; bless {}, $class } |
---|
9071 | |
---|
9072 | # (used with sendmail milter and traditional (non-SMTP) MTA interface) |
---|
9073 | # |
---|
9074 | sub process_policy_request($$$$) { |
---|
9075 | my($self, $sock, $conn, $check_mail, $old_amcl) = @_; |
---|
9076 | # $sock: connected socket from Net::Server |
---|
9077 | # $conn: information about client connection |
---|
9078 | # $check_mail: subroutine ref to be called with file handle |
---|
9079 | |
---|
9080 | my(%attr); |
---|
9081 | $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count); |
---|
9082 | if ($old_amcl) { |
---|
9083 | # Accept a single request from traditional amavis helper program. |
---|
9084 | # Receive TEMPDIR/SENDER/RCPTS/LDA/LDAARGS from client |
---|
9085 | # Simple protocol: \2 means LDA follows; \3 means EOT (end of transmission) |
---|
9086 | my($state) = 0; $attr{'request'} = 'AM.CL'; my($response) = "\001"; |
---|
9087 | my($rv,@recips,@ldaargs,$inbuff); local($1); |
---|
9088 | my(@attr_names) = qw(tempdir sender recipient ldaargs); |
---|
9089 | while (defined($rv = recv($sock, $inbuff, 8192, 0))) { |
---|
9090 | $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count); |
---|
9091 | if ($state < 2) { |
---|
9092 | $attr{$attr_names[$state]} = $inbuff; $state++; |
---|
9093 | } elsif ($state == 2 && $inbuff eq "\002") { |
---|
9094 | $state++; |
---|
9095 | } elsif ($state >= 2 && $inbuff eq "\003") { |
---|
9096 | section_time('got data'); |
---|
9097 | $attr{'recipient'} = \@recips; $attr{'ldaargs'} = \@ldaargs; |
---|
9098 | $attr{'delivery_care_of'} = @ldaargs ? 'client' : 'server'; |
---|
9099 | eval { |
---|
9100 | my($msginfo) = preprocess_policy_query(\%attr); |
---|
9101 | $response = (map { /^exit_code=(\d+)\z/ ? $1 : () } |
---|
9102 | check_amcl_policy($conn,$msginfo,$check_mail,1))[0]; |
---|
9103 | }; |
---|
9104 | if ($@ ne '') { |
---|
9105 | chomp($@); do_log(-2,"policy_server FAILED: $@"); |
---|
9106 | $response = EX_TEMPFAIL; |
---|
9107 | } |
---|
9108 | $state = 4; |
---|
9109 | } elsif ($state == 2) { |
---|
9110 | push(@recips, $inbuff); |
---|
9111 | } else { |
---|
9112 | push(@ldaargs, $inbuff); |
---|
9113 | } |
---|
9114 | defined(send($sock,$response,0)) |
---|
9115 | or die "send failed in state $state: $!"; |
---|
9116 | last if $state >= 4; |
---|
9117 | $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count); |
---|
9118 | } |
---|
9119 | if ($state==4 && defined($rv)) { |
---|
9120 | # normal termination |
---|
9121 | } elsif (!defined($rv) && $! != 0) { |
---|
9122 | die "recv failed in state $state: $!"; |
---|
9123 | } else { # eof or a runaway state |
---|
9124 | die "helper client session terminated unexpectedly, state: $state"; |
---|
9125 | } |
---|
9126 | do_log(2, Amavis::Timing::report()); # report elapsed times |
---|
9127 | |
---|
9128 | } else { # new amavis helper protocol AM.PDP or a Postfix policy server |
---|
9129 | # for Postfix policy server see Postfix docs SMTPD_POLICY_README |
---|
9130 | my(@response); local($1,$2,$3); |
---|
9131 | local($/) = "\012"; # set line terminator to LF (Postfix idiosyncrasy) |
---|
9132 | $! = undef; |
---|
9133 | while(<$sock>) { # can accept multiple tasks |
---|
9134 | $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count); |
---|
9135 | Amavis::Timing::init(); snmp_counters_init(); |
---|
9136 | # must not use \r and \n, not \015 and \012 on certain platforms |
---|
9137 | if (/^\015?\012\z/) { # end of request |
---|
9138 | section_time('got data'); |
---|
9139 | eval { |
---|
9140 | my($msginfo) = preprocess_policy_query(\%attr); |
---|
9141 | @response = $attr{'request'} eq 'smtpd_access_policy' |
---|
9142 | ? postfix_policy($conn,$msginfo,\%attr) |
---|
9143 | : check_amcl_policy($conn,$msginfo,$check_mail,0); |
---|
9144 | }; |
---|
9145 | if ($@ ne '') { |
---|
9146 | chomp($@); do_log(-2, "policy_server FAILED: $@"); |
---|
9147 | @response = (proto_encode('setreply','450','4.5.0',"Failure: $@"), |
---|
9148 | proto_encode('return_value','tempfail'), |
---|
9149 | proto_encode('exit_code',sprintf("%d",EX_TEMPFAIL))); |
---|
9150 | # last; |
---|
9151 | } |
---|
9152 | $sock->print( map { $_."\015\012" } (@response,'') ) |
---|
9153 | or die "Can't write response to socket: $!"; |
---|
9154 | %attr = (); @response = (); |
---|
9155 | do_log(2, Amavis::Timing::report()); |
---|
9156 | } elsif (/^ ([^=\000\012]*?) (=|:[ \t]*) |
---|
9157 | ([^\012]*?) \015?\012 \z/xsi) { |
---|
9158 | my($attr_name) = Amavis::tcp_lookup_decode($1); |
---|
9159 | my($attr_val) = Amavis::tcp_lookup_decode($3); |
---|
9160 | if (!exists $attr{$attr_name}) { |
---|
9161 | $attr{$attr_name} = $attr_val; |
---|
9162 | } else { |
---|
9163 | if (!ref($attr{$attr_name})) |
---|
9164 | { $attr{$attr_name} = [ $attr{$attr_name} ] } |
---|
9165 | push(@{$attr{$attr_name}}, $attr_val); |
---|
9166 | } |
---|
9167 | my($known_attr) = scalar(grep {$_ eq $attr_name} qw( |
---|
9168 | request helo_name protocol_state protocol_name queue_id |
---|
9169 | client_name client_address sender recipient) ); |
---|
9170 | do_log(!$known_attr?-1:1, "policy protocol: $attr_name=$attr_val"); |
---|
9171 | } else { |
---|
9172 | do_log(-1, "policy protocol: INVALID ATTRIBUTE LINE: $_"); |
---|
9173 | } |
---|
9174 | $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count); |
---|
9175 | $! = undef; # we'll be interested in the errno from the I/O op in while |
---|
9176 | } |
---|
9177 | if (!defined($_) && $! != 0) { die "read from client socket FAILED: $!" } |
---|
9178 | }; |
---|
9179 | $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count); |
---|
9180 | } |
---|
9181 | |
---|
9182 | # Return a new Amavis::In::Message object |
---|
9183 | # based on given policy query attributes |
---|
9184 | # |
---|
9185 | sub preprocess_policy_query($) { |
---|
9186 | my($attr_ref) = @_; |
---|
9187 | |
---|
9188 | my($msginfo) = Amavis::In::Message->new; |
---|
9189 | $msginfo->rx_time(time); # now |
---|
9190 | |
---|
9191 | # amavisd -> amavis-helper protocol query consists of any number of |
---|
9192 | # the following lines, the response is terminated by an empty line. |
---|
9193 | # The 'request=AM.PDP' is a required first field, the order of |
---|
9194 | # remaining fields is arbitrary. |
---|
9195 | # Required fields are: request, tempdir, sender, recipient (one or more) |
---|
9196 | # request=AM.PDP |
---|
9197 | # tempdir=/var/amavis/amavis-milter-MWZmu9Di |
---|
9198 | # tempdir_removed_by=client (tempdir_removed_by=server is a default) |
---|
9199 | # mail_file=/var/amavis/am.../email.txt (defaults to tempdir/email.txt) |
---|
9200 | # sender=<foo@example.com> |
---|
9201 | # recipient=<bar1@example.net> |
---|
9202 | # recipient=<bar2@example.net> |
---|
9203 | # recipient=<bar3@example.net> |
---|
9204 | # delivery_care_of=server (client or server, client is a default) |
---|
9205 | # queue_id=qid |
---|
9206 | # protocol_name=ESMTP |
---|
9207 | # helo_name=b.example.com |
---|
9208 | # client_address=10.2.3.4 |
---|
9209 | |
---|
9210 | my($sender,@recips); |
---|
9211 | $msginfo->delivery_method( |
---|
9212 | lc($attr_ref->{'delivery_care_of'}) eq 'server' ? c('forward_method') :''); |
---|
9213 | $msginfo->client_delete(lc($attr_ref->{'tempdir_removed_by'}) eq 'client' |
---|
9214 | ? 1 : 0); |
---|
9215 | $msginfo->queue_id($attr_ref->{'queue_id'}) |
---|
9216 | if exists $attr_ref->{'queue_id'}; |
---|
9217 | $msginfo->client_addr($attr_ref->{'client_address'}) |
---|
9218 | if exists $attr_ref->{'client_address'}; |
---|
9219 | $msginfo->client_name($attr_ref->{'client_name'}) |
---|
9220 | if exists $attr_ref->{'client_name'}; |
---|
9221 | $msginfo->client_proto($attr_ref->{'protocol_name'}) |
---|
9222 | if exists $attr_ref->{'protocol_name'}; |
---|
9223 | $msginfo->client_helo($attr_ref->{'helo_name'}) |
---|
9224 | if exists $attr_ref->{'helo_name'}; |
---|
9225 | if (exists $attr_ref->{'sender'}) { |
---|
9226 | $sender = $attr_ref->{'sender'}; |
---|
9227 | $sender = unquote_rfc2821_local($sender); |
---|
9228 | $msginfo->sender($sender); |
---|
9229 | } |
---|
9230 | if (exists $attr_ref->{'recipient'}) { |
---|
9231 | my($r) = $attr_ref->{'recipient'}; |
---|
9232 | @recips = !ref($r) ? $r : @$r; |
---|
9233 | $_ = unquote_rfc2821_local($_) for @recips; |
---|
9234 | $msginfo->recips(\@recips); |
---|
9235 | } |
---|
9236 | if (!exists $attr_ref->{'tempdir'}) { |
---|
9237 | $msginfo->mail_tempdir($TEMPBASE); # defaults to $TEMPBASE |
---|
9238 | } else { |
---|
9239 | local($1,$2); my($tempdir) = $attr_ref->{tempdir}; |
---|
9240 | $tempdir =~ /^ (?: \Q$TEMPBASE\E | \Q$MYHOME\E ) |
---|
9241 | \/ (?! \.{1,2} \z) [A-Za-z0-9_.-]+ \z/xso |
---|
9242 | or die "Invalid/unexpected temporary directory name '$tempdir'"; |
---|
9243 | $msginfo->mail_tempdir(untaint($tempdir)); |
---|
9244 | } |
---|
9245 | if (!exists $attr_ref->{'mail_file'}) { |
---|
9246 | $msginfo->mail_text_fn($msginfo->mail_tempdir . '/email.txt'); |
---|
9247 | } else { |
---|
9248 | # SECURITY: just believe the supplied file name, blindly untainting it |
---|
9249 | $msginfo->mail_text_fn(untaint($attr_ref->{'mail_file'})); |
---|
9250 | } |
---|
9251 | if ($msginfo->mail_text_fn ne '') { |
---|
9252 | my($fname) = $msginfo->mail_text_fn; |
---|
9253 | do_log(5, "preprocess_policy_query: opening mail file '$fname'"); |
---|
9254 | # set new amavis message id |
---|
9255 | new_am_id( ($fname =~ m{amavis-(milter-)?([^/]+?)\z}s ? $2 : undef) ); |
---|
9256 | # file created by amavis helper program, just open it |
---|
9257 | my($fh) = IO::File->new; |
---|
9258 | $fh->open($fname,'<') or die "Can't open file $fname: $!"; |
---|
9259 | binmode($fh,":bytes") or die "Can't cancel :utf8 mode: $!" |
---|
9260 | if $unicode_aware; |
---|
9261 | $msginfo->mail_text($fh); # save file handle to object |
---|
9262 | } |
---|
9263 | if ($attr_ref->{'request'} =~ /^AM\.(CL|PDP)\z/) { |
---|
9264 | do_log(1, sprintf("%s %s: <%s> -> %s", |
---|
9265 | $attr_ref->{'request'}, $msginfo->mail_tempdir, $sender, |
---|
9266 | join(',', qquote_rfc2821_local(@recips)) )); |
---|
9267 | } else { |
---|
9268 | do_log(1, sprintf("%s(%s): %s %s: %s[%s] <%s> -> <%s>", |
---|
9269 | @$attr_ref{qw(request protocol_state protocol_name queue_id |
---|
9270 | client_name client_address sender recipient)})); |
---|
9271 | } |
---|
9272 | $msginfo; |
---|
9273 | } |
---|
9274 | |
---|
9275 | sub check_amcl_policy($$$$) { |
---|
9276 | my($conn,$msginfo,$check_mail,$old_amcl) = @_; |
---|
9277 | |
---|
9278 | my($smtp_resp, $exit_code, $preserve_evidence); |
---|
9279 | my(%baseline_policy_bank); my($policy_changed) = 0; |
---|
9280 | %baseline_policy_bank = %current_policy_bank; |
---|
9281 | # do some sanity checks before deciding to call check_mail() |
---|
9282 | if (!ref($msginfo->per_recip_data) || !defined($msginfo->mail_text)) { |
---|
9283 | $smtp_resp = '450 4.5.0 Incomplete request'; $exit_code = EX_TEMPFAIL; |
---|
9284 | } else { |
---|
9285 | my($cl_ip) = $msginfo->client_addr; |
---|
9286 | if ($cl_ip ne '' && defined $policy_bank{'MYNETS'} |
---|
9287 | && lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) { |
---|
9288 | Amavis::load_policy_bank('MYNETS'); $policy_changed = 1; |
---|
9289 | } |
---|
9290 | debug_oneshot(1) if lookup(0,$msginfo->sender,@{ca('debug_sender_maps')}); |
---|
9291 | # check_mail() expects open file on $fh, need not be rewound |
---|
9292 | my($fh) = $msginfo->mail_text; my($tempdir) = $msginfo->mail_tempdir; |
---|
9293 | ($smtp_resp, $exit_code, $preserve_evidence) = |
---|
9294 | &$check_mail($conn,$msginfo,0,$tempdir); |
---|
9295 | $fh->close or die "Can't close temp file: $!" if $fh; |
---|
9296 | $fh = undef; $msginfo->mail_text(undef); |
---|
9297 | my($errn) = $tempdir eq '' ? ENOENT : (stat($tempdir) ? 0 : 0+$!); |
---|
9298 | if ($tempdir eq '' || $errn == ENOENT) { |
---|
9299 | # do nothing |
---|
9300 | } elsif ($msginfo->client_delete) { |
---|
9301 | do_log(4, "AM.PDP: deletion of $tempdir is client's responsibility"); |
---|
9302 | } elsif ($preserve_evidence) { |
---|
9303 | do_log(-1,"AM.PDP: tempdir is to be PRESERVED: $tempdir"); |
---|
9304 | } else { |
---|
9305 | do_log(4, "AM.PDP: tempdir being removed: $tempdir"); |
---|
9306 | my($fname) = $msginfo->mail_text_fn; |
---|
9307 | unlink($fname) or die "Can't remove file $fname: $!" if $fname ne ''; |
---|
9308 | rmdir_recursively($tempdir); |
---|
9309 | } |
---|
9310 | } |
---|
9311 | # amavisd -> amavis-helper protocol response consists of any number of |
---|
9312 | # the following lines, the response is terminated by an empty line |
---|
9313 | # addrcpt=recipient |
---|
9314 | # delrcpt=recipient |
---|
9315 | # addheader=hdr_head hdr_body |
---|
9316 | # chgheader=index hdr_head hdr_body |
---|
9317 | # delheader=index hdr_head |
---|
9318 | # replacebody=new_body (not implemented) |
---|
9319 | # return_value=continue|reject|discard|accept|tempfail |
---|
9320 | # setreply=rcode xcode message |
---|
9321 | # exit_code=n (old amavis helper, not applicable to milter) |
---|
9322 | |
---|
9323 | my(@response); my($rcpt_deletes,$rcpt_count)=(0,0); |
---|
9324 | if (ref($msginfo->per_recip_data)) { |
---|
9325 | for my $r (@{$msginfo->per_recip_data}) |
---|
9326 | { $rcpt_count++; if ($r->recip_done) { $rcpt_deletes++ } } |
---|
9327 | } |
---|
9328 | if ($smtp_resp=~/^([1-5]\d\d) ([245]\.\d{1,3}\.\d{1,3})(?: |\z)(.*)\z/s) |
---|
9329 | { push(@response, proto_encode('setreply', $1,$2,$3)) } |
---|
9330 | if ( $exit_code == EX_TEMPFAIL) { |
---|
9331 | push(@response, proto_encode('return_value','tempfail')); |
---|
9332 | } elsif ($exit_code == EX_NOUSER) { # reject the whole message |
---|
9333 | push(@response, proto_encode('return_value','reject')); |
---|
9334 | } elsif ($exit_code == EX_UNAVAILABLE) { # reject the whole message |
---|
9335 | push(@response, proto_encode('return_value','reject')); |
---|
9336 | } elsif ($exit_code == 99) { # discard the whole message |
---|
9337 | push(@response, proto_encode('return_value','discard')); |
---|
9338 | } elsif ($msginfo->delivery_method ne '') { # explicit forwarding by server |
---|
9339 | $rcpt_count==$rcpt_deletes or die "Not all recips done"; # just in case |
---|
9340 | # MTA is relieved of duty to deliver a message, amavisd did the forwarding |
---|
9341 | $exit_code = EX_OK; # *** 99 or EX_OK; ??? (doesn't really matter with |
---|
9342 | # helper client programs which can't do the delivery) |
---|
9343 | push(@response, proto_encode('return_value','continue')); # 'discard' ??? |
---|
9344 | } elsif ($rcpt_count-$rcpt_deletes <= 0) { # none left, should be discarded |
---|
9345 | # discarding could have been requested (?) |
---|
9346 | do_log(-1, "WARN: no recips left (forgot to set ". |
---|
9347 | "\$forward_method=undef using milter?), $smtp_resp"); |
---|
9348 | $exit_code = 99; |
---|
9349 | push(@response, proto_encode('return_value','discard')); |
---|
9350 | } else { # EX_OK |
---|
9351 | for my $r (@{$msginfo->per_recip_data}) { # modified recipient addresses? |
---|
9352 | my($addr,$newaddr) = ($r->recip_addr, $r->recip_final_addr); |
---|
9353 | if ($r->recip_done) { # delete |
---|
9354 | push(@response, proto_encode('delrcpt', |
---|
9355 | quote_rfc2821_local($addr))); |
---|
9356 | } elsif ($newaddr ne $addr) { # modify, e.g. adding extension |
---|
9357 | push(@response, proto_encode('delrcpt', |
---|
9358 | quote_rfc2821_local($addr))); |
---|
9359 | push(@response, proto_encode('addrcpt', |
---|
9360 | quote_rfc2821_local($newaddr))); |
---|
9361 | } |
---|
9362 | } |
---|
9363 | my($hdr_edits) = $msginfo->header_edits; |
---|
9364 | if ($hdr_edits) { # any added or modified header fields? |
---|
9365 | local($1,$2); |
---|
9366 | # Inserting. Not posible to specify placement of header fields in milter! |
---|
9367 | for my $hf (@{$hdr_edits->{prepend}}, @{$hdr_edits->{append}}) { |
---|
9368 | if ($hf =~ /^([^:]+):[ \t]*(.*?)$/s) |
---|
9369 | { push(@response, proto_encode('addheader',$1,$2)) } |
---|
9370 | } |
---|
9371 | my($field_name,$edit,$field_body); |
---|
9372 | while ( ($field_name,$edit) = each %{$hdr_edits->{edit}} ) { |
---|
9373 | $field_body = $msginfo->mime_entity->head->get($field_name,0); |
---|
9374 | if (!defined($field_body)) { |
---|
9375 | # such header field does not exist, do nothing |
---|
9376 | } elsif (!defined($edit)) { # delete existing header field |
---|
9377 | push(@response, proto_encode('delheader',"1",$field_name)); |
---|
9378 | } else { # edit the first occurrence |
---|
9379 | chomp($field_body); |
---|
9380 | $field_body = hdr($field_name, &$edit($field_name,$field_body)); |
---|
9381 | $field_body = $1 if $field_body =~ /^[^:]+:[ \t]*(.*?)$/s; |
---|
9382 | push(@response, proto_encode('chgheader', "1", |
---|
9383 | $field_name, $field_body)); |
---|
9384 | } |
---|
9385 | } |
---|
9386 | } |
---|
9387 | if ($old_amcl) { # milter via old amavis helper program |
---|
9388 | # warn if there is anything that should be done but MTA is not capable of |
---|
9389 | # (or a helper program can not pass the request) |
---|
9390 | for (grep { /^(delrcpt|addrcpt)=/ } @response) |
---|
9391 | { do_log(-1, "WARN: MTA can't do: $_") } |
---|
9392 | if ($rcpt_deletes && $rcpt_count-$rcpt_deletes > 0) { |
---|
9393 | do_log(-1, "WARN: ACCEPT THE WHOLE MESSAGE, ". |
---|
9394 | "MTA-in can't do selective recips deletion"); |
---|
9395 | } |
---|
9396 | } |
---|
9397 | push(@response, proto_encode('return_value','continue')); |
---|
9398 | } |
---|
9399 | push(@response, proto_encode('exit_code',"$exit_code")); |
---|
9400 | do_log(2, "mail checking ended: ".join("\n",@response)); |
---|
9401 | if ($policy_changed) { |
---|
9402 | %current_policy_bank = %baseline_policy_bank; $policy_changed = 0; |
---|
9403 | } |
---|
9404 | @response; |
---|
9405 | } |
---|
9406 | |
---|
9407 | sub postfix_policy($$$) { |
---|
9408 | my($conn,$msginfo,$attr_ref) = @_; |
---|
9409 | my(@response); |
---|
9410 | if (!exists($attr_ref->{'request'})) { |
---|
9411 | die "no 'request' attribute"; |
---|
9412 | } elsif ($attr_ref->{'request'} ne 'smtpd_access_policy') { |
---|
9413 | die ("unknown 'request' value: " . $attr_ref->{'request'}); |
---|
9414 | } else { |
---|
9415 | @response = 'action=DUNNO'; |
---|
9416 | } |
---|
9417 | @response; |
---|
9418 | } |
---|
9419 | |
---|
9420 | sub proto_encode($@) { |
---|
9421 | my($attribute_name,@strings) = @_; local($1); |
---|
9422 | $attribute_name =~ # encode all but alfanumerics, '_' and '-' |
---|
9423 | s/([^0-9a-zA-Z_-])/sprintf("%%%02x",ord($1))/eg; |
---|
9424 | for (@strings) { # encode % and nonprintables |
---|
9425 | s/([^\041-\044\046-\176])/sprintf("%%%02x",ord($1))/eg; |
---|
9426 | } |
---|
9427 | ## encode % and nonprintables, but leave SP and TAB as-is |
---|
9428 | # $str =~ s/[^\011\040-\044\046-\176]/sprintf("%%%02x",ord($&))/eg; |
---|
9429 | $attribute_name . '=' . join(' ',@strings); |
---|
9430 | } |
---|
9431 | |
---|
9432 | 1; |
---|
9433 | |
---|
9434 | __DATA__ |
---|
9435 | # |
---|
9436 | package Amavis::In::SMTP; |
---|
9437 | use strict; |
---|
9438 | use re 'taint'; |
---|
9439 | |
---|
9440 | BEGIN { |
---|
9441 | use Exporter (); |
---|
9442 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
9443 | $VERSION = '2.034'; |
---|
9444 | @ISA = qw(Exporter); |
---|
9445 | } |
---|
9446 | use Errno qw(ENOENT EACCES); |
---|
9447 | use MIME::Base64; |
---|
9448 | |
---|
9449 | BEGIN { |
---|
9450 | import Amavis::Conf qw(:platform :confvars c cr ca); |
---|
9451 | import Amavis::Util qw(ll do_log am_id new_am_id snmp_counters_init |
---|
9452 | prolong_timer debug_oneshot sanitize_str |
---|
9453 | strip_tempdir rmdir_recursively); |
---|
9454 | import Amavis::Lookup qw(lookup lookup_ip_acl); |
---|
9455 | import Amavis::Timing qw(section_time); |
---|
9456 | import Amavis::rfc2821_2822_Tools; |
---|
9457 | import Amavis::In::Message; |
---|
9458 | import Amavis::In::Connection; |
---|
9459 | } |
---|
9460 | |
---|
9461 | sub new($) { |
---|
9462 | my($class) = @_; |
---|
9463 | my($self) = bless {}, $class; |
---|
9464 | $self->{sock} = undef; # SMTP socket |
---|
9465 | $self->{proto} = undef; # SMTP / ((ESMTP / LMTP) (A | S | SA)? ) |
---|
9466 | $self->{pipelining} = undef; # may we buffer responses? |
---|
9467 | $self->{smtp_outbuf} = undef; # SMTP responses buffer for PIPELINING |
---|
9468 | $self->{fh_pers} = undef; # persistent file handle for email.txt |
---|
9469 | $self->{tempdir_persistent} = undef;# temporary directory for check_mail |
---|
9470 | $self->{preserve} = undef; # don't delete tempdir on exit |
---|
9471 | $self->{tempdir_empty} = 1; # anything of interest in tempdir? |
---|
9472 | $self->{session_closed_normally} = undef; # closed properly with QUIT |
---|
9473 | $self; |
---|
9474 | } |
---|
9475 | |
---|
9476 | sub preserve_evidence # try to preserve temporary files etc in case of trouble |
---|
9477 | { my($self)=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift) } |
---|
9478 | |
---|
9479 | sub DESTROY { |
---|
9480 | my($self) = shift; |
---|
9481 | eval { do_log(5,"Amavis::In::SMTP::DESTROY called") }; |
---|
9482 | eval { |
---|
9483 | $self->{fh_pers}->close |
---|
9484 | or die "Can't close temp file: $!" if $self->{fh_pers}; |
---|
9485 | my($errn) = $self->{tempdir_pers} eq '' ? ENOENT |
---|
9486 | : (stat($self->{tempdir_pers}) ? 0 : 0+$!); |
---|
9487 | if (defined $self->{tempdir_pers} && $errn != ENOENT) { |
---|
9488 | # this will not be included in the TIMING report, |
---|
9489 | # but it only occurs infrequently and doesn't take that long |
---|
9490 | if ($self->preserve_evidence && !$self->{tempdir_empty}) { |
---|
9491 | do_log(-1,"SMTP shutdown: tempdir is to be PRESERVED: ". |
---|
9492 | $self->{tempdir_pers}); |
---|
9493 | } else { |
---|
9494 | do_log(3, sprintf("SMTP shutdown: %s is being removed: %s%s", |
---|
9495 | $self->{tempdir_empty} ? 'empty tempdir' : 'tempdir', |
---|
9496 | $self->{tempdir_pers}, |
---|
9497 | $self->preserve_evidence ? ', nothing to preserve' : '')); |
---|
9498 | rmdir_recursively($self->{tempdir_pers}); |
---|
9499 | } |
---|
9500 | } |
---|
9501 | if (! $self->{session_closed_normally}) { |
---|
9502 | $self->smtp_resp(1,"421 4.3.2 Service shutting down, closing channel"); |
---|
9503 | } |
---|
9504 | }; |
---|
9505 | if ($@ ne '') |
---|
9506 | { my($eval_stat) = $@; eval { do_log(1,"SMTP shutdown: $eval_stat") } } |
---|
9507 | } |
---|
9508 | |
---|
9509 | sub prepare_tempdir($) { |
---|
9510 | my($self) = @_; |
---|
9511 | if (! defined $self->{tempdir_pers} ) { |
---|
9512 | # invent a name for a temporary directory for this child, and create it |
---|
9513 | my($now_iso8601) = iso8601_timestamp(time,1); # or: iso8601_utc_timestamp |
---|
9514 | $self->{tempdir_pers} = sprintf("%s/amavis-%s-%05d", |
---|
9515 | $TEMPBASE, $now_iso8601, $$); |
---|
9516 | } |
---|
9517 | my($dname) = $self->{tempdir_pers}; |
---|
9518 | my(@stat_list) = lstat($dname); my($errn) = @stat_list ? 0 : 0+$!; |
---|
9519 | if (!$errn && ! -d _) { # exists, but is not a directory !? |
---|
9520 | die "prepare_tempdir: $dname is not a directory!!!"; |
---|
9521 | } elsif (!$errn) { |
---|
9522 | my($dev,$ino) = @stat_list; |
---|
9523 | if ($dev != $self->{tempdir_dev} || $ino != $self->{tempdir_ino}) { |
---|
9524 | do_log(-1,"prepare_tempdir: $dname is no longer the same directory!!!"); |
---|
9525 | ($self->{tempdir_dev},$self->{tempdir_ino}) = @stat_list; |
---|
9526 | } |
---|
9527 | } elsif ($errn == ENOENT) { |
---|
9528 | do_log(4,"prepare_tempdir: creating directory $dname"); |
---|
9529 | mkdir($dname,0750) or die "Can't create directory $dname: $!"; |
---|
9530 | ($self->{tempdir_dev},$self->{tempdir_ino}) = lstat($dname); |
---|
9531 | $self->{tempdir_empty} = 1; |
---|
9532 | section_time('mkdir tempdir'); |
---|
9533 | } |
---|
9534 | # prepare temporary file for writing (and reading later) |
---|
9535 | my($fname) = $dname . '/email.txt'; |
---|
9536 | @stat_list = lstat($fname); $errn = @stat_list ? 0 : 0+$!; |
---|
9537 | if (!$errn && ! -f _) { # exists, but is not a regular file !? |
---|
9538 | die "prepare_tempdir: $fname is not a regular file!!!"; |
---|
9539 | } elsif ($self->{fh_pers} && !$errn && -f _) { |
---|
9540 | my($dev,$ino) = @stat_list; |
---|
9541 | if ($dev != $self->{file_dev} || $ino != $self->{file_ino}) { |
---|
9542 | do_log(-1,"prepare_tempdir: $fname is no longer the same file!!!"); |
---|
9543 | ($self->{file_dev}, $self->{file_ino}) = @stat_list; |
---|
9544 | } |
---|
9545 | $self->{fh_pers}->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
9546 | $self->{fh_pers}->truncate(0) or die "Can't truncate mail file: $!"; |
---|
9547 | } else { |
---|
9548 | do_log(4,"prepare_tempdir: creating file $fname"); |
---|
9549 | $self->{fh_pers} = IO::File->new($fname,'+>',0640) |
---|
9550 | or die "Can't create file $fname: $!"; |
---|
9551 | ($self->{file_dev}, $self->{file_ino}) = lstat($fname); |
---|
9552 | section_time('create email.txt'); |
---|
9553 | } |
---|
9554 | } |
---|
9555 | |
---|
9556 | sub authenticate($$$) { |
---|
9557 | my($state,$auth_mech,$auth_resp) = @_; |
---|
9558 | my($result,$newchallenge); |
---|
9559 | if ($auth_mech eq 'ANONYMOUS') { # rfc2245 |
---|
9560 | $result = [$auth_resp,undef]; |
---|
9561 | } elsif ($auth_mech eq 'PLAIN') { # rfc2595, "user\0authname\0pass" |
---|
9562 | if (!defined($auth_resp)) { $newchallenge = '' } |
---|
9563 | else { $result = [ (split(/\000/,$auth_resp,-1))[0,2] ] } |
---|
9564 | } elsif ($auth_mech eq 'LOGIN' && !defined $state) { |
---|
9565 | $newchallenge = 'Username:'; $state = []; |
---|
9566 | } elsif ($auth_mech eq 'LOGIN' && @$state==0) { |
---|
9567 | push(@$state, $auth_resp); $newchallenge = 'Password:'; |
---|
9568 | } elsif ($auth_mech eq 'LOGIN' && @$state==1) { |
---|
9569 | push(@$state, $auth_resp); $result = $state; |
---|
9570 | } # CRAM-MD5:rfc2195, DIGEST-MD5:rfc2831 |
---|
9571 | ($state,$result,$newchallenge); |
---|
9572 | } |
---|
9573 | |
---|
9574 | # Accept a SMTP or LMTP connect (which can do any number of SMTP transactions) |
---|
9575 | # and call content checking for each message received |
---|
9576 | # |
---|
9577 | sub process_smtp_request($$$$) { |
---|
9578 | my($self, $sock, $lmtp, $conn, $check_mail) = @_; |
---|
9579 | # $sock: connected socket from Net::Server |
---|
9580 | # $lmtp: use LMTP protocol instead of (E)SMTP |
---|
9581 | # $conn: information about client connection |
---|
9582 | # $check_mail: subroutine ref to be called with file handle |
---|
9583 | |
---|
9584 | my($msginfo,$authenticated,$auth_user,$auth_pass); |
---|
9585 | $self->{sock} = $sock; |
---|
9586 | $self->{pipelining} = 0; # may we buffer responses? |
---|
9587 | $self->{smtp_outbuf} = []; # SMTP responses buffer for PIPELINING |
---|
9588 | |
---|
9589 | my($myheloname); |
---|
9590 | # $myheloname = $myhostname; |
---|
9591 | # $myheloname = 'localhost'; |
---|
9592 | # $myheloname = '[127.0.0.1]'; |
---|
9593 | $myheloname = '[' . $conn->socket_ip . ']'; |
---|
9594 | |
---|
9595 | new_am_id(undef, $Amavis::child_invocation_count, undef); |
---|
9596 | my($initial_am_id) = 1; my($sender,@recips); my($got_rcpt); |
---|
9597 | my($max_recip_size_limit); # maximum of per-recipient message size limits |
---|
9598 | my($terminating,$aborting,$eof,$voluntary_exit); my($seq) = 0; |
---|
9599 | my(%xforward_args); my(%baseline_policy_bank); my($policy_changed); |
---|
9600 | %baseline_policy_bank = %current_policy_bank; $policy_changed = 0; |
---|
9601 | $conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'SMTP'); |
---|
9602 | |
---|
9603 | # system-wide message size limit, if any |
---|
9604 | my($message_size_limit) = c('smtpd_message_size_limit'); |
---|
9605 | if ($message_size_limit && $message_size_limit < 65536) |
---|
9606 | { $message_size_limit = 65536 } # rfc2821 requires at least 64k |
---|
9607 | my($smtpd_greeting_banner_tmp) = c('smtpd_greeting_banner'); |
---|
9608 | $smtpd_greeting_banner_tmp =~ |
---|
9609 | s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) } |
---|
9610 | { { 'helo-name' => $myheloname, |
---|
9611 | 'version' => $myversion, |
---|
9612 | 'version-id' => $myversion_id, |
---|
9613 | 'version-date' => $myversion_date, |
---|
9614 | 'product' => $myproduct_name, |
---|
9615 | 'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)} |
---|
9616 | }egx; |
---|
9617 | $self->smtp_resp(1, "220 $smtpd_greeting_banner_tmp"); |
---|
9618 | |
---|
9619 | $0 = sprintf("amavisd (ch%d-idle)", $Amavis::child_invocation_count); |
---|
9620 | Amavis::Timing::go_idle(4); |
---|
9621 | undef $!; |
---|
9622 | while(<$sock>) { |
---|
9623 | $0 = sprintf("amavisd (ch%d-%s)", |
---|
9624 | $Amavis::child_invocation_count, am_id()); |
---|
9625 | Amavis::Timing::go_busy(5); |
---|
9626 | prolong_timer('reading SMTP command'); |
---|
9627 | { # a block is used as a 'switch' statement - 'last' will exit from it |
---|
9628 | my($cmd) = $_; |
---|
9629 | do_log(4, $self->{proto} . "< $cmd"); |
---|
9630 | !/^ \s* ([A-Za-z]+) (?: \s+ (.*?) )? \s* \015\012 \z/xs && do { |
---|
9631 | $self->smtp_resp(1,"500 5.5.2 Error: bad syntax", 1, $cmd); last; |
---|
9632 | }; |
---|
9633 | $_ = uc($1); my($args) = $2; |
---|
9634 | /^RSET|DATA|QUIT\z/ && $args ne '' && do { |
---|
9635 | $self->smtp_resp(1,"501 5.5.4 Error: $_ does not accept arguments", |
---|
9636 | 1,$cmd); |
---|
9637 | last; |
---|
9638 | }; |
---|
9639 | /^RSET\z/ && do { $sender = undef; @recips = (); $got_rcpt = 0; |
---|
9640 | $max_recip_size_limit = undef; $msginfo = undef; |
---|
9641 | if ($policy_changed) { |
---|
9642 | %current_policy_bank = %baseline_policy_bank; |
---|
9643 | $policy_changed = 0; |
---|
9644 | } |
---|
9645 | $self->smtp_resp(0,"250 2.0.0 Ok $_"); last }; |
---|
9646 | /^NOOP\z/ && do { $self->smtp_resp(1,"250 2.0.0 Ok $_"); last }; |
---|
9647 | /^QUIT\z/ && do { |
---|
9648 | my($smtpd_quit_banner_tmp) = c('smtpd_quit_banner'); |
---|
9649 | $smtpd_quit_banner_tmp =~ |
---|
9650 | s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) } |
---|
9651 | { { 'helo-name' => $myheloname, |
---|
9652 | 'version' => $myversion, |
---|
9653 | 'version-id' => $myversion_id, |
---|
9654 | 'version-date' => $myversion_date, |
---|
9655 | 'product' => $myproduct_name, |
---|
9656 | 'protocol' => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)} |
---|
9657 | }egx; |
---|
9658 | $self->smtp_resp(1,"221 2.0.0 $smtpd_quit_banner_tmp"); |
---|
9659 | $terminating=1; last; |
---|
9660 | }; |
---|
9661 | ### !$lmtp && /^HELO\z/ && do { # strict |
---|
9662 | /^HELO\z/ && do { |
---|
9663 | $sender = undef; @recips = (); $got_rcpt = 0; # implies RSET |
---|
9664 | $max_recip_size_limit = undef; $msginfo = undef; # forget previous |
---|
9665 | if ($policy_changed) |
---|
9666 | { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 } |
---|
9667 | $self->{pipelining} = 0; $self->smtp_resp(0,"250 $myheloname"); |
---|
9668 | $lmtp = 0; $conn->smtp_proto($self->{proto} = 'SMTP'); |
---|
9669 | $conn->smtp_helo($args); section_time('SMTP HELO'); last; |
---|
9670 | }; |
---|
9671 | ### (!$lmtp && /^EHLO\z/ || $lmtp && /^LHLO\z/) && do { # strict |
---|
9672 | (/^EHLO\z/ || /^LHLO\z/) && do { |
---|
9673 | $sender = undef; @recips = (); $got_rcpt = 0; # implies RSET |
---|
9674 | $max_recip_size_limit = undef; $msginfo = undef; # forget previous |
---|
9675 | if ($policy_changed) |
---|
9676 | { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 } |
---|
9677 | $lmtp = /^LHLO\z/ ? 1 : 0; |
---|
9678 | $conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'ESMTP'); |
---|
9679 | $self->{pipelining} = 1; |
---|
9680 | $self->smtp_resp(0,"250 $myheloname\n" . join("\n", |
---|
9681 | 'PIPELINING', |
---|
9682 | !defined($message_size_limit) ? 'SIZE' |
---|
9683 | : sprintf('SIZE %d',$message_size_limit), |
---|
9684 | '8BITMIME', |
---|
9685 | 'ENHANCEDSTATUSCODES', |
---|
9686 | !@{ca('auth_mech_avail')} ? () |
---|
9687 | : join(' ','AUTH',@{ca('auth_mech_avail')}), |
---|
9688 | 'XFORWARD NAME ADDR PROTO HELO' )); |
---|
9689 | $conn->smtp_helo($args); section_time("SMTP $_"); |
---|
9690 | last; |
---|
9691 | }; |
---|
9692 | /^XFORWARD\z/ && do { # Postfix extension |
---|
9693 | if (defined($sender)) { |
---|
9694 | $self->smtp_resp(0,"503 5.5.1 Error: XFORWARD not allowed within transaction", 1, $cmd); |
---|
9695 | last; |
---|
9696 | } |
---|
9697 | my($bad); |
---|
9698 | for (split(' ',$args)) { |
---|
9699 | if (!/^( [A-Za-z0-9] [A-Za-z0-9-]* ) = ( [\041-\176]{0,255} )\z/xs) { |
---|
9700 | $self->smtp_resp(0,"501 5.5.4 Syntax error in XFORWARD parameters", |
---|
9701 | 1, $cmd); |
---|
9702 | $bad = 1; last; |
---|
9703 | } else { |
---|
9704 | my($name,$val) = (uc($1), $2); |
---|
9705 | if ($name =~ /^(?:NAME|ADDR|PROTO|HELO)\z/) { |
---|
9706 | $val = undef if uc($val) eq '[UNAVAILABLE]'; |
---|
9707 | $xforward_args{$name} = $val; |
---|
9708 | } else { |
---|
9709 | $self->smtp_resp(0,"501 5.5.4 XFORWARD command parameter error: $name=$val",1,$cmd); |
---|
9710 | $bad = 1; last; |
---|
9711 | } |
---|
9712 | } |
---|
9713 | } |
---|
9714 | $self->smtp_resp(1,"250 2.5.0 Ok") if !$bad; |
---|
9715 | last; |
---|
9716 | }; |
---|
9717 | /^HELP\z/ && do { |
---|
9718 | $self->smtp_resp(1,"214 2.0.0 See amavisd-new home page at:\n". |
---|
9719 | "http://www.ijs.si/software/amavisd/"); |
---|
9720 | last; |
---|
9721 | }; |
---|
9722 | /^AUTH\z/ && @{ca('auth_mech_avail')} && do { # rfc2554 |
---|
9723 | if ($args !~ /^([^ ]+)(?: ([^ ]*))?\z/is) { |
---|
9724 | $self->smtp_resp(0,"501 5.5.2 Syntax: AUTH mech [initresp]",1,$cmd); |
---|
9725 | last; |
---|
9726 | } |
---|
9727 | my($auth_mech,$auth_resp) = (uc($1), $2); |
---|
9728 | if ($authenticated) { |
---|
9729 | $self->smtp_resp(0,"503 5.5.1 Error: session already authenticated", 1, $cmd); |
---|
9730 | } elsif (defined($sender)) { |
---|
9731 | $self->smtp_resp(0,"503 5.5.1 Error: AUTH not allowed within transaction", 1, $cmd); |
---|
9732 | } elsif (!grep {uc($_) eq $auth_mech} @{ca('auth_mech_avail')}) { |
---|
9733 | $self->smtp_resp(0,"504 5.7.6 Error: requested authentication mechanism not supported", 1, $cmd); |
---|
9734 | } else { |
---|
9735 | my($state,$result,$challenge); |
---|
9736 | if ($auth_resp eq '=') { $auth_resp = '' } # zero length |
---|
9737 | elsif ($auth_resp eq '') { $auth_resp = undef } |
---|
9738 | for (;;) { |
---|
9739 | if ($auth_resp !~ m{^[A-Za-z0-9+/=]*\z}) { |
---|
9740 | $self->smtp_resp(0,"501 5.5.4 Authentication failed: malformed authentication response", 1, $cmd); |
---|
9741 | last; |
---|
9742 | } else { |
---|
9743 | $auth_resp = decode_base64($auth_resp) if $auth_resp ne ''; |
---|
9744 | ($state,$result,$challenge) = |
---|
9745 | authenticate($state, $auth_mech, $auth_resp); |
---|
9746 | if (ref($result) eq 'ARRAY') { |
---|
9747 | $self->smtp_resp(0,"235 2.7.1 Authentication successful"); |
---|
9748 | $authenticated = 1; ($auth_user,$auth_pass) = @$result; |
---|
9749 | do_log(2,"AUTH $auth_mech, user=$auth_user"); |
---|
9750 | # do_log(2,"AUTH $auth_mech, user=$auth_user, pass=$auth_resp"); |
---|
9751 | last; |
---|
9752 | } elsif (defined $result && !$result) { |
---|
9753 | $self->smtp_resp(0,"535 5.7.1 Authentication failed", 1, $cmd); |
---|
9754 | last; |
---|
9755 | } |
---|
9756 | } |
---|
9757 | # server challenge or ready prompt |
---|
9758 | $self->smtp_resp(1,"334 ".encode_base64($challenge,'')); |
---|
9759 | $auth_resp = <$sock>; |
---|
9760 | do_log(5, $self->{proto} . "< $auth_resp"); |
---|
9761 | $auth_resp =~ s/\015?\012\z//; |
---|
9762 | if ($auth_resp eq '*') { |
---|
9763 | $self->smtp_resp(0,"501 5.7.1 Authentication aborted"); |
---|
9764 | last; |
---|
9765 | } |
---|
9766 | } |
---|
9767 | } |
---|
9768 | last; |
---|
9769 | }; |
---|
9770 | /^VRFY\z/ && do { |
---|
9771 | $self->smtp_resp(1,"502 5.5.1 Command $_ not implemented", 1, $cmd); |
---|
9772 | # if ($args eq '') { |
---|
9773 | # $self->smtp_resp(1,"501 5.5.2 Syntax: VRFY address", 1, $cmd); |
---|
9774 | # } else { |
---|
9775 | # $self->smtp_resp(1,"252 2.0.0 Cannot VRFY user, but will accept ". |
---|
9776 | # "message and attempt delivery", 0, $cmd); |
---|
9777 | # } |
---|
9778 | last; |
---|
9779 | }; |
---|
9780 | /^MAIL\z/ && do { # begin new transaction |
---|
9781 | if (defined($sender)) { |
---|
9782 | $self->smtp_resp(0,"503 5.5.1 Error: nested MAIL command", 1, $cmd); |
---|
9783 | last; |
---|
9784 | } |
---|
9785 | if (!$authenticated && |
---|
9786 | c('auth_required_inp') && @{ca('auth_mech_avail')} ) { |
---|
9787 | $self->smtp_resp(0,"530 5.7.1 Authentication required", 1, $cmd); |
---|
9788 | last; |
---|
9789 | } |
---|
9790 | # begin SMTP transaction |
---|
9791 | my($now) = time; |
---|
9792 | prolong_timer('MAIL FROM received - timer reset', $child_timeout); |
---|
9793 | if (!$seq) { # the first connect |
---|
9794 | section_time('SMTP pre-MAIL'); |
---|
9795 | } else { # establish new time reference for each transaction |
---|
9796 | Amavis::Timing::init(); snmp_counters_init(); |
---|
9797 | } |
---|
9798 | $seq++; |
---|
9799 | new_am_id(undef,$Amavis::child_invocation_count,$seq) |
---|
9800 | if !$initial_am_id; |
---|
9801 | $initial_am_id = 0; $self->prepare_tempdir; |
---|
9802 | my($cl_ip) = $xforward_args{'ADDR'}; |
---|
9803 | if ($cl_ip ne '' && defined $policy_bank{'MYNETS'} |
---|
9804 | && lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) { |
---|
9805 | Amavis::load_policy_bank('MYNETS'); $policy_changed = 1; |
---|
9806 | } |
---|
9807 | $msginfo = Amavis::In::Message->new; |
---|
9808 | $msginfo->rx_time($now); |
---|
9809 | $msginfo->delivery_method(c('forward_method')); |
---|
9810 | my($submitter); |
---|
9811 | if ($authenticated) { |
---|
9812 | $msginfo->auth_user($auth_user); $msginfo->auth_pass($auth_pass); |
---|
9813 | $conn->smtp_proto($self->{proto}.'A') # rfc3848 |
---|
9814 | if $self->{proto} =~ /^(LMTP|ESMTP)\z/i; |
---|
9815 | } elsif (c('auth_reauthenticate_forwarded') && |
---|
9816 | c('amavis_auth_user') ne '') { |
---|
9817 | $msginfo->auth_user(c('amavis_auth_user')); |
---|
9818 | $msginfo->auth_pass(c('amavis_auth_pass')); |
---|
9819 | $submitter = qquote_rfc2821_local(c('mailfrom_notify_recip')); |
---|
9820 | } |
---|
9821 | $msginfo->client_addr($xforward_args{'ADDR'}); |
---|
9822 | $msginfo->client_name($xforward_args{'NAME'}); |
---|
9823 | $msginfo->client_proto($xforward_args{'PROTO'}); |
---|
9824 | $msginfo->client_helo($xforward_args{'HELO'}); |
---|
9825 | %xforward_args = (); # reset values for the next transation |
---|
9826 | # permit some sloppy syntax without angle brackets |
---|
9827 | if ($args !~ /^FROM: \s* |
---|
9828 | ( < (?: " (?: \\. | [^\\"] )* " | [^"@] )* |
---|
9829 | (?: @ (?: \[ (?: \\. | [^\]\\] )* \] | |
---|
9830 | [^\[\]\\>] )* )? |
---|
9831 | > | |
---|
9832 | [^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )* |
---|
9833 | ) (?: \s+ ([\040-\176]+) )? \z/isx ) { |
---|
9834 | $self->smtp_resp(0,"501 5.5.2 Syntax: MAIL FROM: <address>",1,$cmd); |
---|
9835 | last; |
---|
9836 | } |
---|
9837 | my($bad); my($addr,$opt) = ($1,$2); |
---|
9838 | for (split(' ',$opt)) { |
---|
9839 | if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]* ) = |
---|
9840 | ( [\041-\074\076-\176]+ ) \z/xs) { # printable, not '=' or SP |
---|
9841 | $self->smtp_resp(0,"501 5.5.4 Syntax error in MAIL FROM parameters", |
---|
9842 | 1,$cmd); |
---|
9843 | $bad = 1; last; |
---|
9844 | } else { |
---|
9845 | my($name,$val) = (uc($1),$2); |
---|
9846 | if ($name eq 'SIZE' && $val=~/^\d{1,20}\z/) { # rfc1870 |
---|
9847 | $msginfo->msg_size($val+0); |
---|
9848 | if ($message_size_limit && $val > $message_size_limit) { |
---|
9849 | my($msg) = "552 5.3.4 Declared message size ($val B) exceeds ". |
---|
9850 | "fixed maximium message size of $message_size_limit B"; |
---|
9851 | do_log(0, $self->{proto}." REJECT 'MAIL FROM': $msg"); |
---|
9852 | $self->smtp_resp(0,$msg, 0,$cmd); |
---|
9853 | $bad = 1; last; |
---|
9854 | } |
---|
9855 | } elsif ($name eq 'BODY' && $val=~/^7BIT|8BITMIME\z/i){ |
---|
9856 | $msginfo->body_type(uc($val)); |
---|
9857 | } elsif ($name eq 'AUTH' && @{ca('auth_mech_avail')} && |
---|
9858 | !defined($submitter) ) { # rfc2554 |
---|
9859 | $submitter = $val; # encoded as xtext: rfc3461 |
---|
9860 | $submitter =~ s/\+([0-9a-fA-F]{2})/pack("C",hex($1))/eg; |
---|
9861 | do_log(5, "MAIL command, $authenticated, submitter: $submitter"); |
---|
9862 | } else { |
---|
9863 | my($msg); |
---|
9864 | if ($name eq 'AUTH' && !@{ca('auth_mech_avail')}) { |
---|
9865 | $msg = "503 5.7.4 Error: authentication disabled"; |
---|
9866 | } else { |
---|
9867 | $msg = "504 5.5.4 MAIL command parameter error: $name=$val"; |
---|
9868 | } |
---|
9869 | $self->smtp_resp(0,$msg,1,$cmd); |
---|
9870 | $bad = 1; last; |
---|
9871 | } |
---|
9872 | } |
---|
9873 | } |
---|
9874 | if (!$bad) { |
---|
9875 | $addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr; |
---|
9876 | $self->smtp_resp(0,"250 2.1.0 Sender $addr OK"); |
---|
9877 | $sender = unquote_rfc2821_local($addr); |
---|
9878 | debug_oneshot(lookup(0,$sender,@{ca('debug_sender_maps')}) ? 1 : 0, |
---|
9879 | $self->{proto} . "< $cmd"); |
---|
9880 | # $submitter = "<$addr>" if !defined($submitter); # rfc2554: MAY |
---|
9881 | $submitter = '<>' if !defined($msginfo->auth_user); |
---|
9882 | $msginfo->auth_submitter($submitter); |
---|
9883 | }; |
---|
9884 | last; |
---|
9885 | }; |
---|
9886 | /^RCPT\z/ && do { |
---|
9887 | if (!defined($sender)) { |
---|
9888 | $self->smtp_resp(0,"503 5.5.1 Need MAIL command before RCPT",1,$cmd); |
---|
9889 | @recips = (); $got_rcpt = 0; |
---|
9890 | last; |
---|
9891 | } |
---|
9892 | $got_rcpt++; |
---|
9893 | # permit some sloppy syntax without angle brackets |
---|
9894 | if ($args !~ /^TO: \s* |
---|
9895 | ( < (?: " (?: \\. | [^\\"] )* " | [^"@] )* |
---|
9896 | (?: @ (?: \[ (?: \\. | [^\]\\] )* \] | |
---|
9897 | [^\[\]\\>] )* )? |
---|
9898 | > | |
---|
9899 | [^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )* |
---|
9900 | ) (?: \s+ ([\040-\176]+) )? \z/isx ) { |
---|
9901 | $self->smtp_resp(0,"501 5.5.2 Syntax: RCPT TO: <address>",1,$cmd); |
---|
9902 | last; |
---|
9903 | } |
---|
9904 | if ($2 ne '') { |
---|
9905 | $self->smtp_resp(0,"504 5.5.4 RCPT command parameter not implemented: $2", |
---|
9906 | 1, $cmd); |
---|
9907 | ### $self->smtp_resp(0,"555 5.5.4 RCPT command parameter unrecognized: $2", 1, $cmd); |
---|
9908 | } elsif ($got_rcpt > $smtpd_recipient_limit) { |
---|
9909 | $self->smtp_resp(0,"452 4.5.3 Too many recipients"); |
---|
9910 | } else { |
---|
9911 | my($addr,$opt) = ($1, $2); |
---|
9912 | $addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr; |
---|
9913 | my($addr_unq) = unquote_rfc2821_local($addr); |
---|
9914 | my($recip_size_limit); my($mslm) = ca('message_size_limit_maps'); |
---|
9915 | $recip_size_limit = lookup(0,$addr_unq, @$mslm) if @$mslm; |
---|
9916 | if ($recip_size_limit && $recip_size_limit < 65536) |
---|
9917 | { $recip_size_limit = 65536 } # rfc2821 requires at least 64k |
---|
9918 | if ($recip_size_limit > $max_recip_size_limit) |
---|
9919 | { $max_recip_size_limit = $recip_size_limit } |
---|
9920 | my($mail_size) = $msginfo->msg_size; |
---|
9921 | if (defined $mail_size && $recip_size_limit && $mail_size > $recip_size_limit) { |
---|
9922 | my($msg) = "552 5.3.4 Declared message size ($mail_size B) ". |
---|
9923 | "exceeds fixed maximium message size of $recip_size_limit B, ". |
---|
9924 | "recipient $addr"; |
---|
9925 | do_log(0, $self->{proto}." REJECT 'RCPT TO': $msg"); |
---|
9926 | $self->smtp_resp(0,$msg, 0,$cmd); |
---|
9927 | } else { |
---|
9928 | push(@recips,$addr_unq); |
---|
9929 | $self->smtp_resp(0,"250 2.1.5 Recipient $addr OK"); |
---|
9930 | } |
---|
9931 | }; |
---|
9932 | last; |
---|
9933 | }; |
---|
9934 | /^DATA\z/ && !@recips && do { |
---|
9935 | if (!defined($sender)) { |
---|
9936 | $self->smtp_resp(1,"503 5.5.1 Need MAIL command before DATA",1,$cmd); |
---|
9937 | } elsif (!$got_rcpt) { |
---|
9938 | $self->smtp_resp(1,"503 5.5.1 Need RCPT command before DATA",1,$cmd); |
---|
9939 | } elsif ($lmtp) { # rfc2033 requires 503 code! |
---|
9940 | $self->smtp_resp(1,"503 5.1.1 Error (DATA): no valid recipients",0,$cmd); |
---|
9941 | } else { |
---|
9942 | $self->smtp_resp(1,"554 5.1.1 Error (DATA): no valid recipients",0,$cmd); |
---|
9943 | } |
---|
9944 | last; |
---|
9945 | }; |
---|
9946 | /^DATA\z/ && do { |
---|
9947 | # set timer to the initial value, MTA timer starts here |
---|
9948 | prolong_timer('DATA received - timer reset', $child_timeout); |
---|
9949 | if ($message_size_limit) { # enforce system-wide size limit |
---|
9950 | if (!$max_recip_size_limit || |
---|
9951 | $max_recip_size_limit > $message_size_limit) { |
---|
9952 | $max_recip_size_limit = $message_size_limit; |
---|
9953 | } |
---|
9954 | } |
---|
9955 | my($within_data_transfer,$complete); |
---|
9956 | my($size) = 0; my($over_size) = 0; |
---|
9957 | eval { |
---|
9958 | $msginfo->sender($sender); $msginfo->recips(\@recips); |
---|
9959 | ll(1) && do_log(1, sprintf("%s:%s:%s %s: <%s> -> %s Received: %s", |
---|
9960 | $conn->smtp_proto, |
---|
9961 | $conn->socket_ip eq $inet_socket_bind ? '' |
---|
9962 | : '['.$conn->socket_ip.']', |
---|
9963 | $conn->socket_port, $self->{tempdir_pers}, |
---|
9964 | $sender, join(',', qquote_rfc2821_local(@recips)), |
---|
9965 | join(' ', ($msginfo->msg_size eq '' ? () |
---|
9966 | : 'SIZE='.$msginfo->msg_size), |
---|
9967 | ($msginfo->body_type eq '' ? () |
---|
9968 | : 'BODY='.$msginfo->body_type), |
---|
9969 | received_line($conn,$msginfo,am_id(),0) ) |
---|
9970 | ) ); |
---|
9971 | $self->smtp_resp(1,"354 End data with <CR><LF>.<CR><LF>"); |
---|
9972 | $within_data_transfer = 1; |
---|
9973 | section_time('SMTP pre-DATA-flush') if $self->{pipelining}; |
---|
9974 | $self->{tempdir_empty} = 0; |
---|
9975 | do { local($/) = "\015\012"; # set input line terminator to CRLF |
---|
9976 | while(<$sock>) { # use native I/O for speed |
---|
9977 | # do_log(5, $self->{proto} . "< $_"); |
---|
9978 | if (/^\./) { |
---|
9979 | if ($_ eq ".\015\012") |
---|
9980 | { $complete = 1; $within_data_transfer = 0; last } |
---|
9981 | s/^\.(.+\015\012)\z/$1/s; # dot de-stuffing, rfc2821 |
---|
9982 | } |
---|
9983 | $size += length($_); # message size is defined in rfc1870 |
---|
9984 | if (!$over_size) { |
---|
9985 | chomp; # remove \015\012 (the $/), faster than s/// |
---|
9986 | print {$self->{fh_pers}} $_,$eol |
---|
9987 | or die "Can't write to mail file: $!"; |
---|
9988 | if ($max_recip_size_limit && $size > $max_recip_size_limit) { |
---|
9989 | do_log(1,"Message size exceeded $max_recip_size_limit B, ". |
---|
9990 | "skiping further input"); |
---|
9991 | print {$self->{fh_pers}} $eol,"***TRUNCATED***",$eol |
---|
9992 | or die "Can't write to mail file: $!"; |
---|
9993 | $over_size = 1; |
---|
9994 | } |
---|
9995 | } |
---|
9996 | } |
---|
9997 | $eof = 1 if !$complete; |
---|
9998 | }; # restores line terminator |
---|
9999 | # normal data termination, or eof on socket, or fatal error |
---|
10000 | do_log(4, $self->{proto} . "< .\015\012") if $complete; |
---|
10001 | $self->{fh_pers}->flush or die "Can't flush mail file: $!"; |
---|
10002 | # On some systems you have to do a seek whenever you |
---|
10003 | # switch between reading and writing. Amongst other things, |
---|
10004 | # this may have the effect of calling stdio's clearerr(3). |
---|
10005 | $self->{fh_pers}->seek(0,1) or die "Can't seek on file: $!"; |
---|
10006 | section_time('SMTP DATA'); |
---|
10007 | }; |
---|
10008 | if ($@ ne '' || !$complete || $over_size) { # err or connection broken |
---|
10009 | chomp($@); |
---|
10010 | # on error, either send: '421 Shutting down', |
---|
10011 | # or: '451 Aborted, error in processing' and NOT shut down! |
---|
10012 | if ($over_size && $@ eq '' && !$within_data_transfer) { |
---|
10013 | my($msg) = "552 5.3.4 Message size ($size B) exceeds ". |
---|
10014 | "fixed maximium message size of $max_recip_size_limit B"; |
---|
10015 | do_log(0, $self->{proto}." REJECT: $msg"); |
---|
10016 | $self->smtp_resp(0,$msg, 0,$cmd); |
---|
10017 | } elsif (!$within_data_transfer) { |
---|
10018 | my($msg) = "Error in processing: " . |
---|
10019 | !$complete && $@ eq '' ? 'incomplete' : $@; |
---|
10020 | do_log(-2, $self->{proto}." TROUBLE: 451 4.5.0 $msg"); |
---|
10021 | $self->smtp_resp(1, "451 4.5.0 $msg"); |
---|
10022 | ### $aborting = $msg; |
---|
10023 | } else { |
---|
10024 | $aborting = "client broke the connection ". |
---|
10025 | "during data transfer" if $eof; |
---|
10026 | $aborting .= ', ' if $aborting ne '' && $@ ne ''; |
---|
10027 | $aborting .= $@; |
---|
10028 | $aborting = '???' if $aborting eq ''; |
---|
10029 | do_log($@ ne '' ? -1 : 3, |
---|
10030 | $self->{proto}." TROUBLE, ABORTING: $aborting"); |
---|
10031 | } |
---|
10032 | } else { # all OK |
---|
10033 | # |
---|
10034 | # Is it acceptable to do all this processing here, |
---|
10035 | # before returning response??? According to rfc1047 |
---|
10036 | # it is not a good idea! But at the moment we do not have |
---|
10037 | # much choice, amavis has no queueing mechanism and can not |
---|
10038 | # accept responsibility for delivery. |
---|
10039 | # |
---|
10040 | # check contents before responding |
---|
10041 | # check_mail() expects open file on $self->{fh_pers}, |
---|
10042 | # need not be rewound |
---|
10043 | $msginfo->mail_tempdir($self->{tempdir_pers}); |
---|
10044 | $msginfo->mail_text_fn($self->{tempdir_pers} . '/email.txt'); |
---|
10045 | $msginfo->mail_text($self->{fh_pers}); |
---|
10046 | my($declared_size) = $msginfo->msg_size; |
---|
10047 | if (!defined($declared_size)) { |
---|
10048 | $msginfo->msg_size($size); |
---|
10049 | } elsif ($size > $declared_size) { # shouldn't happen with decent MTA |
---|
10050 | do_log(2,"Actual message size $size B greater than the ". |
---|
10051 | "declared $declared_size B"); |
---|
10052 | $msginfo->msg_size($size); |
---|
10053 | } elsif ($size < $declared_size) { # not unusual |
---|
10054 | do_log(4,"Actual message size $size B, declared $declared_size B"); |
---|
10055 | $msginfo->msg_size($size); |
---|
10056 | } |
---|
10057 | my($smtp_resp, $exit_code, $preserve_evidence) = |
---|
10058 | &$check_mail($conn,$msginfo, $lmtp,$self->{tempdir_pers}); |
---|
10059 | if ($preserve_evidence) { $self->preserve_evidence(1) } |
---|
10060 | if ($smtp_resp !~ /^4/ && |
---|
10061 | grep { !$_->recip_done } @{$msginfo->per_recip_data}) { |
---|
10062 | die "TROUBLE: (MISCONFIG) not all recipients done, " . |
---|
10063 | "forward_method is: " . $msginfo->delivery_method; |
---|
10064 | } |
---|
10065 | if (!$lmtp) { |
---|
10066 | do_log(4, "sending SMTP response: \"$smtp_resp\""); |
---|
10067 | $self->smtp_resp(0, $smtp_resp); |
---|
10068 | } else { |
---|
10069 | my($bounced) = $msginfo->dsn_sent; |
---|
10070 | for my $r (@{$msginfo->per_recip_data}) { |
---|
10071 | my($resp) = $r->recip_smtp_response; |
---|
10072 | if ($bounced && $smtp_resp=~/^2/ && $resp!~/^2/) { |
---|
10073 | # as the message was already bounced by us, |
---|
10074 | # MTA must not bounce it again; failure status |
---|
10075 | # needs to be converted into success! |
---|
10076 | $resp = sprintf("250 2.5.0 Ok, DSN %s (%s)", |
---|
10077 | $bounced==1 ? 'sent' : 'muted', $resp); |
---|
10078 | } |
---|
10079 | do_log(4, sprintf("sending LMTP response for <%s>: \"%s\"", |
---|
10080 | $r->recip_addr, $resp)); |
---|
10081 | $self->smtp_resp(0, $resp); |
---|
10082 | } |
---|
10083 | } |
---|
10084 | }; |
---|
10085 | alarm(0); do_log(5,"timer stopped after DATA end"); |
---|
10086 | if ($self->preserve_evidence && !$self->{tempdir_empty}) { |
---|
10087 | # keep evidence in case of trouble |
---|
10088 | do_log(-1,"PRESERVING EVIDENCE in ".$self->{tempdir_pers}); |
---|
10089 | $self->{fh_pers}->close or die "Can't close mail file: $!"; |
---|
10090 | $self->{fh_pers} = undef; $self->{tempdir_pers} = undef; |
---|
10091 | $self->{tempdir_empty} = 1; |
---|
10092 | } |
---|
10093 | # cleanup, but leave directory (and file handle if possible) for reuse |
---|
10094 | if ($self->{fh_pers} && !$can_truncate) { |
---|
10095 | # truncate is not standard across all Unix variants, |
---|
10096 | # it is not Posix, but is XPG4-UNIX. |
---|
10097 | # So if we can't truncate a file and leave it open, |
---|
10098 | # we have to create it anew later, at some cost. |
---|
10099 | # |
---|
10100 | $self->{fh_pers}->close or die "Can't close mail file: $!"; |
---|
10101 | $self->{fh_pers} = undef; |
---|
10102 | unlink($self->{tempdir_pers}.'/email.txt') |
---|
10103 | or die "Can't delete file ".$self->{tempdir_pers}."/email.txt: $!"; |
---|
10104 | section_time('delete email.txt'); |
---|
10105 | } |
---|
10106 | if (defined $self->{tempdir_pers}) { # prepare for the next one |
---|
10107 | strip_tempdir($self->{tempdir_pers}); $self->{tempdir_empty} = 1; |
---|
10108 | } |
---|
10109 | $sender = undef; @recips = (); $got_rcpt = 0; # implicit RSET |
---|
10110 | $max_recip_size_limit = undef; $msginfo = undef; # forget previous |
---|
10111 | if ($policy_changed) |
---|
10112 | { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 } |
---|
10113 | $self->preserve_evidence(0); # reset |
---|
10114 | # report elapsed times by section for each transaction |
---|
10115 | # (the time for the QUIT remains unaccounted for) |
---|
10116 | do_log(2, Amavis::Timing::report()); |
---|
10117 | Amavis::Timing::init(); snmp_counters_init(); |
---|
10118 | last; |
---|
10119 | }; # DATA |
---|
10120 | # catchall (EXPN, TURN, unknown): |
---|
10121 | $self->smtp_resp(1,"502 5.5.1 Error: command ($_) not implemented",1,$cmd); |
---|
10122 | # $self->smtp_resp(1,"500 5.5.2 Error: command ($_) not recognized", 1,$cmd); |
---|
10123 | }; # end of 'switch' block |
---|
10124 | if ($terminating || defined $aborting) { # exit SMTP-session loop |
---|
10125 | $voluntary_exit = 1; last; |
---|
10126 | } |
---|
10127 | # rfc2920 requires a flush whenever the local TCP input buffer is |
---|
10128 | # emptied. Since we can't check it (unless we use sysread & select), |
---|
10129 | # we should do a flush here to be in compliance. We could only break |
---|
10130 | # the requirement if we _knew_ we talk with a local MTA client which |
---|
10131 | # uses client-side pipelining. |
---|
10132 | $self->smtp_resp_flush; |
---|
10133 | $0 = sprintf("amavisd (ch%d-%s-idle)", |
---|
10134 | $Amavis::child_invocation_count, am_id()); |
---|
10135 | Amavis::Timing::go_idle(6); |
---|
10136 | undef $!; |
---|
10137 | } # end of while |
---|
10138 | my($errn,$errs); |
---|
10139 | if (!$voluntary_exit) { |
---|
10140 | $eof = 1; |
---|
10141 | if (!defined($_)) { $errn = 0+$!; $errs = "$!" } |
---|
10142 | } |
---|
10143 | $0 = sprintf("amavisd (ch%d)", $Amavis::child_invocation_count); |
---|
10144 | Amavis::Timing::go_busy(7); |
---|
10145 | # come here when: QUIT is received, eof or err on socket, or we need to abort |
---|
10146 | $self->smtp_resp_flush; # just in case, the session might have been disconnected |
---|
10147 | my($msg) = |
---|
10148 | defined $aborting && !$eof ? "ABORTING the session: $aborting" : |
---|
10149 | defined $aborting ? $aborting : |
---|
10150 | !$terminating ? "client broke the connection without a QUIT ($errs)" : ''; |
---|
10151 | do_log($aborting?-1:3, $self->{proto}.': NOTICE: '.$msg) if $msg ne ''; |
---|
10152 | if (defined $aborting && !$eof) |
---|
10153 | { $self->smtp_resp(1,"421 4.3.2 Service shutting down, ".$aborting) } |
---|
10154 | $self->{session_closed_normally} = 1; |
---|
10155 | # closes connection after child_finish_hook |
---|
10156 | } |
---|
10157 | |
---|
10158 | # sends a SMTP response consisting of 3-digit code and an optional message; |
---|
10159 | # slow down evil clients by delaying response on permanent errors |
---|
10160 | sub smtp_resp($$$;$$) { |
---|
10161 | my($self, $flush,$resp, $penalize,$line) = @_; |
---|
10162 | if ($penalize) { |
---|
10163 | do_log(-1, $self->{proto} . ": $resp; PENALIZE: $line"); |
---|
10164 | sleep 5; |
---|
10165 | section_time('SMTP penalty wait'); |
---|
10166 | } |
---|
10167 | $resp = sanitize_str($resp,1); |
---|
10168 | local($1,$2,$3,$4); |
---|
10169 | if ($resp !~ /^ ([1-5]\d\d) (\ |-|\z) |
---|
10170 | ([245] \. \d{1,3} \. \d{1,3} (?: \ |\z) )? |
---|
10171 | (.*) \z/xs) |
---|
10172 | { die "Internal error(2): bad SMTP response code: '$resp'" } |
---|
10173 | my($resp_code,$continuation,$enhanced,$tail) = ($1,$2,$3, $4); |
---|
10174 | my($lead_len) = length($resp_code) + 1 + length($enhanced); |
---|
10175 | while (length($tail) > 512-2-$lead_len || $tail =~ /\n/) { |
---|
10176 | # rfc2821: The maximum total length of a reply line including the |
---|
10177 | # reply code and the <CRLF> is 512 characters. More information |
---|
10178 | # may be conveyed through multiple-line replies. |
---|
10179 | my($head) = substr($tail,0,512-2-$lead_len); |
---|
10180 | if ($head =~ /^([^\n]*\n)/) { $head = $1 } |
---|
10181 | $tail = substr($tail,length($head)); chomp($head); |
---|
10182 | push(@{$self->{smtp_outbuf}}, $resp_code.'-'.$enhanced.$head); |
---|
10183 | } |
---|
10184 | push(@{$self->{smtp_outbuf}},$resp_code.$continuation.$enhanced.$tail); |
---|
10185 | $self->smtp_resp_flush if $flush || !$self->{pipelining} || |
---|
10186 | @{$self->{smtp_outbuf}} > 200; |
---|
10187 | } |
---|
10188 | |
---|
10189 | sub smtp_resp_flush($) { |
---|
10190 | my($self) = shift; |
---|
10191 | if (@{$self->{smtp_outbuf}}) { |
---|
10192 | for my $resp (@{$self->{smtp_outbuf}}) { |
---|
10193 | do_log(4, $self->{proto} . "> $resp"); |
---|
10194 | }; |
---|
10195 | my($stat) = |
---|
10196 | $self->{sock}->print(map { $_."\015\012" } @{$self->{smtp_outbuf}} ); |
---|
10197 | @{$self->{smtp_outbuf}} = (); # prevent printing again even if error |
---|
10198 | $stat or die "Error writing a SMTP response to the socket: $!"; |
---|
10199 | } |
---|
10200 | } |
---|
10201 | |
---|
10202 | 1; |
---|
10203 | |
---|
10204 | __DATA__ |
---|
10205 | # |
---|
10206 | package Amavis::In::QMQPqq; |
---|
10207 | use strict; |
---|
10208 | # use re 'taint'; # (is this module ready for this yet?) |
---|
10209 | |
---|
10210 | BEGIN { |
---|
10211 | use Exporter (); |
---|
10212 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
10213 | $VERSION = '1.17'; |
---|
10214 | @ISA = qw(Exporter); |
---|
10215 | } |
---|
10216 | use POSIX qw(strftime); |
---|
10217 | use Errno qw(ENOENT); |
---|
10218 | |
---|
10219 | BEGIN { |
---|
10220 | import Amavis::Conf qw(:platform :confvars :dynamic_confvars c cr ca); |
---|
10221 | import Amavis::Util qw(do_log am_id prolong_timer debug_oneshot |
---|
10222 | untaint sanitize_str strip_tempdir rmdir_recursively); |
---|
10223 | import Amavis::Lookup qw(lookup); |
---|
10224 | import Amavis::Timing qw(section_time); |
---|
10225 | import Amavis::rfc2821_2822_Tools; |
---|
10226 | import Amavis::In::Message; |
---|
10227 | import Amavis::In::Connection; |
---|
10228 | } |
---|
10229 | |
---|
10230 | sub new($) { |
---|
10231 | my($class) = @_; |
---|
10232 | my($self) = bless {}, $class; |
---|
10233 | $self->{fh_pers} = undef; # persistent file handle for email.txt |
---|
10234 | $self->{tempdir_pers} = undef; # temporary directory for check_mail |
---|
10235 | $self->{preserve} = undef; # don't delete tempdir on exit |
---|
10236 | $self->{tempdir_empty} = 1; # anything of interest in tempdir? |
---|
10237 | $self->{bytesleft} = undef; # bytes left for whole package |
---|
10238 | $self->{len} = undef; # set by getlen() method |
---|
10239 | $self->{sock} = undef; # connected socket |
---|
10240 | $self->{proto} = undef; # protocol |
---|
10241 | $self->{session_closed_normally} = undef; # closed properly? (waited for K/Z/D) |
---|
10242 | $self; |
---|
10243 | } |
---|
10244 | |
---|
10245 | sub preserve_evidence # try to preserve temporary files etc in case of trouble |
---|
10246 | { my($self)=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift) } |
---|
10247 | |
---|
10248 | sub DESTROY { |
---|
10249 | my($self) = shift; |
---|
10250 | # do_log(0, "Amavis::In::QMQPqq::DESTROY called"); |
---|
10251 | $self->{fh_pers}->close |
---|
10252 | or die "Can't close temp file: $!" if $self->{fh_pers}; |
---|
10253 | my($errn) = $self->{tempdir_pers} eq '' ? ENOENT |
---|
10254 | : (stat($self->{tempdir_pers}) ? 0 : 0+$!); |
---|
10255 | if (defined $self->{tempdir_pers} && $errn != ENOENT) { |
---|
10256 | # this will not be included in the TIMING report, |
---|
10257 | # but it only occurs infrequently and doesn't take that long |
---|
10258 | if ($self->preserve_evidence && !$self->{tempdir_empty}) { |
---|
10259 | do_log(0, "tempdir is to be PRESERVED: ".$self->{tempdir_pers}); |
---|
10260 | } else { |
---|
10261 | do_log(2, "tempdir being removed: ".$self->{tempdir_pers}); |
---|
10262 | rmdir_recursively($self->{tempdir_pers}); |
---|
10263 | } |
---|
10264 | } |
---|
10265 | if (! $self->{session_closed_normally}) { |
---|
10266 | $self->qmqpqq_resp("Z","Service shutting down, closing channel"); |
---|
10267 | } |
---|
10268 | } |
---|
10269 | |
---|
10270 | sub prepare_tempdir($) { |
---|
10271 | my($self) = @_; |
---|
10272 | if (! defined $self->{tempdir_pers} ) { |
---|
10273 | # invent a name for a temporary directory for this child, and create it |
---|
10274 | my($now_iso8601) = strftime("%Y%m%dT%H%M%S", localtime); |
---|
10275 | $self->{tempdir_pers} = sprintf("%s/amavis-%s-%05d", |
---|
10276 | $TEMPBASE, $now_iso8601, $$); |
---|
10277 | } |
---|
10278 | my($errn) = stat($self->{tempdir_pers}) ? 0 : 0+$!; |
---|
10279 | if ($errn == ENOENT || ! -d _) { |
---|
10280 | mkdir($self->{tempdir_pers}, 0750) |
---|
10281 | or die "Can't create directory $self->{tempdir_pers}: $!"; |
---|
10282 | $self->{tempdir_empty} = 1; |
---|
10283 | section_time('mkdir tempdir'); |
---|
10284 | } |
---|
10285 | # prepare temporary file for writing (and reading later) |
---|
10286 | my($fname) = $self->{tempdir_pers} . "/email.txt"; |
---|
10287 | my($errn) = stat($fname) ? 0 : 0+$!; |
---|
10288 | if ($self->{fh_pers} && !$errn && -f _) { |
---|
10289 | $self->{fh_pers}->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
10290 | $self->{fh_pers}->truncate(0) or die "Can't truncate mail file: $!"; |
---|
10291 | } else { |
---|
10292 | $self->{fh_pers} = IO::File->new($fname, 'w+', 0640) |
---|
10293 | or die "Can't create file $fname: $!"; |
---|
10294 | section_time('create email.txt'); |
---|
10295 | } |
---|
10296 | } |
---|
10297 | |
---|
10298 | |
---|
10299 | # get byte, die if no bytes left |
---|
10300 | sub getbyte($) { |
---|
10301 | my($self) = shift; |
---|
10302 | if(!$self->{bytesleft}--) { |
---|
10303 | die("No bytes left"); |
---|
10304 | } |
---|
10305 | if(defined($_ = $self->{sock}->getc)) { |
---|
10306 | return($_); |
---|
10307 | } |
---|
10308 | die("EOF on socket"); |
---|
10309 | } |
---|
10310 | |
---|
10311 | sub getlen($) { |
---|
10312 | my($self) = shift; |
---|
10313 | my($ch,$len); |
---|
10314 | |
---|
10315 | for(;;) { |
---|
10316 | $ch = $self->getbyte; |
---|
10317 | if($ch eq ':') { |
---|
10318 | return($self->{len} = $len); |
---|
10319 | } |
---|
10320 | if($ch !~ /^\d$/) { |
---|
10321 | die("Char '$ch' is not a number while determining length"); |
---|
10322 | } |
---|
10323 | $len .= $ch; |
---|
10324 | } |
---|
10325 | } |
---|
10326 | |
---|
10327 | sub getcomma($) { |
---|
10328 | my($self) = shift; |
---|
10329 | if($self->getbyte ne ',') { |
---|
10330 | die("Comma expected, found '$_'"); |
---|
10331 | } |
---|
10332 | } |
---|
10333 | |
---|
10334 | sub getnetstring($$) { |
---|
10335 | my($self) = shift; |
---|
10336 | ($self->{sock}->read($_[0],$self->getlen) == $self->{len}) || |
---|
10337 | die("EOF on socket"); |
---|
10338 | $self->{bytesleft} -= $self->{len}; |
---|
10339 | $self->getcomma; |
---|
10340 | } |
---|
10341 | |
---|
10342 | |
---|
10343 | # Accept a QMQPqq connect |
---|
10344 | # and call content checking for the message received |
---|
10345 | # |
---|
10346 | sub process_qmqpqq_request($$$$) { |
---|
10347 | my($self,$sock,$conn,$check_mail) = @_; |
---|
10348 | # $sock: connected socket from Net::Server |
---|
10349 | # $conn: information about client connection |
---|
10350 | # $check_mail: subroutine ref to be called with file handle |
---|
10351 | |
---|
10352 | $self->{proto} = "QMQPqq"; |
---|
10353 | $self->{sock} = $sock; # store $sock info for getbyte() method |
---|
10354 | $self->{bytesleft} = 20; # initial bytesleft value, there should |
---|
10355 | # NEVER EVER be longer email than 10^20 (approximately) |
---|
10356 | # bytes but increase if needed ;) |
---|
10357 | $self->{len} = undef; |
---|
10358 | |
---|
10359 | my($msginfo); |
---|
10360 | |
---|
10361 | my($sender,@recips); |
---|
10362 | |
---|
10363 | my($len); |
---|
10364 | |
---|
10365 | $conn->smtp_proto("QMQPqq"); # the name of the method is too specific |
---|
10366 | eval { |
---|
10367 | # get length of whole package |
---|
10368 | $self->{bytesleft} = $self->getlen; |
---|
10369 | |
---|
10370 | # get length of 'email' |
---|
10371 | $len = $self->getlen; |
---|
10372 | section_time('initial length determination'); |
---|
10373 | |
---|
10374 | am_id(sprintf("%05d-%02d",$$,$Amavis::child_invocation_count)); |
---|
10375 | |
---|
10376 | # prepare tempdir |
---|
10377 | $self->prepare_tempdir; |
---|
10378 | $msginfo = Amavis::In::Message->new; |
---|
10379 | $msginfo->rx_time(time); |
---|
10380 | $msginfo->delivery_method(c('forward_method')); |
---|
10381 | |
---|
10382 | # get 'email' |
---|
10383 | $self->{tempdir_empty} = 0; |
---|
10384 | my $size = 16384; |
---|
10385 | while(($len > 0) && ($sock->read($_,($len >= $size ? $size : $size = $len)) == $size)) { |
---|
10386 | (print {$self->{fh_pers}} $_) || |
---|
10387 | die("Can't write to mail file: $!"); |
---|
10388 | $len -= $size; |
---|
10389 | } |
---|
10390 | if($len > 0) { |
---|
10391 | die("EOF on socket"); |
---|
10392 | } |
---|
10393 | $self->{fh_pers}->flush || die("Can't flush mail file: $!"); |
---|
10394 | $self->{fh_pers}->seek(0,1) || die("Can't seek on file: $!"); |
---|
10395 | $self->{bytesleft} -= $self->{len}; |
---|
10396 | section_time('email receiving'); |
---|
10397 | # comma has to follow |
---|
10398 | $self->getcomma; |
---|
10399 | |
---|
10400 | # get sender |
---|
10401 | $self->getnetstring($sender); |
---|
10402 | section_time('sender receiving'); |
---|
10403 | |
---|
10404 | # get recips |
---|
10405 | my $i = 0; |
---|
10406 | while($self->{bytesleft}) { |
---|
10407 | $self->getnetstring($recips[$i++]); |
---|
10408 | } |
---|
10409 | section_time('recips receiving'); |
---|
10410 | |
---|
10411 | # final comma has to follow |
---|
10412 | $self->{bytesleft} = 1; |
---|
10413 | $self->getcomma; |
---|
10414 | |
---|
10415 | $msginfo->sender($sender); |
---|
10416 | $msginfo->recips(\@recips); |
---|
10417 | |
---|
10418 | do_log(1, sprintf("%s:%s:%s %s: <%s> -> %s Received: %s", |
---|
10419 | $self->{proto},$conn->socket_ip eq $inet_socket_bind ? |
---|
10420 | '' : '['.$conn->socket_ip.']', |
---|
10421 | $conn->socket_port, $self->{tempdir_pers}, |
---|
10422 | $sender, join(',', map{"<$_>"}@recips), |
---|
10423 | join(' ', |
---|
10424 | ($msginfo->msg_size eq '' ? () |
---|
10425 | : 'SIZE='.$msginfo->msg_size), |
---|
10426 | ($msginfo->body_type eq '' ? () |
---|
10427 | : 'BODY='.$msginfo->body_type), |
---|
10428 | received_line($conn,$msginfo,am_id(),0) ) |
---|
10429 | )); |
---|
10430 | |
---|
10431 | $msginfo->mail_tempdir($self->{tempdir_pers}); |
---|
10432 | $msginfo->mail_text_fn($self->{tempdir_pers} . '/email.txt'); |
---|
10433 | $msginfo->mail_text($self->{fh_pers}); |
---|
10434 | |
---|
10435 | my($smtp_resp,$exit_code,$preserve_evidence) = |
---|
10436 | &$check_mail($conn,$msginfo,0,$self->{tempdir_pers}); |
---|
10437 | |
---|
10438 | if ($preserve_evidence) { |
---|
10439 | $self->preserve_evidence(1); |
---|
10440 | } |
---|
10441 | if ($smtp_resp !~ /^4/ && |
---|
10442 | grep { !$_->recip_done } @{$msginfo->per_recip_data}) { |
---|
10443 | die("TROUBLE/MISCONFIG: not all recipients done, ". |
---|
10444 | "\$forward_method is \"$forward_method\""); |
---|
10445 | } |
---|
10446 | |
---|
10447 | # all ok |
---|
10448 | if($smtp_resp =~ /^2/) { |
---|
10449 | $self->qmqpqq_resp("K",$smtp_resp); |
---|
10450 | } |
---|
10451 | # permanent reject |
---|
10452 | elsif($smtp_resp =~ /^5/) { |
---|
10453 | $self->qmqpqq_resp("D",$smtp_resp); |
---|
10454 | } |
---|
10455 | # temporary reject (or other error if !~ /^4/) |
---|
10456 | else { |
---|
10457 | $self->qmqpqq_resp("Z",$smtp_resp); |
---|
10458 | } |
---|
10459 | }; |
---|
10460 | |
---|
10461 | alarm(0); do_log(5,"timer stopped after QMQPqq eval"); |
---|
10462 | |
---|
10463 | if($@ ne '') { |
---|
10464 | chomp($@); |
---|
10465 | |
---|
10466 | do_log(0,"QMQPqq: NOTICE: $@"); |
---|
10467 | $self->qmqpqq_resp("Z","Service shutting down, $@"); |
---|
10468 | } |
---|
10469 | |
---|
10470 | if ($self->preserve_evidence && !$self->{tempdir_empty}) { |
---|
10471 | # keep evidence in case of trouble |
---|
10472 | do_log(0,"PRESERVING EVIDENCE in ".$self->{tempdir_pers}); |
---|
10473 | $self->{fh_pers}->close or die "Can't close mail file: $!"; |
---|
10474 | $self->{fh_pers} = undef; $self->{tempdir_pers} = undef; |
---|
10475 | $self->{tempdir_empty} = 1; |
---|
10476 | } |
---|
10477 | |
---|
10478 | # cleanup, but leave directory (and file handle |
---|
10479 | # if possible) for reuse |
---|
10480 | if ($self->{fh_pers} && !$can_truncate) { |
---|
10481 | # truncate is not standard across all Unix variants, |
---|
10482 | # it is not Posix, but is XPG4-UNIX. |
---|
10483 | # So if we can't truncate a file and leave it open, |
---|
10484 | # we have to create it anew later, at some cost. |
---|
10485 | # |
---|
10486 | $self->{fh_pers}->close or die "Can't close mail file: $!"; |
---|
10487 | $self->{fh_pers} = undef; |
---|
10488 | unlink($self->{tempdir_pers}."/email.txt") |
---|
10489 | or die "Can't delete file ". |
---|
10490 | $self->{tempdir_pers}."/email.txt: $!"; |
---|
10491 | section_time('delete email.txt'); |
---|
10492 | } |
---|
10493 | |
---|
10494 | if (defined $self->{tempdir_pers}) { # prepare for the next one |
---|
10495 | strip_tempdir($self->{tempdir_pers}); |
---|
10496 | $self->{tempdir_empty} = 1; |
---|
10497 | } |
---|
10498 | |
---|
10499 | $self->preserve_evidence(0); # reset |
---|
10500 | # report elapsed times by section for each transaction |
---|
10501 | do_log(2, Amavis::Timing::report()); |
---|
10502 | |
---|
10503 | $self->{session_closed_normally} = 1; |
---|
10504 | # closes connection after child_finish_hook |
---|
10505 | } |
---|
10506 | |
---|
10507 | # sends a QMQPqq response consisting of K/D/Z code and an optional message; |
---|
10508 | # slow down evil clients by delaying response on permanent errors |
---|
10509 | sub qmqpqq_resp($$$;$$) { |
---|
10510 | my($self,$code,$resp,$penalize,$line) = @_; |
---|
10511 | if($code !~ /^(K|Z|D)$/) { |
---|
10512 | die("Internal error(2): bad QMQPqq response code: '$code'"); |
---|
10513 | } |
---|
10514 | if($penalize) { |
---|
10515 | do_log(0,"QMQPqq: $resp; PENALIZE: $line"); |
---|
10516 | sleep 5; |
---|
10517 | section_time('QMQPqq penalty wait'); |
---|
10518 | } |
---|
10519 | $resp = sanitize_str($resp,1); |
---|
10520 | do_log(4,"QMQPqq> $resp"); |
---|
10521 | print($self->netstring($code . $resp)); |
---|
10522 | } |
---|
10523 | |
---|
10524 | sub netstring($$) { |
---|
10525 | my($self,$string) = @_; |
---|
10526 | return(sprintf("%d:%s,",length($string),$string)); |
---|
10527 | } |
---|
10528 | |
---|
10529 | 1; |
---|
10530 | |
---|
10531 | __DATA__ |
---|
10532 | # |
---|
10533 | package Amavis::AV; |
---|
10534 | use strict; |
---|
10535 | use re 'taint'; |
---|
10536 | |
---|
10537 | BEGIN { |
---|
10538 | use Exporter (); |
---|
10539 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
10540 | $VERSION = '2.034'; |
---|
10541 | @ISA = qw(Exporter); |
---|
10542 | @EXPORT_OK = qw(&sophos_savi_init); |
---|
10543 | } |
---|
10544 | |
---|
10545 | use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED |
---|
10546 | WEXITSTATUS WTERMSIG WSTOPSIG); |
---|
10547 | use Errno qw(EPIPE ENOTCONN ENOENT EACCES); |
---|
10548 | use Socket; |
---|
10549 | use IO::Socket; |
---|
10550 | use IO::Socket::UNIX; |
---|
10551 | |
---|
10552 | use subs @EXPORT_OK; |
---|
10553 | use vars @EXPORT; |
---|
10554 | |
---|
10555 | BEGIN { |
---|
10556 | import Amavis::Conf qw(:platform :confvars c cr ca); |
---|
10557 | import Amavis::Util qw(ll untaint min max do_log am_id rmdir_recursively |
---|
10558 | exit_status_str run_command); |
---|
10559 | import Amavis::Timing qw(section_time); |
---|
10560 | } |
---|
10561 | |
---|
10562 | use vars qw(%st_socket_created %st_sock); # keep persistent state (per-socket) |
---|
10563 | |
---|
10564 | # subroutine available for calling from @av_scanners list entries; |
---|
10565 | # it has the same args and returns as run_av() below |
---|
10566 | sub ask_daemon { ask_av(\&ask_daemon_internal, @_) } |
---|
10567 | |
---|
10568 | sub clamav_module_init($) { |
---|
10569 | my($av_name) = @_; |
---|
10570 | # each child should reinitialize clamav module to reload databases. |
---|
10571 | my($clamav_version) = Mail::ClamAV->VERSION; |
---|
10572 | my($dbdir) = Mail::ClamAV::retdbdir(); |
---|
10573 | my($clamav_obj) = Mail::ClamAV->new($dbdir); |
---|
10574 | ref $clamav_obj |
---|
10575 | or die "$av_name: Can't load db from $dbdir: $Mail::ClamAV::Error"; |
---|
10576 | $clamav_obj->buildtrie; |
---|
10577 | $clamav_obj->maxreclevel($MAXLEVELS) if $MAXLEVELS; |
---|
10578 | $clamav_obj->maxfiles($MAXFILES); |
---|
10579 | $clamav_obj->maxfilesize($MAX_EXPANSION_QUOTA || 30*1024*1024); |
---|
10580 | if ($clamav_version >= 0.12) { |
---|
10581 | $clamav_obj->maxratio($MAX_EXPANSION_FACTOR); |
---|
10582 | # $clamav_obj->archivememlim(0); # limit memory usage for bzip2 (0/1) |
---|
10583 | } |
---|
10584 | do_log(2,"$av_name init"); |
---|
10585 | section_time('clamav_module_init'); |
---|
10586 | ($clamav_obj,$clamav_version); |
---|
10587 | } |
---|
10588 | |
---|
10589 | # to be called from sub ask_clamav |
---|
10590 | use vars qw($clamav_obj $clamav_version); |
---|
10591 | sub clamav_module_internal($@) { |
---|
10592 | my($query, $bare_fnames,$names_to_parts,$tempdir, $av_name) = @_; |
---|
10593 | if (!defined $clamav_obj) { |
---|
10594 | ($clamav_obj,$clamav_version) = clamav_module_init($av_name); # first time |
---|
10595 | } elsif ($clamav_obj->statchkdir) { # db reload needed? |
---|
10596 | do_log(2, "$av_name: reloading virus database"); |
---|
10597 | ($clamav_obj,$clamav_version) = clamav_module_init($av_name); |
---|
10598 | } |
---|
10599 | my($fname) = "$tempdir/parts/$query"; # file to be checked |
---|
10600 | my($part) = $names_to_parts->{$query}; # get corresponding parts object |
---|
10601 | my($options) = 0; # bitfield of options to Mail::ClamAV::scan |
---|
10602 | my($opt_archive,$opt_mail); |
---|
10603 | if ($clamav_version < 0.12) { |
---|
10604 | $opt_archive = &Mail::ClamAV::CL_ARCHIVE; |
---|
10605 | $opt_mail = &Mail::ClamAV::CL_MAIL; |
---|
10606 | } else { # >= 0.12, reflects renamed flags in libclamav 0.80 |
---|
10607 | $opt_archive = &Mail::ClamAV::CL_SCAN_ARCHIVE; |
---|
10608 | $opt_mail = &Mail::ClamAV::CL_SCAN_MAIL; |
---|
10609 | } |
---|
10610 | $options |= &Mail::ClamAV::CL_SCAN_STDOPT if $clamav_version >= 0.13; |
---|
10611 | $options |= $opt_archive; # turn on ARCHIVE |
---|
10612 | $options &= ~$opt_mail; # turn off MAIL |
---|
10613 | if (ref($part) && (lc($part->type_short) eq 'mail' || |
---|
10614 | lc($part->type_declared) eq 'message/rfc822')) { |
---|
10615 | do_log(2, "$av_name: $query - enabling option CL_MAIL"); |
---|
10616 | $options |= $opt_mail; # turn on MAIL |
---|
10617 | } |
---|
10618 | my($ret) = $clamav_obj->scan(untaint($fname), $options); |
---|
10619 | my($output,$status); |
---|
10620 | if ($ret->virus) { $status = 1; $output = "INFECTED: $ret" } |
---|
10621 | elsif ($ret->clean) { $status = 0; $output = "CLEAN" } |
---|
10622 | else { $status = 2; $output = $ret->error.", errno=".$ret->errno } |
---|
10623 | ($status,$output); # return synthesised status and a result string |
---|
10624 | } |
---|
10625 | |
---|
10626 | # subroutine available for calling from @av_scanners list entries; |
---|
10627 | # it has the same args and returns as run_av() below |
---|
10628 | sub ask_clamav { ask_av(\&clamav_module_internal, @_) } |
---|
10629 | |
---|
10630 | |
---|
10631 | use vars qw($savi_obj); |
---|
10632 | sub sophos_savi_init { |
---|
10633 | my($av_name, $command) = @_; |
---|
10634 | my(@savi_bool_options) = qw( |
---|
10635 | FullSweep DynamicDecompression FullMacroSweep OLE2Handling |
---|
10636 | IgnoreTemplateBit VBA3Handling VBA5Handling OF95DecryptHandling |
---|
10637 | HelpHandling DecompressVBA5 Emulation PEHandling ExcelFormulaHandling |
---|
10638 | PowerPointMacroHandling PowerPointEmbeddedHandling ProjectHandling |
---|
10639 | ZipDecompression ArjDecompression RarDecompression UueDecompression |
---|
10640 | GZipDecompression TarDecompression CmzDecompression HqxDecompression |
---|
10641 | MbinDecompression !LoopBackEnabled |
---|
10642 | Lha SfxArchives MSCabinet TnefAttachmentHandling MSCompress |
---|
10643 | !DeleteAllMacros Vbe !ExecFileDisinfection VisioFileHandling |
---|
10644 | ActiveMimeHandling !DelVBA5Project |
---|
10645 | ScrapObjectHandling SrpStreamHandling Office2001Handling |
---|
10646 | Upx PalmPilotHandling HqxDecompression |
---|
10647 | Pdf Rtf Html Elf WordB OutlookExpress |
---|
10648 | ); |
---|
10649 | # starting with SAVI V3: Mac and SafeMacDfHandling options were removed; |
---|
10650 | # new option GrpArchiveUnpack makes individual settings unnecessary; |
---|
10651 | # option 'Mime' may cause a CPU loop when checking broken mail with some |
---|
10652 | # versions of Sophos library (even with more recent ones!) |
---|
10653 | my($savi_obj) = SAVI->new; |
---|
10654 | ref $savi_obj or die "$av_name: Can't create SAVI object, err=$savi_obj"; |
---|
10655 | my($version) = $savi_obj->version; |
---|
10656 | ref $version or die "$av_name: Can't get SAVI version, err=$version"; |
---|
10657 | do_log(2,sprintf("$av_name init: Version %s (engine %d.%d) recognizing %d viruses\n", |
---|
10658 | $version->string, $version->major, $version->minor, $version->count)); |
---|
10659 | # for ($version->ide_list) |
---|
10660 | # { do_log(2, sprintf("$av_name: IDE %s released %s", $_->name, $_->date)) } |
---|
10661 | my($error) = $savi_obj->set('MaxRecursionDepth', $MAXLEVELS, 1); |
---|
10662 | !defined $error or die "$av_name: error setting MaxRecursionDepth: err=$error"; |
---|
10663 | $error = $savi_obj->set('NamespaceSupport', 3); # new with Sophos 3.67 |
---|
10664 | !defined $error |
---|
10665 | or do_log(-1,"$av_name: error setting NamespaceSupport: err=$error"); |
---|
10666 | for (@savi_bool_options) { |
---|
10667 | my($value) = /^!/ ? 0 : 1; s/^!+//; |
---|
10668 | $error = $savi_obj->set($_, $value); |
---|
10669 | !defined $error or die "$av_name: Error setting $_: err=$error"; |
---|
10670 | } |
---|
10671 | section_time('sophos_savi_init'); |
---|
10672 | $savi_obj; |
---|
10673 | } |
---|
10674 | |
---|
10675 | # to be called from sub sophos_savi |
---|
10676 | sub sophos_savi_internal { |
---|
10677 | my($query, |
---|
10678 | $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args) = @_; |
---|
10679 | # if (defined $args) { $savi_obj = $args } |
---|
10680 | # else { |
---|
10681 | $savi_obj = sophos_savi_init($av_name,$command) if !defined $savi_obj; |
---|
10682 | # } |
---|
10683 | my($fname) = "$tempdir/parts/$query"; # file to be checked |
---|
10684 | my($part) = $names_to_parts->{$query}; # get corresponding parts object |
---|
10685 | my($mime_option_value) = 0; |
---|
10686 | if (ref($part) && (lc($part->type_short) eq 'mail' || |
---|
10687 | lc($part->type_declared) eq 'message/rfc822')) { |
---|
10688 | do_log(2, "$av_name: $query - enabling option MIME"); |
---|
10689 | $mime_option_value = 1; |
---|
10690 | } |
---|
10691 | my($error) = $savi_obj->set('MIME', $mime_option_value); |
---|
10692 | !defined $error or die sprintf("%s: Error %s option MIME: err=%s", |
---|
10693 | $av_name, $mime_option_value ? 'setting' : 'clearing', $error); |
---|
10694 | my($output,$status); my($result) = $savi_obj->scan($fname); |
---|
10695 | if (!ref($result)) { # error |
---|
10696 | my($msg) = "$av_name: error scanning file $fname, " . |
---|
10697 | $savi_obj->error_string($result) . " ($result) $!"; |
---|
10698 | if (! grep {$result == $_} (514,527,530,538,549) ) { |
---|
10699 | $status = 2; $output = "ERROR: $msg\n"; |
---|
10700 | } else { # don't panic on non-fatal (encrypted, corrupted, partial) |
---|
10701 | $status = 0; $output = "CLEAN: $msg\n"; |
---|
10702 | } |
---|
10703 | do_log(-1,$output); |
---|
10704 | } elsif ($result->infected) { |
---|
10705 | $status = 1; $output = "INFECTED $query\n"; |
---|
10706 | for my $virus_name ($result->viruses) { $output .= "$virus_name FOUND\n" } |
---|
10707 | } else { |
---|
10708 | $status = 0; $output = "CLEAN $query\n"; |
---|
10709 | } |
---|
10710 | ($status,$output); # return synthesised status and a result string |
---|
10711 | } |
---|
10712 | |
---|
10713 | # subroutine available for calling from @av_scanners list entries; |
---|
10714 | # it has the same args and returns as run_av() below |
---|
10715 | sub ask_sophos_savi { |
---|
10716 | my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args, |
---|
10717 | $sts_clean,$sts_infected,$how_to_get_names) = @_; |
---|
10718 | if (@_ < 3+6) { # supply default arguments for backwards compatibility |
---|
10719 | $args = ["*"]; $sts_clean = [0]; $sts_infected = [1]; |
---|
10720 | $how_to_get_names = qr/^(.*) FOUND$/; |
---|
10721 | } |
---|
10722 | ask_av(\&sophos_savi_internal, |
---|
10723 | $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args, |
---|
10724 | $sts_clean, $sts_infected, $how_to_get_names); |
---|
10725 | } |
---|
10726 | |
---|
10727 | |
---|
10728 | # same args and returns as run_av() below, |
---|
10729 | # but prepended by a $query, which is the string to be sent to the daemon. |
---|
10730 | # Handles both UNIX and INET domain sockets. |
---|
10731 | # More than one socket may be specified for redundancy, they will be tried |
---|
10732 | # one after the other until one succeeds. |
---|
10733 | # |
---|
10734 | sub ask_daemon_internal { |
---|
10735 | my($query, # expanded query template, often a command and a file or dir name |
---|
10736 | $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args, |
---|
10737 | $sts_clean,$sts_infected,$how_to_get_names, # regexps |
---|
10738 | ) = @_; |
---|
10739 | my($query_template_orig,$sockets) = @$args; |
---|
10740 | my($output); my($socketname,$is_inet); |
---|
10741 | if (!ref($sockets)) { $sockets = [ $sockets ] } |
---|
10742 | my($max_retries) = 2 * @$sockets; my($retries) = 0; |
---|
10743 | $SIG{PIPE} = 'IGNORE'; # 'send' to broken pipe would throw a signal |
---|
10744 | for (;;) { # gracefully handle cases when av child times out or restarts |
---|
10745 | @$sockets >= 1 or die "no sockets specified!?"; # sanity |
---|
10746 | $socketname = $sockets->[0]; # try the first one in the current list |
---|
10747 | $is_inet = $socketname =~ m{^/} ? 0 : 1; # simpleminded: unix vs. inet sock |
---|
10748 | eval { |
---|
10749 | if (!$st_socket_created{$socketname}) { |
---|
10750 | ll(3) && do_log(3, "$av_name: Connecting to socket " . |
---|
10751 | join(' ',$daemon_chroot_dir,$socketname). |
---|
10752 | (!$retries ? '' : ", retry #$retries") ); |
---|
10753 | if ($is_inet) { # inet socket |
---|
10754 | $st_sock{$socketname} = IO::Socket::INET->new($socketname) |
---|
10755 | or die "Can't connect to INET socket $socketname: $!\n"; |
---|
10756 | $st_socket_created{$socketname} = 1; |
---|
10757 | } else { # unix socket |
---|
10758 | $st_sock{$socketname} = IO::Socket::UNIX->new(Type => SOCK_STREAM) |
---|
10759 | or die "Can't create UNIX socket: $!\n"; |
---|
10760 | $st_socket_created{$socketname} = 1; |
---|
10761 | $st_sock{$socketname}->connect( pack_sockaddr_un($socketname) ) |
---|
10762 | or die "Can't connect to UNIX socket $socketname: $!\n"; |
---|
10763 | } |
---|
10764 | } |
---|
10765 | ll(3) && do_log(3,sprintf("$av_name: Sending %s to %s socket %s", |
---|
10766 | $query, $is_inet?"INET":"UNIX", $socketname)); |
---|
10767 | # UGLY: bypass send method in IO::Socket to be able to retrieve |
---|
10768 | # status/errno directly from 'send', not from 'getpeername': |
---|
10769 | defined send($st_sock{$socketname}, $query, 0) |
---|
10770 | or die "Can't send to socket $socketname: $!\n"; |
---|
10771 | if ($av_name =~ /^(Sophie|Trophie)/i) { |
---|
10772 | # Sophie and Trophie can accept multiple requests per session |
---|
10773 | # and return a single line response each time |
---|
10774 | defined $st_sock{$socketname}->recv($output, 1024) |
---|
10775 | or die "Can't receive from socket $socketname: $!\n"; |
---|
10776 | } else { |
---|
10777 | $output = join('', $st_sock{$socketname}->getlines); |
---|
10778 | $st_sock{$socketname}->close |
---|
10779 | or die "Can't close socket $socketname: $!\n"; |
---|
10780 | $st_sock{$socketname}=undef; $st_socket_created{$socketname}=0; |
---|
10781 | } |
---|
10782 | $! = undef; |
---|
10783 | $output ne '' or die "Empty result from $socketname\n"; |
---|
10784 | }; |
---|
10785 | last if $@ eq ''; |
---|
10786 | # error handling (most interesting error codes are EPIPE and ENOTCONN) |
---|
10787 | chomp($@); my($err) = "$!"; my($errn) = 0+$!; |
---|
10788 | ++$retries <= $max_retries |
---|
10789 | or die "Too many retries to talk to $socketname ($@)"; |
---|
10790 | # is ECONNREFUSED for INET sockets common enough too? |
---|
10791 | if ($retries <= 1 && $errn == EPIPE) { # common, don't cause concern |
---|
10792 | do_log(2,"$av_name broken pipe (don't worry), retrying ($retries)"); |
---|
10793 | } else { |
---|
10794 | do_log( ($retries>1?-1:1), "$av_name: $@, retrying ($retries)"); |
---|
10795 | if ($retries % @$sockets == 0) { # every time the list is exhausted |
---|
10796 | my($dly) = min(20, 1 + 5 * ($retries/@$sockets - 1)); |
---|
10797 | do_log(3,"$av_name: sleeping for $dly s"); |
---|
10798 | sleep($dly); # slow down a possible runaway |
---|
10799 | } |
---|
10800 | } |
---|
10801 | if ($st_socket_created{$socketname}) { |
---|
10802 | # prepare for a retry, ignore 'close' status |
---|
10803 | $st_sock{$socketname}->close; |
---|
10804 | $st_sock{$socketname} = undef; $st_socket_created{$socketname} = 0; |
---|
10805 | } |
---|
10806 | # leave good socket as the first entry in the list |
---|
10807 | # so that it will be tried first when needed again |
---|
10808 | push(@$sockets, shift @$sockets) if @$sockets>1; # circular shift left |
---|
10809 | } |
---|
10810 | (0,$output); # return synthesised status and result string |
---|
10811 | } |
---|
10812 | |
---|
10813 | # ask_av is a common subroutine available to be used by ask_daemon, ask_clamav, |
---|
10814 | # ask_sophos_savi and similar front-end routines used in @av_scanners entries. |
---|
10815 | # It traverses supplied files or directory ($bare_fnames) and calls a supplied |
---|
10816 | # subroutine for each file to be scanned, summarizing the final av scan result. |
---|
10817 | # It has the same args and returns as run_av() below, prepended by a checking |
---|
10818 | # subroutine argument. |
---|
10819 | sub ask_av { |
---|
10820 | my($code) = shift; # strip away the first argument, a subroutine ref |
---|
10821 | my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args, |
---|
10822 | $sts_clean,$sts_infected,$how_to_get_names) = @_; |
---|
10823 | my($query_template) = ref $args eq 'ARRAY' ? $args->[0] : $args; |
---|
10824 | do_log(5, "ask_av ($av_name): query template1: $query_template"); |
---|
10825 | my($checking_each_file) = $query_template =~ /\*/; |
---|
10826 | my($scan_status,@virusname); my($output) = ''; |
---|
10827 | for my $f ($checking_each_file ? @$bare_fnames : ("$tempdir/parts")) { |
---|
10828 | my($query) = $query_template; |
---|
10829 | if (!$checking_each_file) { # scanner can be given a directory name |
---|
10830 | $query =~ s[{}][$tempdir/parts]g; # replace {} with directory name |
---|
10831 | do_log(3,"Using ($av_name) on dir: $query"); |
---|
10832 | } else { # must check each file individually |
---|
10833 | # replace {}/* with directory name and file, and * with current file name |
---|
10834 | $query =~ s[ ({}/)? \* ][ $1 eq '' ? $f : "$tempdir/parts/$f" ]gesx; |
---|
10835 | do_log(3,"Using ($av_name) on file: $query"); |
---|
10836 | } |
---|
10837 | my($t_status,$t_output) = &$code($query, @_); |
---|
10838 | do_log(4,"ask_av ($av_name) result: $t_output"); |
---|
10839 | # braindead Perl: ""=~/x{0}/ serves as explicit default for an empty regexp |
---|
10840 | if (defined $sts_infected && ( |
---|
10841 | ref($sts_infected) eq 'ARRAY' ? (grep {$_==$t_status} @$sts_infected) |
---|
10842 | : ""=~/x{0}/ && $t_output=~/$sts_infected/m)) { # is infected |
---|
10843 | # test for infected first, in case both expressions match |
---|
10844 | $scan_status = 1; # 'true' indicates virus found, no errors |
---|
10845 | my(@t_virusnames) = ref($how_to_get_names) eq 'CODE' |
---|
10846 | ? &$how_to_get_names($t_output) |
---|
10847 | : ""=~/x{0}/ && $t_output=~/$how_to_get_names/gm; |
---|
10848 | @t_virusnames = map { defined $_ ? $_ : () } @t_virusnames; |
---|
10849 | push(@virusname, @t_virusnames); |
---|
10850 | $output .= $t_output . $eol; |
---|
10851 | do_log(2,"ask_av ($av_name): $f INFECTED: ".join(", ",@t_virusnames)); |
---|
10852 | } elsif (!defined($sts_clean)) { # clean, but inconclusive |
---|
10853 | # by convention: undef $sts_clean means result is inconclusive, |
---|
10854 | # file appears clean, but continue scanning with other av scanners, |
---|
10855 | # the current scanner does not want to vouch for it; useful for a |
---|
10856 | # scanner like jpeg checker which tests for one vulnerability only |
---|
10857 | do_log(3,"ask_av ($av_name): $f CLEAN, but inconclusive"); |
---|
10858 | } elsif (ref($sts_clean) eq 'ARRAY' |
---|
10859 | ? (grep {$_==$t_status} @$sts_clean) |
---|
10860 | : ""=~/x{0}/ && $t_output=~/$sts_clean/m) { # is clean |
---|
10861 | $scan_status = 0 if !$scan_status; # no viruses, no errors |
---|
10862 | do_log(3,"ask_av ($av_name): $f CLEAN"); |
---|
10863 | } else { |
---|
10864 | do_log(-2,"ask_av ($av_name) FAILED - unexpected result: $t_output"); |
---|
10865 | last; # error, bail out |
---|
10866 | } |
---|
10867 | } |
---|
10868 | if (!@$bare_fnames) { $scan_status = 0 } # no errors, no viruses |
---|
10869 | do_log(3,"$av_name result: clean") if defined($scan_status) && !$scan_status; |
---|
10870 | ($scan_status,$output,\@virusname); |
---|
10871 | } |
---|
10872 | |
---|
10873 | # Call a virus scanner and parse its output. |
---|
10874 | # Returns a triplet (or die in case of failure). |
---|
10875 | # The first element of the triplet is interpreted as follows: |
---|
10876 | # - true if virus found, |
---|
10877 | # - 0 if no viruses found, |
---|
10878 | # - undef if it did not complete its job; |
---|
10879 | # the second element is a string, the text as provided by the virus scanner; |
---|
10880 | # the third element is ref to a list of virus names found (if any). |
---|
10881 | # (it is guaranteed the list will be nonempty if virus was found) |
---|
10882 | # |
---|
10883 | sub run_av { |
---|
10884 | # first three args are prepended, not part of n-tuple |
---|
10885 | my($bare_fnames, # a ref to a list of filenames to scan (basenames) |
---|
10886 | $names_to_parts, # ref to a hash that maps base file names to parts object |
---|
10887 | $tempdir, # temporary directory |
---|
10888 | $av_name, $command, $args, |
---|
10889 | $sts_clean, # a ref to a list of status values, or a regexp |
---|
10890 | $sts_infected, # a ref to a list of status values, or a regexp |
---|
10891 | $how_to_get_names, # ref to sub, or a regexp to get list of virus names |
---|
10892 | $pre_code, $post_code, # routines to be invoked before and after av |
---|
10893 | ) = @_; |
---|
10894 | my($scan_status,$virusnames,$error_str); my($output) = ''; |
---|
10895 | &$pre_code(@_) if defined $pre_code; |
---|
10896 | if (ref($command) eq 'CODE') { |
---|
10897 | do_log(3,"Using $av_name: (built-in interface)"); |
---|
10898 | ($scan_status,$output,$virusnames) = &$command(@_); |
---|
10899 | } else { |
---|
10900 | my(@args) = split(' ',$args); |
---|
10901 | if (grep { m{^({}/)?\*\z} } @args) { # {}/* or *, list each file |
---|
10902 | # replace asterisks with bare file names (basenames) if alone or in {}/* |
---|
10903 | local($1); |
---|
10904 | @args = map { !m{^({}/)?\*\z} ? $_ |
---|
10905 | : map {$1.untaint($_)} @$bare_fnames } @args; |
---|
10906 | } |
---|
10907 | for (@args) { s[{}][$tempdir/parts]g } # replace {} with directory name |
---|
10908 | # NOTE: RAV does not like '</dev/null' in its command! |
---|
10909 | ll(3) && do_log(3, "Using ($av_name): " . join(' ',$command,@args)); |
---|
10910 | my($proc_fh,$pid) = run_command(undef, "&1", $command, @args); |
---|
10911 | while( defined($_ = $proc_fh->getline) ) { $output .= $_ } |
---|
10912 | my($err); $proc_fh->close or $err=$!; my($child_stat) = $?; |
---|
10913 | $error_str = exit_status_str($child_stat,$err); |
---|
10914 | my($retval) = WEXITSTATUS($child_stat); |
---|
10915 | local($1); chomp($output); my($output_trimmed) = $output; |
---|
10916 | $output_trimmed =~ s/\r\n/\n/gs; |
---|
10917 | $output_trimmed =~ s/([ \t\n\r])[ \t\n\r]{4,}/$1.../gs; |
---|
10918 | $output_trimmed = "..." . substr($output_trimmed,-800) |
---|
10919 | if length($output_trimmed) > 800; |
---|
10920 | do_log(3, "run_av: $command $error_str, $output_trimmed"); |
---|
10921 | # braindead Perl: ""=~/x{0}/ serves as explicit default for an empty regexp |
---|
10922 | if (!WIFEXITED($child_stat)) { |
---|
10923 | } elsif (defined $sts_infected && ( |
---|
10924 | ref($sts_infected) eq 'ARRAY' |
---|
10925 | ? (grep {$_==$retval} @$sts_infected) |
---|
10926 | : ""=~/x{0}/ && $output=~/$sts_infected/m)) { # is infected |
---|
10927 | # test for infected first, in case both expressions match |
---|
10928 | $virusnames = []; # get a list of virus names by parsing output |
---|
10929 | @$virusnames = ref($how_to_get_names) eq 'CODE' |
---|
10930 | ? &$how_to_get_names($output) |
---|
10931 | : ""=~/x{0}/ && $output=~/$how_to_get_names/gm; |
---|
10932 | @$virusnames = map { defined $_ ? $_ : () } @$virusnames; |
---|
10933 | $scan_status = 1; # 'true' indicates virus found |
---|
10934 | do_log(2,"run_av ($av_name): INFECTED: ".join(", ",@$virusnames)); |
---|
10935 | } elsif (!defined($sts_clean)) { # clean, but inconclusive |
---|
10936 | # by convention: undef $sts_clean means result is inconclusive, |
---|
10937 | # file appears clean, but continue scanning with other av scanners, |
---|
10938 | # the current scanner does not want to vouch for it; useful for a |
---|
10939 | # scanner like jpeg checker which tests for one vulnerability only |
---|
10940 | do_log(3,"run_av ($av_name): clean, but inconclusive"); |
---|
10941 | } elsif (ref($sts_clean) eq 'ARRAY' ? (grep {$_==$retval} @$sts_clean) |
---|
10942 | : ""=~/x{0}/ && $output=~/$sts_clean/m) { # is clean |
---|
10943 | $scan_status = 0; # 'false' (but defined) indicates no viruses |
---|
10944 | do_log(5,"run_av ($av_name): clean"); |
---|
10945 | } else { |
---|
10946 | $error_str = "unexpected $error_str, output=\"$output_trimmed\""; |
---|
10947 | } |
---|
10948 | $output = $output_trimmed if length($output) > 900; |
---|
10949 | } |
---|
10950 | &$post_code(@_) if defined $post_code; |
---|
10951 | $virusnames = [] if !defined $virusnames; |
---|
10952 | @$virusnames = (undef) if $scan_status && !@$virusnames; # nonnil |
---|
10953 | if (!defined($scan_status) && defined($error_str)) { |
---|
10954 | die "$command $error_str"; # die is more informative than return value |
---|
10955 | } |
---|
10956 | ($scan_status, $output, $virusnames); |
---|
10957 | } |
---|
10958 | |
---|
10959 | sub virus_scan($$$) { |
---|
10960 | my($tempdir,$firsttime,$parts_root) = @_; |
---|
10961 | my($scan_status,$output,@virusname,@detecting_scanners); |
---|
10962 | my($anyone_done); my($anyone_tried); |
---|
10963 | my($bare_fnames_ref,$names_to_parts); |
---|
10964 | my(@errors); my($j); my($tier) = 'primary'; |
---|
10965 | for my $av (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) { |
---|
10966 | next if !defined $av; |
---|
10967 | if ($av eq "\000") { # 'magic' separator between lists |
---|
10968 | last if $anyone_done; |
---|
10969 | do_log(-2,"WARN: all $tier virus scanners failed, considering backups"); |
---|
10970 | $tier = 'secondary'; next; |
---|
10971 | } |
---|
10972 | next if !ref $av || !defined $av->[1]; |
---|
10973 | if (!defined $bare_fnames_ref) { # first time: collect file names to scan |
---|
10974 | ($bare_fnames_ref,$names_to_parts) = |
---|
10975 | files_to_scan("$tempdir/parts",$parts_root); |
---|
10976 | do_log(2, "Not calling virus scanners, ". |
---|
10977 | "no files to scan in $tempdir/parts") if !@$bare_fnames_ref; |
---|
10978 | } |
---|
10979 | $anyone_tried++; my($this_status,$this_output,$this_vn); |
---|
10980 | if (!@$bare_fnames_ref) { # no files to scan? |
---|
10981 | ($this_status,$this_output,$this_vn) = (0, '', []); # declare clean |
---|
10982 | } else { # call virus scanner |
---|
10983 | eval { |
---|
10984 | ($this_status,$this_output,$this_vn) = |
---|
10985 | run_av($bare_fnames_ref,$names_to_parts,$tempdir, @$av); |
---|
10986 | }; |
---|
10987 | if ($@ ne '') { |
---|
10988 | my($err) = $@; chomp($err); |
---|
10989 | $err = "$av->[0] av-scanner FAILED: $err"; |
---|
10990 | do_log(-2,$err); push(@errors,$err); |
---|
10991 | $this_status = undef; |
---|
10992 | }; |
---|
10993 | } |
---|
10994 | $anyone_done++ if defined $this_status; |
---|
10995 | $j++; section_time("AV-scan-$j"); |
---|
10996 | if ($this_status) { # virus detected by this scanner |
---|
10997 | push(@detecting_scanners, $av->[0]); |
---|
10998 | if (!@virusname) { # store results of the first scanner detecting |
---|
10999 | @virusname = @$this_vn; |
---|
11000 | $scan_status = $this_status; $output = $this_output; |
---|
11001 | } |
---|
11002 | last if c('first_infected_stops_scan'); # stop now if we found a virus? |
---|
11003 | } elsif (!defined($scan_status)) { # tentatively keep regardless of status |
---|
11004 | $scan_status = $this_status; $output = $this_output; |
---|
11005 | } |
---|
11006 | } |
---|
11007 | if (@virusname && @detecting_scanners) { |
---|
11008 | my(@ds) = @detecting_scanners; for (@ds) { s/,/;/ } # facilitates parsing |
---|
11009 | ll(2) && do_log(2, sprintf("virus_scan: (%s), detected by %d scanners: %s", |
---|
11010 | join(', ',@virusname), scalar(@ds), join(', ',@ds))); |
---|
11011 | } |
---|
11012 | $output =~ s{\Q$tempdir\E/parts/?}{}gs if defined $output; # hide path info |
---|
11013 | if (!$anyone_tried) { die "NO VIRUS SCANNERS AVAILABLE\n" } |
---|
11014 | elsif (!$anyone_done) |
---|
11015 | { die ("ALL VIRUS SCANNERS FAILED: ".join("; ",@errors)."\n") } |
---|
11016 | ($scan_status, $output, \@virusname, \@detecting_scanners); # return a quad |
---|
11017 | } |
---|
11018 | |
---|
11019 | # return a ref to a list of files to be scanned in a given directory |
---|
11020 | sub files_to_scan($$) { |
---|
11021 | my($dir,$parts_root) = @_; |
---|
11022 | my($names_to_parts) = {}; # a hash that maps base file names |
---|
11023 | # to Amavis::Unpackers::Part object |
---|
11024 | for (my($part), my(@unvisited)=($parts_root); |
---|
11025 | @unvisited and $part=shift(@unvisited); |
---|
11026 | push(@unvisited,@{$part->children})) |
---|
11027 | { # traverse decomposed parts tree breadth-first, match it to actual files |
---|
11028 | $names_to_parts->{$part->base_name} = $part if $part ne $parts_root; |
---|
11029 | } |
---|
11030 | local(*DIR); my($f); my($bare_fnames_ref) = []; my(%bare_fnames); |
---|
11031 | opendir(DIR, $dir) or die "Can't open directory $dir: $!"; |
---|
11032 | # traverse parts directory and check for actual files |
---|
11033 | while (defined($f = readdir(DIR))) { |
---|
11034 | my($fname) = "$dir/$f"; |
---|
11035 | my($errn) = lstat($fname) ? 0 : 0+$!; |
---|
11036 | next if $errn == ENOENT; |
---|
11037 | if ($errn) { die "files_to_scan: file $fname inaccessible: $!" } |
---|
11038 | if (!-r _) { # attempting to gain read access to the file |
---|
11039 | do_log(3,"files_to_scan: attempting to gain read access to $fname"); |
---|
11040 | chmod(0750,untaint($fname)) |
---|
11041 | or die "files_to_scan: Can't change protection on $fname: $!"; |
---|
11042 | $errn = lstat($fname) ? 0 : 0+$!; |
---|
11043 | if ($errn) { die "files_to_scan: file $fname inaccessible: $!" } |
---|
11044 | if (!-r _) { die "files_to_scan: file $fname not readable" } |
---|
11045 | } |
---|
11046 | next if ($f eq '.' || $f eq '..') && -d _; # this or the parent directory |
---|
11047 | if (!-f _ || !exists $names_to_parts->{$f}) { # nonregular f. or unexpected |
---|
11048 | my($what) = -l _ ? 'symlink' : -d _ ? 'directory' : -f _ ? 'file' |
---|
11049 | : 'non-regular file'; |
---|
11050 | my($msg) = "removing unexpected $what $fname"; |
---|
11051 | $msg .= ", it has no corresponding parts object" |
---|
11052 | if !exists $names_to_parts->{$f}; |
---|
11053 | do_log(-1, "WARN: files_to_scan: ".$msg); |
---|
11054 | if (-d _) { rmdir_recursively(untaint($fname)) } |
---|
11055 | else { unlink(untaint($fname)) or die "Can't delete $what $fname: $!" } |
---|
11056 | } elsif (-z _) { |
---|
11057 | # empty file |
---|
11058 | } else { |
---|
11059 | if ($f !~ /^[A-Za-z0-9_.-]+\z/s) |
---|
11060 | {do_log(-1,"WARN: files_to_scan: unexpected/suspicious file name: $f")} |
---|
11061 | push(@$bare_fnames_ref,$f); $bare_fnames{$f} = 1; |
---|
11062 | } |
---|
11063 | } |
---|
11064 | closedir(DIR) or die "Can't close directory $dir: $!"; |
---|
11065 | # remove entries from %$names_to_parts that have no corresponding files |
---|
11066 | my($fname,$part); |
---|
11067 | while ( ($fname,$part) = each %$names_to_parts ) { |
---|
11068 | next if exists $bare_fnames{$fname}; |
---|
11069 | if ($part->exists) { |
---|
11070 | my($type_short) = $part->type_short; |
---|
11071 | ll(4) && do_log(4,sprintf( |
---|
11072 | "files_to_scan: info: part %s (%s) no longer present", |
---|
11073 | $fname, (!ref $type_short ? $type_short : join(', ',@$type_short)) )); |
---|
11074 | } |
---|
11075 | delete $names_to_parts->{$fname}; # delete is allowed for the current elem. |
---|
11076 | } |
---|
11077 | ($bare_fnames_ref, $names_to_parts); |
---|
11078 | } |
---|
11079 | |
---|
11080 | 1; |
---|
11081 | |
---|
11082 | __DATA__ |
---|
11083 | # |
---|
11084 | package Amavis::SpamControl; |
---|
11085 | use strict; |
---|
11086 | use re 'taint'; |
---|
11087 | |
---|
11088 | BEGIN { |
---|
11089 | use Exporter (); |
---|
11090 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
11091 | $VERSION = '2.034'; |
---|
11092 | @ISA = qw(Exporter); |
---|
11093 | } |
---|
11094 | use FileHandle; |
---|
11095 | use Mail::SpamAssassin; |
---|
11096 | |
---|
11097 | BEGIN { |
---|
11098 | import Amavis::Conf qw(:platform :sa $daemon_user c cr ca); |
---|
11099 | import Amavis::Util qw(ll do_log retcode exit_status_str run_command |
---|
11100 | prolong_timer); |
---|
11101 | import Amavis::rfc2821_2822_Tools; |
---|
11102 | import Amavis::Timing qw(section_time); |
---|
11103 | import Amavis::Lookup qw(lookup); |
---|
11104 | } |
---|
11105 | |
---|
11106 | use subs @EXPORT_OK; |
---|
11107 | |
---|
11108 | use vars qw($spamassassin_obj); |
---|
11109 | |
---|
11110 | # called at startup, before the main fork |
---|
11111 | sub init() { |
---|
11112 | do_log(1, "SpamControl: initializing Mail::SpamAssassin"); |
---|
11113 | my($saved_umask) = umask; |
---|
11114 | $spamassassin_obj = Mail::SpamAssassin->new({ |
---|
11115 | debug => $sa_debug, |
---|
11116 | save_pattern_hits => $sa_debug, |
---|
11117 | dont_copy_prefs => 1, |
---|
11118 | local_tests_only => $sa_local_tests_only, |
---|
11119 | home_dir_for_helpers => $helpers_home, |
---|
11120 | stop_at_threshold => 0, |
---|
11121 | # DEF_RULES_DIR => '/usr/local/share/spamassassin', |
---|
11122 | # LOCAL_RULES_DIR => '/etc/mail/spamassassin', |
---|
11123 | #see man Mail::SpamAssassin for other options |
---|
11124 | }); |
---|
11125 | # $Mail::SpamAssassin::DEBUG->{rbl}=-3; |
---|
11126 | # $Mail::SpamAssassin::DEBUG->{dcc}=-3; |
---|
11127 | # $Mail::SpamAssassin::DEBUG->{pyzor}=-3; |
---|
11128 | # $Mail::SpamAssassin::DEBUG->{bayes}=-3; |
---|
11129 | # $Mail::SpamAssassin::DEBUG->{rulesrun}=4+64; |
---|
11130 | if ($sa_auto_whitelist && Mail::SpamAssassin::Version() < 3) { |
---|
11131 | do_log(1, "SpamControl: turning on SA auto-whitelisting (AWL)"); |
---|
11132 | # create a factory for the persistent address list |
---|
11133 | my($addrlstfactory) = Mail::SpamAssassin::DBBasedAddrList->new; |
---|
11134 | $spamassassin_obj->set_persistent_address_list_factory($addrlstfactory); |
---|
11135 | } |
---|
11136 | $spamassassin_obj->compile_now; # ensure all modules etc. are preloaded |
---|
11137 | alarm(0); # seems like SA forgets to clear alarm in some cases |
---|
11138 | umask($saved_umask); # restore our umask, SA clobbered it |
---|
11139 | do_log(1, "SpamControl: done"); |
---|
11140 | } |
---|
11141 | |
---|
11142 | # check envelope sender if white or blacklisted by each recipient; |
---|
11143 | # Saves the result in recip_blacklisted_sender and recip_whitelisted_sender |
---|
11144 | # properties of each recipient object. |
---|
11145 | # |
---|
11146 | sub white_black_list($$$$$) { |
---|
11147 | my($conn,$msginfo,$sql_wblist,$user_id_sql,$ldap_policy) = @_; |
---|
11148 | my($any_w)=0; my($any_b)=0; my($all)=1; my($wr,$br); |
---|
11149 | my($sender) = $msginfo->sender; |
---|
11150 | do_log(4,"wbl: checking sender <$sender>"); |
---|
11151 | for my $r (@{$msginfo->per_recip_data}) { |
---|
11152 | next if $r->recip_done; # already dealt with |
---|
11153 | my($found,$wb,$boost); my($recip) = $r->recip_addr; |
---|
11154 | my($user_id_ref,$mk_ref) = !defined $sql_wblist ? ([],[]) |
---|
11155 | : lookup(1,$recip,$user_id_sql); |
---|
11156 | do_log(5,"wbl: (SQL) recip <$recip>, ".scalar(@$user_id_ref)." matches") |
---|
11157 | if defined $sql_wblist && ll(5); |
---|
11158 | for my $ind (0..$#{$user_id_ref}) { # for ALL SQL sets matching the recip |
---|
11159 | my($user_id) = $user_id_ref->[$ind]; my($mkey); |
---|
11160 | ($wb,$mkey) = lookup(0,$sender, |
---|
11161 | Amavis::Lookup::SQLfield->new($sql_wblist,'wb','S',$user_id) ); |
---|
11162 | do_log(4,"wbl: (SQL) recip <$recip>, rid=$user_id, got: \"$wb\""); |
---|
11163 | if (!defined($wb)) { # NULL field or no match: remains undefined |
---|
11164 | } elsif ($wb =~ /^ *([+-]?\d+(?:\.\d*)?) *\z/) { # numeric |
---|
11165 | my($val) = 0+$1; # penalty points to be added to the score |
---|
11166 | $boost += $val; |
---|
11167 | ll(2) && do_log(2,sprintf( |
---|
11168 | "wbl: (SQL) soft-%slisted (%s) sender <%s> => <%s> (rid=%s)", |
---|
11169 | ($val<0?'white':'black'), $val, $sender, $recip, $user_id)); |
---|
11170 | $wb = undef; # not hard- white or blacklisting |
---|
11171 | } elsif ($wb =~ /^[ \000]*\z/) { # neutral, stops the search |
---|
11172 | $found++; $wb = 0; |
---|
11173 | do_log(5,"wbl: (SQL) recip <$recip> is neutral to sender <$sender>"); |
---|
11174 | } elsif ($wb =~ /^([BbNnFf])[ ]*\z/) { # blacklisted (B, N, F) |
---|
11175 | $found++; $wb = -1; $any_b++; $br = $recip; |
---|
11176 | $r->recip_blacklisted_sender(1); |
---|
11177 | do_log(5,"wbl: (SQL) recip <$recip> blacklisted sender <$sender>"); |
---|
11178 | } else { # whitelisted (W, Y, T) or anything else |
---|
11179 | if ($wb =~ /^([WwYyTt])[ ]*\z/) { |
---|
11180 | do_log(5, "wbl: (SQL) recip <$recip> whitelisted sender <$sender>"); |
---|
11181 | } else { |
---|
11182 | do_log(-1,"wbl: (SQL) recip <$recip> whitelisted sender <$sender>, ". |
---|
11183 | "unexpected wb field value: \"$wb\""); |
---|
11184 | } |
---|
11185 | $found++; $wb = +1; $any_w++; $wr = $recip; |
---|
11186 | $r->recip_whitelisted_sender(1); |
---|
11187 | } |
---|
11188 | last if $found; |
---|
11189 | } |
---|
11190 | if (!$found && defined($ldap_policy)) { |
---|
11191 | my($wblist); |
---|
11192 | my($keys_ref,$rhs_ref) = make_query_keys($sender,0,0); |
---|
11193 | my(@keys) = @$keys_ref; |
---|
11194 | do_log(5,sprintf("wbl: (LDAP) query keys: %s", |
---|
11195 | join(', ',map{"\"$_\""}@keys))); |
---|
11196 | $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new($ldap_policy,'amavisBlacklistSender','L-')); |
---|
11197 | for my $key (@keys) { |
---|
11198 | if (grep {/^\Q$key\E\z/i} @$wblist) { |
---|
11199 | $found++; $wb = -1; $br = $recip; $any_b++; |
---|
11200 | $r->recip_blacklisted_sender(1); |
---|
11201 | do_log(5,"wbl: (LDAP) recip <$recip> blacklisted sender <$sender>"); |
---|
11202 | } |
---|
11203 | } |
---|
11204 | $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new($ldap_policy,'amavisWhitelistSender','L-')); |
---|
11205 | for my $key (@keys) { |
---|
11206 | if (grep {/^\Q$key\E\z/i} @$wblist) { |
---|
11207 | $found++; $wb = +1; $wr = $recip; $any_w++; |
---|
11208 | $r->recip_whitelisted_sender(1); |
---|
11209 | do_log(5,"wbl: (LDAP) recip <$recip> whitelisted sender <$sender>"); |
---|
11210 | } |
---|
11211 | } |
---|
11212 | } |
---|
11213 | if (!$found) { # fall back to static lookups if no match |
---|
11214 | # sender can be both white- and blacklisted at the same time |
---|
11215 | my($val); my($r_ref,$mk_ref,@t); |
---|
11216 | |
---|
11217 | # NOTE on the specifics of $per_recip_blacklist_sender_lookup_tables : |
---|
11218 | # the $r_ref below is supposed to be a ref to a single lookup table |
---|
11219 | # for compatibility with pre-2.0 versions of amavisd-new; |
---|
11220 | # Note that this is different from @score_sender_maps, which is |
---|
11221 | # supposed to contain a ref to a _list_ of lookup tables as a result |
---|
11222 | # of the first-level lookup (on the recipient address as a key). |
---|
11223 | # |
---|
11224 | ($r_ref,$mk_ref) = lookup(0,$recip, |
---|
11225 | Amavis::Lookup::Label->new("blacklist_recip<$recip>"), |
---|
11226 | cr('per_recip_blacklist_sender_lookup_tables')); |
---|
11227 | @t = ( (defined $r_ref ? $r_ref : ()), @{ca('blacklist_sender_maps')} ); |
---|
11228 | $val = lookup(0,$sender, |
---|
11229 | Amavis::Lookup::Label->new("blacklist_sender<$sender>"), |
---|
11230 | @t) if @t; |
---|
11231 | if ($val) { |
---|
11232 | $found++; $wb = -1; $br = $recip; $any_b++; |
---|
11233 | $r->recip_blacklisted_sender(1); |
---|
11234 | do_log(5,"wbl: recip <$recip> blacklisted sender <$sender>"); |
---|
11235 | } |
---|
11236 | # similar for whitelists: |
---|
11237 | ($r_ref,$mk_ref) = lookup(0,$recip, |
---|
11238 | Amavis::Lookup::Label->new("whitelist_recip<$recip>"), |
---|
11239 | cr('per_recip_whitelist_sender_lookup_tables')); |
---|
11240 | @t = ( (defined $r_ref ? $r_ref : ()), @{ca('whitelist_sender_maps')} ); |
---|
11241 | $val = lookup(0,$sender, |
---|
11242 | Amavis::Lookup::Label->new("whitelist_sender<$sender>"), |
---|
11243 | @t) if @t; |
---|
11244 | if ($val) { |
---|
11245 | $found++; $wb = +1; $wr = $recip; $any_w++; |
---|
11246 | $r->recip_whitelisted_sender(1); |
---|
11247 | do_log(5,"wbl: recip <$recip> whitelisted sender <$sender>"); |
---|
11248 | } |
---|
11249 | } |
---|
11250 | if (!defined($boost)) { # static lookups if no match |
---|
11251 | # note the first argument of lookup() is true, requesting ALL matches |
---|
11252 | my($r_ref,$mk_ref) = lookup(1,$recip, |
---|
11253 | Amavis::Lookup::Label->new("score_recip<$recip>"), |
---|
11254 | @{ca('score_sender_maps')}); |
---|
11255 | for my $j (0..$#{$r_ref}) { # for ALL tables matching the recipient |
---|
11256 | my($val,$key) = lookup(0,$sender, |
---|
11257 | Amavis::Lookup::Label->new("score_sender<$sender>"), |
---|
11258 | @{$r_ref->[$j]} ); |
---|
11259 | if ($val != 0) { |
---|
11260 | $boost += $val; |
---|
11261 | ll(2) && do_log(2, |
---|
11262 | sprintf("wbl: soft-%slisted (%s) sender <%s> => <%s>, ". |
---|
11263 | "recip_key=\"%s\"", ($val<0?'white':'black'), |
---|
11264 | $val, $sender, $recip, $mk_ref->[$j])); |
---|
11265 | } |
---|
11266 | } |
---|
11267 | } |
---|
11268 | $r->recip_score_boost($boost) if defined $boost; |
---|
11269 | $all = 0 if !$wb; |
---|
11270 | } |
---|
11271 | if (!ll(2)) { |
---|
11272 | # don't bother preparing log report which will not be printed |
---|
11273 | } else { |
---|
11274 | my($msg) = ''; |
---|
11275 | if ($all && $any_w && !$any_b) { $msg = "whitelisted" } |
---|
11276 | elsif ($all && $any_b && !$any_w) { $msg = "blacklisted" } |
---|
11277 | elsif ($all) { $msg = "black or whitelisted by all recips" } |
---|
11278 | elsif ($any_b || $any_w) { |
---|
11279 | $msg .= "whitelisted by ".($any_w>1?"$any_w recips, ":"$wr, ") if $any_w; |
---|
11280 | $msg .= "blacklisted by ".($any_b>1?"$any_b recips, ":"$br, ") if $any_b; |
---|
11281 | $msg .= "but not by all,"; |
---|
11282 | } |
---|
11283 | do_log(2,"wbl: $msg sender <$sender>") if $msg ne ''; |
---|
11284 | } |
---|
11285 | ($any_w+$any_b, $all); |
---|
11286 | } |
---|
11287 | |
---|
11288 | # - returns true if spam detected, |
---|
11289 | # - returns 0 if no spam found, |
---|
11290 | # - throws exception (die) in case of errors, |
---|
11291 | # or just returns undef if it did not complete its jobs |
---|
11292 | # |
---|
11293 | sub spam_scan($$) { |
---|
11294 | my($conn,$msginfo) = @_; |
---|
11295 | my($spam_level, $spam_status, $spam_report); my(@lines); |
---|
11296 | my($hdr_edits) = $msginfo->header_edits; |
---|
11297 | if (!$hdr_edits) { |
---|
11298 | $hdr_edits = Amavis::Out::EditHeader->new; |
---|
11299 | $msginfo->header_edits($hdr_edits); |
---|
11300 | } |
---|
11301 | my($dspam_signature,$dspam_result,$dspam_fname); |
---|
11302 | push(@lines, sprintf("Return-Path: %s\n", # fake a local delivery agent |
---|
11303 | qquote_rfc2821_local($msginfo->sender))); |
---|
11304 | push(@lines, sprintf("X-Envelope-To: %s\n", |
---|
11305 | join(",\n ",qquote_rfc2821_local(@{$msginfo->recips})))); |
---|
11306 | my($fh) = $msginfo->mail_text; |
---|
11307 | my($mbsl) = c('sa_mail_body_size_limit'); |
---|
11308 | if ( defined $mbsl && |
---|
11309 | ($msginfo->orig_body_size > $mbsl || |
---|
11310 | $msginfo->orig_header_size + 1 + $msginfo->orig_body_size |
---|
11311 | > 5*1024 + $mbsl) |
---|
11312 | ) { |
---|
11313 | do_log(1,"spam_scan: not wasting time on SA, message ". |
---|
11314 | "longer than $mbsl bytes: ". |
---|
11315 | $msginfo->orig_header_size .'+'. $msginfo->orig_body_size); |
---|
11316 | } else { |
---|
11317 | if ($dspam eq '') { |
---|
11318 | do_log(5,"spam_scan: DSPAM not available, skipping it"); |
---|
11319 | } else { |
---|
11320 | # pass the mail to DSPAM, extract its result headers and feed them to SA |
---|
11321 | $dspam_fname = $msginfo->mail_tempdir . '/dspam.msg'; |
---|
11322 | my($dspam_fh) = IO::File->new; # will receive output from DSPAM |
---|
11323 | $dspam_fh->open($dspam_fname,'>',0640) |
---|
11324 | or die "Can't create file $dspam_fname: $!"; |
---|
11325 | $fh->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
11326 | my($proc_fh,$pid) = run_command('&'.fileno($fh), "&1", $dspam, |
---|
11327 | qw(--stdout --deliver=spam,innocent |
---|
11328 | --mode=tum --feature=chained,noise |
---|
11329 | --enable-signature-headers |
---|
11330 | --user), $daemon_user, |
---|
11331 | ); # --mode=teft |
---|
11332 | # qw(--stdout --deliver-spam) # dspam < 3.0 |
---|
11333 | # keep X-DSPAM-*, ignore other changes e.g. Content-Transfer-Encoding |
---|
11334 | my($all_local) = !grep { !lookup(0,$_,@{ca('local_domains_maps')}) } |
---|
11335 | @{$msginfo->recips}; |
---|
11336 | my($first_line); |
---|
11337 | while (defined($_ = $proc_fh->getline)) { # scan mail header from DSPAM |
---|
11338 | $dspam_fh->print($_) or die "Can't write to $dspam_fname: $!"; |
---|
11339 | if (!defined($first_line)) |
---|
11340 | { $first_line = $_; do_log(5,"spam_scan: from DSPAM: $first_line") } |
---|
11341 | last if $_ eq $eol; |
---|
11342 | local($1,$2); |
---|
11343 | if (/^(X-DSPAM[^:]*):[ \t]*(.*)$/) { # note: does not handle folding |
---|
11344 | my($hh,$hb) = ($1,$2); |
---|
11345 | $dspam_signature = $hb if /^X-DSPAM-Signature:/i; |
---|
11346 | $dspam_result = $hb if /^X-DSPAM-Result:/i; |
---|
11347 | do_log(3,$_); push(@lines,$_); # store header in array passed to SA |
---|
11348 | # add DSPAM header fields to passed mail |
---|
11349 | $hdr_edits->append_header($hh,$hb) if $all_local; |
---|
11350 | } |
---|
11351 | } |
---|
11352 | while ($proc_fh->read($_,16384) > 0) { # copy mail body from DSPAM |
---|
11353 | $dspam_fh->print($_) or die "Can't write to $dspam_fname: $!"; |
---|
11354 | } |
---|
11355 | my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); |
---|
11356 | $dspam_fh->close or die "Can't close $dspam_fname: $!"; |
---|
11357 | do_log(-1,sprintf("WARN: DSPAM problem, %s, result=%s", |
---|
11358 | exit_status_str($?,$err), $first_line) |
---|
11359 | ) if $retval || !defined $first_line; |
---|
11360 | do_log(4,"spam_scan: DSPAM gave: $dspam_signature, $dspam_result"); |
---|
11361 | section_time('DSPAM'); |
---|
11362 | } |
---|
11363 | # read mail into memory in preparation for SpamAssasin |
---|
11364 | my($body_lines) = 0; |
---|
11365 | $fh->seek(0,0) or die "Can't rewind mail file: $!"; |
---|
11366 | while (<$fh>) { push(@lines,$_); last if $_ eq $eol } # header |
---|
11367 | while (<$fh>) { push(@lines,$_); $body_lines++ } # body |
---|
11368 | section_time('SA msg read'); |
---|
11369 | |
---|
11370 | my($sa_required, $sa_tests); |
---|
11371 | my($saved_umask) = umask; |
---|
11372 | my($remaining_time) = alarm(0); # check how much time is left |
---|
11373 | eval { |
---|
11374 | # NOTE ON TIMEOUTS: SpamAssassin may use timer for its own purpose, |
---|
11375 | # disabling it before returning. It seems it only uses timer when |
---|
11376 | # external tests are enabled, so in order for our timeout to be |
---|
11377 | # useful, $sa_local_tests_only needs to be true (e.g. 1). |
---|
11378 | local $SIG{ALRM} = sub { |
---|
11379 | my($s) = Carp::longmess("SA TIMED OUT, backtrace:"); |
---|
11380 | # crop at some rather arbitrary limit |
---|
11381 | if (length($s) > 900) { $s = substr($s,0,900-3) . "..." } |
---|
11382 | do_log(-1,$s); |
---|
11383 | }; |
---|
11384 | # prepared to wait no more than n seconds |
---|
11385 | alarm($sa_timeout) if $sa_timeout > 0; |
---|
11386 | my($mail_obj); my($sa_version) = Mail::SpamAssassin::Version(); |
---|
11387 | do_log(5,"calling SA parse, SA version $sa_version"); |
---|
11388 | # *** note that $sa_version could be 3.0.1, which is not really numeric! |
---|
11389 | if ($sa_version >= 3) { |
---|
11390 | $mail_obj = $spamassassin_obj->parse(\@lines); |
---|
11391 | } else { # 2.63 or earlier |
---|
11392 | $mail_obj = Mail::SpamAssassin::NoMailAudit->new(data => \@lines, |
---|
11393 | add_From_line => 0); |
---|
11394 | } |
---|
11395 | section_time('SA parse'); |
---|
11396 | do_log(4,"CALLING SA check"); |
---|
11397 | my($per_msg_status); |
---|
11398 | { local($1,$2,$3,$4,$5,$6); # avoid Perl 5.8.0 bug, $1 gets tainted |
---|
11399 | $per_msg_status = $spamassassin_obj->check($mail_obj); |
---|
11400 | } |
---|
11401 | my($rem_t) = alarm(0); |
---|
11402 | do_log(4,"RETURNED FROM SA check, time left: $rem_t s"); |
---|
11403 | |
---|
11404 | { local($1,$2,$3,$4); # avoid Perl 5.8.0..5.8.3...? taint bug |
---|
11405 | $spam_level = $per_msg_status->get_hits; |
---|
11406 | $sa_required = $per_msg_status->get_required_hits; # not used |
---|
11407 | $sa_tests = $per_msg_status->get_names_of_tests_hit; |
---|
11408 | $spam_report = $per_msg_status->get_report; # taints $1 and $2 ! |
---|
11409 | |
---|
11410 | # example of how to gather aditional information from SA: |
---|
11411 | # my($trusted) = $per_msg_status->_get_tag('RELAYSTRUSTED'); |
---|
11412 | # $hdr_edits->append_header('X-TESTING',$trusted); |
---|
11413 | |
---|
11414 | #Experimental, unfinished: |
---|
11415 | # $per_msg_status->rewrite_mail; |
---|
11416 | # my($entity) = nomailaudit_to_mime_entity($mail_obj); |
---|
11417 | |
---|
11418 | $per_msg_status->finish(); |
---|
11419 | } |
---|
11420 | }; |
---|
11421 | section_time('SA check'); |
---|
11422 | umask($saved_umask); # SA changes umask to 0077 |
---|
11423 | prolong_timer('spam_scan_SA', $remaining_time); # restart the timer |
---|
11424 | if ($@ ne '') { # SA timed out? |
---|
11425 | chomp($@); |
---|
11426 | die "$@\n" if $@ ne "timed out"; |
---|
11427 | } |
---|
11428 | $sa_tests =~ s/,\s*/,/g; $spam_status = "tests=".$sa_tests; |
---|
11429 | |
---|
11430 | if ($dspam ne '' && defined $spam_level) { # DSPAM auto-learn |
---|
11431 | my($eat,@options); |
---|
11432 | @options = (qw(--stdout --mode=tum --user), $daemon_user); # --mode=teft |
---|
11433 | if ( $spam_level > 5.0 && $dspam_result eq 'Innocent') { |
---|
11434 | $eat = 'SPAM'; push(@options, qw(--class=spam --source=error)); |
---|
11435 | # @options = qw(--stdout --addspam); # dspam < 3.0 |
---|
11436 | } |
---|
11437 | elsif ($spam_level < 1.0 && $dspam_result eq 'Spam') { |
---|
11438 | $eat = 'HAM'; push(@options, qw(--class=innocent --source=error)); |
---|
11439 | # @options = qw(--stdout --falsepositive); # dspam < 3.0 |
---|
11440 | } |
---|
11441 | if (defined $eat && $dspam_signature ne '') { |
---|
11442 | do_log(2,"DSPAM learn $eat ($spam_level), $dspam_signature"); |
---|
11443 | my($proc_fh,$pid) = run_command($dspam_fname, "&1", $dspam, @options); |
---|
11444 | while (defined($_ = $proc_fh->getline)) {} # consume possible output |
---|
11445 | # my($output) = join('', $proc_fh->getlines); # consume possible output |
---|
11446 | my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); |
---|
11447 | # do_log(-1,"DSPAM learn $eat response:".$output) if $output ne ''; |
---|
11448 | $retval==0 or die ("DSPAM learn $eat FAILED: ".exit_status_str($?,$err)); |
---|
11449 | section_time('DSPAM learn'); |
---|
11450 | } |
---|
11451 | } |
---|
11452 | } |
---|
11453 | if (defined $dspam_fname) { |
---|
11454 | if (($spam_level > 5.0 ? 1 : 0) != ($dspam_result eq 'Spam' ? 1 : 0)) |
---|
11455 | { do_log(2,"DSPAM: different opinions: $dspam_result, $spam_level") } |
---|
11456 | unlink($dspam_fname) or die "Can't delete file $dspam_fname: $!"; |
---|
11457 | } |
---|
11458 | do_log(3,"spam_scan: hits=$spam_level $spam_status"); |
---|
11459 | ($spam_level, $spam_status, $spam_report); |
---|
11460 | } |
---|
11461 | |
---|
11462 | #sub nomailaudit_to_mime_entity($) { |
---|
11463 | # my($mail_obj) = @_; # expect a Mail::SpamAssassin::MsgContainer object |
---|
11464 | # my(@m_hdr) = $mail_obj->header; # in array context returns array of lines |
---|
11465 | # my($m_body) = $mail_obj->body; # returns array ref |
---|
11466 | # my($entity); |
---|
11467 | # # make sure _our_ source line number is reported in case of failure |
---|
11468 | # eval {$entity = MIME::Entity->build( |
---|
11469 | # Type => 'text/plain', Encoding => '-SUGGEST', |
---|
11470 | # Data => $m_body); 1} or do {chomp($@); die $@}; |
---|
11471 | # my($head) = $entity->head; |
---|
11472 | # # insert header fields from template into MIME::Head entity |
---|
11473 | # for my $hdr_line (@m_hdr) { |
---|
11474 | # # make sure _our_ source line number is reported in case of failure |
---|
11475 | # eval {$head->replace($fhead,$fbody); 1} or do {chomp($@); die $@}; |
---|
11476 | # } |
---|
11477 | # $entity; # return the built MIME::Entity |
---|
11478 | #} |
---|
11479 | |
---|
11480 | 1; |
---|
11481 | |
---|
11482 | __DATA__ |
---|
11483 | # |
---|
11484 | package Amavis::Unpackers; |
---|
11485 | use strict; |
---|
11486 | use re 'taint'; |
---|
11487 | |
---|
11488 | BEGIN { |
---|
11489 | use Exporter (); |
---|
11490 | use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); |
---|
11491 | $VERSION = '2.034'; |
---|
11492 | @ISA = qw(Exporter); |
---|
11493 | %EXPORT_TAGS = (); |
---|
11494 | @EXPORT = (); |
---|
11495 | @EXPORT_OK = qw(&init &decompose_part &determine_file_types); |
---|
11496 | } |
---|
11497 | use Errno qw(ENOENT EACCES); |
---|
11498 | use File::Basename qw(basename); |
---|
11499 | use Convert::TNEF; |
---|
11500 | use Convert::UUlib qw(:constants); |
---|
11501 | use Compress::Zlib; |
---|
11502 | use Archive::Tar; |
---|
11503 | use Archive::Zip qw(:CONSTANTS :ERROR_CODES); |
---|
11504 | use File::Copy; |
---|
11505 | |
---|
11506 | BEGIN { |
---|
11507 | import Amavis::Util qw(untaint min max ll do_log retcode exit_status_str |
---|
11508 | snmp_count prolong_timer sanitize_str run_command |
---|
11509 | rmdir_recursively); |
---|
11510 | import Amavis::Conf qw(:platform :confvars :unpack c cr ca); |
---|
11511 | import Amavis::Timing qw(section_time); |
---|
11512 | import Amavis::Lookup qw(lookup); |
---|
11513 | import Amavis::Unpackers::MIME qw(mime_decode); |
---|
11514 | import Amavis::Unpackers::NewFilename qw(consumed_bytes); |
---|
11515 | } |
---|
11516 | |
---|
11517 | use subs @EXPORT_OK; |
---|
11518 | |
---|
11519 | # recursively descend into a directory $dir containing potentially unsafe |
---|
11520 | # files with unpredictable names, soft links, etc., rename each regular |
---|
11521 | # nonempty file to directory $outdir giving it a generated name, |
---|
11522 | # and discard all the rest, including the directory $dir. |
---|
11523 | # Return a pair: number of bytes that 'sanitized' files now occupy, |
---|
11524 | # and a number of parts objects created. |
---|
11525 | # |
---|
11526 | sub flatten_and_tidy_dir($$$;$$); # prototype |
---|
11527 | sub flatten_and_tidy_dir($$$;$$) { |
---|
11528 | my($dir, $outdir, $parent_obj, $item_num_offset, $orig_names) = @_; |
---|
11529 | do_log(4, "flatten_and_tidy_dir: processing directory \"$dir\""); |
---|
11530 | my($cnt_r,$cnt_u) = (0,0); my($consumed_bytes) = 0; |
---|
11531 | local(*DIR); my($f); |
---|
11532 | chmod(0750, $dir) or die "Can't change protection of \"$dir\": $!"; |
---|
11533 | opendir(DIR, $dir) or die "Can't open directory \"$dir\": $!"; |
---|
11534 | my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement; |
---|
11535 | while (defined($f = readdir(DIR))) { |
---|
11536 | my($msg); my($fname) = "$dir/$f"; |
---|
11537 | my($errn) = lstat($fname) ? 0 : 0+$!; |
---|
11538 | if ($errn == ENOENT) { $msg = "does not exist" } |
---|
11539 | elsif ($errn) { $msg = "inaccessible: $!" } |
---|
11540 | if (defined $msg) { die "flatten_and_tidy_dir: \"$fname\" $msg," } |
---|
11541 | next if ($f eq '.' || $f eq '..') && -d _; |
---|
11542 | my($newpart_obj) = Amavis::Unpackers::Part->new($outdir,$parent_obj); |
---|
11543 | $item_num++; |
---|
11544 | $newpart_obj->mime_placement(sprintf("%s/%d",$parent_placement, |
---|
11545 | $item_num+$item_num_offset) ); |
---|
11546 | # save tainted original member name if available, or a tainted file name |
---|
11547 | my($original_name) = !ref($orig_names) ? undef : $orig_names->{$f}; |
---|
11548 | $newpart_obj->name_declared(defined $original_name ? $original_name : $f); |
---|
11549 | # untaint, but if $dir happens to still be tainted, we want to know and die |
---|
11550 | $fname = $dir.'/'.untaint($f); |
---|
11551 | if (-d _) { |
---|
11552 | $newpart_obj->attributes_add('D'); |
---|
11553 | my($bytes,$cnt) = flatten_and_tidy_dir($fname, $outdir, $parent_obj, |
---|
11554 | $item_num+$item_num_offset, $orig_names); |
---|
11555 | $consumed_bytes += $bytes; $item_num += $cnt; |
---|
11556 | } elsif (-l _) { |
---|
11557 | $cnt_u++; $newpart_obj->attributes_add('L'); |
---|
11558 | unlink($fname) or die "Can't remove soft link \"$fname\": $!"; |
---|
11559 | } elsif (!-f _) { |
---|
11560 | do_log(4, "flatten_and_tidy_dir: NONREGULAR FILE \"$fname\""); |
---|
11561 | $cnt_u++; $newpart_obj->attributes_add('S'); |
---|
11562 | unlink($fname) or die "Can't remove nonregular file \"$fname\": $!"; |
---|
11563 | } elsif (-z _) { |
---|
11564 | $cnt_u++; |
---|
11565 | unlink($fname) or die "Can't remove empty file \"$fname\": $!"; |
---|
11566 | } else { |
---|
11567 | chmod(0750, $fname) |
---|
11568 | or die "Can't change protection of file \"$fname\": $!"; |
---|
11569 | my($size) = 0 + (-s _); |
---|
11570 | $newpart_obj->size($size); |
---|
11571 | $consumed_bytes += $size; |
---|
11572 | my($newpart) = $newpart_obj->full_name; |
---|
11573 | ll(5) && do_log(5, |
---|
11574 | sprintf("flatten_and_tidy_dir: renaming \"%s\"%s to %s", $fname, |
---|
11575 | !defined $original_name ? '' : " ($original_name)", $newpart)); |
---|
11576 | $cnt_r++; |
---|
11577 | rename($fname, $newpart) |
---|
11578 | or die "Can't rename \"$fname\" to $newpart: $!"; |
---|
11579 | } |
---|
11580 | } |
---|
11581 | closedir(DIR) or die "Can't close directory \"$dir\": $!"; |
---|
11582 | rmdir($dir) or die "Can't remove directory \"$dir\": $!"; |
---|
11583 | section_time("ren$cnt_r-unl$cnt_u-files$item_num"); |
---|
11584 | ($consumed_bytes, $item_num); |
---|
11585 | } |
---|
11586 | |
---|
11587 | # call 'file(1)' utility for each part, |
---|
11588 | # and associate (save) full and short types with each part |
---|
11589 | # |
---|
11590 | sub determine_file_types($$) { |
---|
11591 | my($tempdir, $partslist_ref) = @_; |
---|
11592 | $file ne '' or die "Unix utility file(1) not available, but is needed"; |
---|
11593 | my($cwd) = "$tempdir/parts"; |
---|
11594 | my(@part_list) = grep { $_->exists } @$partslist_ref; |
---|
11595 | if (!@part_list) { do_log(5, "no parts, file(1) not called") } |
---|
11596 | else { |
---|
11597 | local($1,$2); # avoid Perl taint bug (5.8.3), $cwd and $arg are not tainted |
---|
11598 | # but $arg becomes tainted because $1 is tainted from before |
---|
11599 | my(@file_list); |
---|
11600 | for my $part (@part_list) { |
---|
11601 | my($arg) = $part->full_name; |
---|
11602 | $arg =~ s{^\Q$cwd\E/(.*)\z}{$1}s; # remove cwd if possible |
---|
11603 | push(@file_list, $arg); |
---|
11604 | } |
---|
11605 | chdir($cwd) or die "Can't chdir to $cwd: $!"; |
---|
11606 | my($proc_fh,$pid) = run_command(undef, "&1", $file, @file_list); |
---|
11607 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
11608 | local($_); my($index) = 0; |
---|
11609 | while (defined($_ = $proc_fh->getline)) { |
---|
11610 | chomp; |
---|
11611 | do_log(5, "result line from file(1): ".$_); |
---|
11612 | if ($index > $#file_list) { |
---|
11613 | do_log(-1, "NOTICE: Skipping extra output from file(1): $_"); |
---|
11614 | } else { |
---|
11615 | my($part) = $part_list[$index]; # walk through @part_list in sync |
---|
11616 | my($expect) = $file_list[$index]; # walk through @file_list in sync |
---|
11617 | if (!/^(\Q$expect\E):[ \t]*(.*)\z/s) { # split file name from type |
---|
11618 | do_log(-1,"NOTICE: Skipping bad output from file(1) ". |
---|
11619 | "at [$index, $expect], got: $_"); |
---|
11620 | } else { |
---|
11621 | my($type_short); my($actual_name) = $1; my($type_long) = $2; |
---|
11622 | $type_short = lookup(0,$type_long,@map_full_type_to_short_type_maps); |
---|
11623 | do_log(4, sprintf("File-type of %s: %s%s", |
---|
11624 | $part->base_name, $type_long, |
---|
11625 | (!defined $type_short ? '' |
---|
11626 | : !ref $type_short ? "; ($type_short)" |
---|
11627 | : '; (' . join(', ',@$type_short) . ')' |
---|
11628 | ) )); |
---|
11629 | $part->type_long($type_long); $part->type_short($type_short); |
---|
11630 | $part->attributes_add('C') |
---|
11631 | if !ref($type_short) ? $type_short eq 'pgp' # encrypted? |
---|
11632 | : grep {$_ eq 'pgp'} @$type_short; |
---|
11633 | $index++; |
---|
11634 | } |
---|
11635 | } |
---|
11636 | } |
---|
11637 | if ($index < @part_list) { |
---|
11638 | die sprintf("parsing file(1) results - missing last %d results", |
---|
11639 | @part_list - $index); |
---|
11640 | } |
---|
11641 | my($err); $proc_fh->close or $err = $!; |
---|
11642 | $?==0 or die ("'file' utility ($file) failed, ".exit_status_str($?,$err)); |
---|
11643 | section_time(sprintf('get-file-type%d', scalar(@part_list))); |
---|
11644 | } |
---|
11645 | } |
---|
11646 | |
---|
11647 | sub decompose_mail($$) { |
---|
11648 | my($tempdir,$file_generator_object) = @_; |
---|
11649 | |
---|
11650 | my($hold); my(@parts); my($depth) = 1; my($any_undecipherable) = 0; |
---|
11651 | my($which_section) = "parts_decode"; |
---|
11652 | # fetch all not-yet-visited part names, and start a new cycle |
---|
11653 | TIER: |
---|
11654 | while (@parts = @{$file_generator_object->parts_list}) { |
---|
11655 | if ($MAXLEVELS && $depth > $MAXLEVELS) { |
---|
11656 | $hold = "Maximum decoding depth ($MAXLEVELS) exceeded"; |
---|
11657 | last; |
---|
11658 | } |
---|
11659 | $file_generator_object->parts_list_reset; # new names cycle |
---|
11660 | # clip to avoid very long log entries |
---|
11661 | my(@chopped_parts) = @parts > 5 ? @parts[0..4] : @parts; |
---|
11662 | ll(4) && do_log(4,sprintf("decode_parts: level=%d, #parts=%d : %s", |
---|
11663 | $depth, scalar(@parts), |
---|
11664 | join(', ', (map { $_->base_name } @chopped_parts), |
---|
11665 | (@chopped_parts >= @parts ? () : "...")) )); |
---|
11666 | for my $part (@parts) { # test for existence of all expected files |
---|
11667 | my($fname) = $part->full_name; |
---|
11668 | my($errn) = lstat($fname) ? 0 : 0+$!; |
---|
11669 | if ($errn == ENOENT) { |
---|
11670 | $part->exists(0); |
---|
11671 | # $part->type_short('no-file') if !defined $part->type_short; |
---|
11672 | } elsif ($errn) { |
---|
11673 | die "decompose_mail: inaccessible file $fname: $!"; |
---|
11674 | } elsif (!-f _) { # not a regular file |
---|
11675 | my($what) = -l _ ? 'symlink' : -d _ ? 'directory' : 'non-regular file'; |
---|
11676 | do_log(-1, "WARN: decompose_mail: removing unexpected $what $fname"); |
---|
11677 | if (-d _) { rmdir_recursively($fname) } |
---|
11678 | else { unlink($fname) or die "Can't delete $what $fname: $!" } |
---|
11679 | $part->exists(0); |
---|
11680 | $part->type_short(-l _ ? 'symlink' : -d _ ? 'dir' : 'special') |
---|
11681 | if !defined $part->type_short; |
---|
11682 | } elsif (-z _) { # empty file |
---|
11683 | unlink($fname) or die "Can't remove \"$fname\": $!"; |
---|
11684 | $part->exists(0); |
---|
11685 | $part->type_short('empty') if !defined $part->type_short; |
---|
11686 | $part->type_long('empty') if !defined $part->type_long; |
---|
11687 | } else { |
---|
11688 | $part->exists(1); |
---|
11689 | } |
---|
11690 | } |
---|
11691 | determine_file_types($tempdir, \@parts); |
---|
11692 | for my $part (@parts) { |
---|
11693 | if ($part->exists && !defined($hold)) |
---|
11694 | { $hold = decompose_part($part, $tempdir) } |
---|
11695 | $any_undecipherable++ if grep {$_ eq 'U'} @{ $part->attributes || [] }; |
---|
11696 | } |
---|
11697 | last TIER if defined $hold; |
---|
11698 | $depth++; |
---|
11699 | } |
---|
11700 | section_time($which_section); prolong_timer($which_section); |
---|
11701 | ($hold, $any_undecipherable); |
---|
11702 | } |
---|
11703 | |
---|
11704 | # Decompose the part |
---|
11705 | sub decompose_part($$) { |
---|
11706 | my($part, $tempdir) = @_; |
---|
11707 | my($hold); |
---|
11708 | my($none_called); |
---|
11709 | # possible return values from eval: |
---|
11710 | # 0 - truly atomic, or unknown or archiver failure; consider atomic |
---|
11711 | # 1 - some archiver format, successfully unpacked, result replaces original |
---|
11712 | # 2 - probably unpacked, but keep the original (eg self-extracting archive) |
---|
11713 | my($sts) = eval { |
---|
11714 | my($type_short) = $part->type_short; |
---|
11715 | my(@ts) = !defined $type_short ? () |
---|
11716 | : !ref $type_short ? ($type_short) : @$type_short; |
---|
11717 | return 0 if !@ts; # consider atomic if unknown (returns from eval) |
---|
11718 | snmp_count("OpsDecType-".join('.',@ts)); |
---|
11719 | |
---|
11720 | grep(/^mail\z/,@ts) && return do {mime_decode($part,$tempdir,$part); 2}; |
---|
11721 | grep(/^(asc|uue|hqx|ync)\z/,@ts) && return do_ascii($part,$tempdir); |
---|
11722 | grep(/^F\z/,@ts) && defined $unfreeze |
---|
11723 | && return do_uncompress($part,$tempdir,$unfreeze); |
---|
11724 | grep(/^Z\z/,@ts) && defined $uncompress |
---|
11725 | && return do_uncompress($part,$tempdir,$uncompress); |
---|
11726 | grep(/^bz2?\z/,@ts) && defined $bzip2 |
---|
11727 | && return do_uncompress($part,$tempdir,"$bzip2 -d"); |
---|
11728 | grep(/^gz\z/,@ts) && defined $gzip |
---|
11729 | && return do_uncompress($part,$tempdir,"$gzip -d"); |
---|
11730 | grep(/^gz\z/,@ts) && return do_gunzip($part,$tempdir); # fallback |
---|
11731 | grep(/^lzo\z/,@ts) && defined $lzop |
---|
11732 | && return do_uncompress($part,$tempdir,"$lzop -d"); |
---|
11733 | grep(/^rpm\z/,@ts) && defined $rpm2cpio && (defined $pax || defined $cpio) |
---|
11734 | && return do_uncompress($part,$tempdir,$rpm2cpio); |
---|
11735 | grep(/^(cpio|tar)\z/,@ts) && (defined $pax || defined $cpio) |
---|
11736 | && return do_pax_cpio($part,$tempdir, $pax || $cpio); |
---|
11737 | grep(/^tar\z/,@ts) && return do_tar($part,$tempdir); # fallback |
---|
11738 | grep(/^deb\z/,@ts) && defined $ar |
---|
11739 | && return do_ar($part,$tempdir); |
---|
11740 | # grep(/^a\z/,@ts) && defined $ar # unpacking .a seems like overkill |
---|
11741 | # && return do_ar($part,$tempdir); |
---|
11742 | grep(/^zip\z/,@ts) && return do_unzip($part,$tempdir); |
---|
11743 | grep(/^rar\z/,@ts) && defined $unrar |
---|
11744 | && return do_unrar($part,$tempdir); |
---|
11745 | grep(/^(lha|lzh)\z/,@ts) && defined $lha |
---|
11746 | && return do_lha($part,$tempdir); |
---|
11747 | grep(/^arc\z/,@ts) && defined $arc |
---|
11748 | && return do_arc($part,$tempdir); |
---|
11749 | grep(/^arj\z/,@ts) && defined $unarj |
---|
11750 | && return do_unarj($part,$tempdir); |
---|
11751 | grep(/^zoo\z/,@ts) && defined $zoo |
---|
11752 | && return do_zoo($part,$tempdir); |
---|
11753 | grep(/^cab\z/,@ts) && defined $cabextract |
---|
11754 | && return do_cabextract($part,$tempdir); |
---|
11755 | grep(/^doc\z/,@ts) && defined $ripole |
---|
11756 | && return do_ole($part,$tempdir); |
---|
11757 | grep(/^tnef\z/,@ts) && return do_tnef($part,$tempdir); |
---|
11758 | grep(/^exe\z/,@ts) && return do_executable($part,$tempdir); |
---|
11759 | |
---|
11760 | # Falling through (e.g. HTML) - no match, consider atomic |
---|
11761 | $none_called = 1; |
---|
11762 | return 0; # returns from eval |
---|
11763 | }; |
---|
11764 | if ($@ ne '') { |
---|
11765 | chomp($@); |
---|
11766 | if ($@ =~ /^Exceeded storage quota/ || |
---|
11767 | $@ =~ /^Maximum number of files.*exceeded/) { $hold = $@ } |
---|
11768 | else { |
---|
11769 | do_log(-1,sprintf("Decoding of %s (%s) failed, leaving it unpacked: %s", |
---|
11770 | $part->base_name, $part->type_long, $@)); |
---|
11771 | } |
---|
11772 | $sts = 2; |
---|
11773 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; # just in case |
---|
11774 | } |
---|
11775 | if ($sts == 1 && lookup(0,$part->type_long, @keep_decoded_original_maps)) { |
---|
11776 | # don't trust this file type or unpacker, |
---|
11777 | # keep both the original and the unpacked file |
---|
11778 | ll(4) && do_log(4,sprintf("file type is %s, retain original %s", |
---|
11779 | $part->type_long, $part->base_name)); |
---|
11780 | $sts = 2; |
---|
11781 | } |
---|
11782 | if ($sts == 1) { |
---|
11783 | ll(5) && do_log(5, "decompose_part: deleting ".$part->full_name); |
---|
11784 | unlink($part->full_name) |
---|
11785 | or die sprintf("Can't unlink %s: %s", $part->full_name, $!); |
---|
11786 | } |
---|
11787 | ll(4) && do_log(4,sprintf("decompose_part: %s - %s", $part->base_name, |
---|
11788 | ['atomic','archive, unpacked','source retained']->[$sts])); |
---|
11789 | section_time('decompose_part') unless $none_called; |
---|
11790 | $hold; |
---|
11791 | } |
---|
11792 | |
---|
11793 | # |
---|
11794 | # Uncompression/unarchiving routines |
---|
11795 | # Possible return codes: |
---|
11796 | # 0 - truly atomic, or unknown or archiver failure; consider atomic |
---|
11797 | # 1 - some archiver format, successfully unpacked, result replaces original |
---|
11798 | # 2 - probably unpacked, but keep the original (eg self-extracting archive) |
---|
11799 | |
---|
11800 | # if ASCII text, try multiple decoding methods as provided by UUlib |
---|
11801 | # (uuencoded, xxencoded, BinHex, yEnc, Base64, Quoted-Printable) |
---|
11802 | sub do_ascii($$) { |
---|
11803 | my($part, $tempdir) = @_; |
---|
11804 | |
---|
11805 | snmp_count('OpsDecByUUlibAttempt'); |
---|
11806 | my($envtmpdir_changed); |
---|
11807 | # prevent uunconc.c/UUDecode() from trying to create temp file in '/' |
---|
11808 | if ($ENV{TMPDIR} eq '') { $ENV{TMPDIR} = $TEMPBASE; $envtmpdir_changed = 1 } |
---|
11809 | |
---|
11810 | my($any_errors,$any_decoded); |
---|
11811 | eval { # must not go away without calling Convert::UUlib::CleanUp! |
---|
11812 | my($sts,$count); |
---|
11813 | $sts = Convert::UUlib::Initialize(); |
---|
11814 | $sts==RET_OK or die "Convert::UUlib::Initialize failed: " |
---|
11815 | . Convert::UUlib::strerror($sts); |
---|
11816 | my($uulib_version) = Convert::UUlib::GetOption(OPT_VERSION); |
---|
11817 | !Convert::UUlib::SetOption(OPT_IGNMODE,1) or die "bad uulib OPT_IGNMODE"; |
---|
11818 | # !Convert::UUlib::SetOption(OPT_DESPERATE,1) or die "bad uulib OPT_DESPERATE"; |
---|
11819 | ($sts, $count) = Convert::UUlib::LoadFile($part->full_name); |
---|
11820 | if ($sts != RET_OK) { |
---|
11821 | my($errmsg) = Convert::UUlib::strerror($sts) . ": $!"; |
---|
11822 | $errmsg .= ", (???" |
---|
11823 | . Convert::UUlib::strerror(Convert::UUlib::GetOption(OPT_ERRNO))."???)" |
---|
11824 | if $sts == RET_IOERR; |
---|
11825 | die "Convert::UUlib::LoadFile (uulib V$uulib_version) failed: $errmsg"; |
---|
11826 | } |
---|
11827 | ll(4) && do_log(4,sprintf( |
---|
11828 | "do_ascii: Decoding part %s (%d items), uulib V%s", |
---|
11829 | $part->base_name, $count, $uulib_version)); |
---|
11830 | my($uu); |
---|
11831 | my($item_num) = 0; my($parent_placement) = $part->mime_placement; |
---|
11832 | for (my($j) = 0; $uu = Convert::UUlib::GetFileListItem($j); $j++) { |
---|
11833 | $item_num++; |
---|
11834 | ll(4) && do_log(4,sprintf( |
---|
11835 | "do_ascii(%d): state=0x%02x, enc=%s%s, est.size=%s, name=%s", |
---|
11836 | $j, $uu->state, Convert::UUlib::strencoding($uu->uudet), |
---|
11837 | ($uu->mimetype ne '' ? ", mimetype=" . $uu->mimetype : ''), |
---|
11838 | $uu->size, $uu->filename)); |
---|
11839 | if (!($uu->state & FILE_OK)) { |
---|
11840 | $any_errors++; |
---|
11841 | do_log(1,"do_ascii: Convert::UUlib info: $j not decodable, ".$uu->state); |
---|
11842 | } else { |
---|
11843 | my($newpart_obj)=Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
11844 | $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
11845 | $newpart_obj->name_declared($uu->filename); |
---|
11846 | my($newpart) = $newpart_obj->full_name; |
---|
11847 | $! = undef; |
---|
11848 | $sts = $uu->decode($newpart); # decode to file $newpart |
---|
11849 | my($err_decode) = "$!"; |
---|
11850 | chmod(0750, $newpart) or $! == ENOENT # chmod, don't panic if no file |
---|
11851 | or die "Can't change protection of \"$newpart\": $!"; |
---|
11852 | my($statmsg); |
---|
11853 | my($errn) = lstat($newpart) ? 0 : 0+$!; |
---|
11854 | if ($errn == ENOENT) { $statmsg = "does not exist" } |
---|
11855 | elsif ($errn) { $statmsg = "inaccessible: $!" } |
---|
11856 | elsif ( -l _) { $statmsg = "is a symlink" } |
---|
11857 | elsif ( -d _) { $statmsg = "is a directory" } |
---|
11858 | elsif (!-f _) { $statmsg = "not a regular file" } |
---|
11859 | if (defined $statmsg) { $statmsg = "; file status: $newpart $statmsg" } |
---|
11860 | my($size) = 0 + (-s _); |
---|
11861 | $newpart_obj->size($size); |
---|
11862 | consumed_bytes($size, 'do_ascii'); |
---|
11863 | if ($sts == RET_OK && $errn==0) { |
---|
11864 | $any_decoded++; |
---|
11865 | do_log(4,"do_ascii: RET_OK" . $statmsg) if defined $statmsg; |
---|
11866 | } elsif ($sts == RET_NODATA || $sts == RET_NOEND) { |
---|
11867 | $any_errors++; |
---|
11868 | do_log(-1,"do_ascii: Convert::UUlib error: " |
---|
11869 | . Convert::UUlib::strerror($sts) . $statmsg); |
---|
11870 | } else { |
---|
11871 | $any_errors++; |
---|
11872 | my($errmsg) = Convert::UUlib::strerror($sts) . ":: $err_decode"; |
---|
11873 | $errmsg .= ", " . Convert::UUlib::strerror( |
---|
11874 | Convert::UUlib::GetOption(OPT_ERRNO) ) if $sts == RET_IOERR; |
---|
11875 | die ("Convert::UUlib failed: " . $errmsg . $statmsg); |
---|
11876 | } |
---|
11877 | } |
---|
11878 | } |
---|
11879 | }; |
---|
11880 | my($eval_stat) = $@; |
---|
11881 | Convert::UUlib::CleanUp(); |
---|
11882 | snmp_count('OpsDecByUUlib') if $any_decoded; |
---|
11883 | delete $ENV{TMPDIR} if $envtmpdir_changed; # restore |
---|
11884 | if ($eval_stat ne '') { chomp($eval_stat); die "do_ascii: $eval_stat\n" } |
---|
11885 | ($any_decoded && !$any_errors) ? 1 : $any_errors ? 2 : 0; |
---|
11886 | } |
---|
11887 | |
---|
11888 | # use Archive-Zip |
---|
11889 | sub do_unzip($$) { |
---|
11890 | my($part, $tempdir) = @_; |
---|
11891 | |
---|
11892 | ll(4) && do_log(4, "Unzipping " . $part->base_name); |
---|
11893 | snmp_count('OpsDecByArZipAttempt'); |
---|
11894 | my($zip) = Archive::Zip->new; |
---|
11895 | my(@err_nm) = qw(AZ_OK AZ_STREAM_END AZ_ERROR AZ_FORMAT_ERROR AZ_IO_ERROR); |
---|
11896 | |
---|
11897 | # Need to set up a temporary minimal error handler |
---|
11898 | # because we now test inside do_unzip whether the $part |
---|
11899 | # in question is a zip archive |
---|
11900 | Archive::Zip::setErrorHandler(sub { return 5 }); |
---|
11901 | my($sts) = $zip->read($part->full_name); |
---|
11902 | Archive::Zip::setErrorHandler(sub { die @_ }); |
---|
11903 | if ($sts != AZ_OK) { |
---|
11904 | do_log(4, "do_unzip: not a zip: $err_nm[$sts] ($sts)"); |
---|
11905 | return 0; |
---|
11906 | } |
---|
11907 | my($any_unsupp_compmeth,$any_zero_length); |
---|
11908 | my($encryptedcount,$extractedcount) = (0,0); |
---|
11909 | my($item_num) = 0; my($parent_placement) = $part->mime_placement; |
---|
11910 | for my $mem ($zip->members()) { |
---|
11911 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
11912 | $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
11913 | $newpart_obj->name_declared($mem->fileName); |
---|
11914 | my($compmeth) = $mem->compressionMethod; |
---|
11915 | if ($compmeth != COMPRESSION_DEFLATED && $compmeth != COMPRESSION_STORED) { |
---|
11916 | $any_unsupp_compmeth = $compmeth; |
---|
11917 | $newpart_obj->attributes_add('U'); |
---|
11918 | } elsif ($mem->isEncrypted) { |
---|
11919 | $encryptedcount++; |
---|
11920 | $newpart_obj->attributes_add('U','C'); |
---|
11921 | } elsif ($mem->isDirectory) { |
---|
11922 | $newpart_obj->attributes_add('D'); |
---|
11923 | } else { |
---|
11924 | # want to read uncompressed - set to COMPRESSION_STORED |
---|
11925 | my($oldc) = $mem->desiredCompressionMethod(COMPRESSION_STORED); |
---|
11926 | $sts = $mem->rewindData(); |
---|
11927 | $sts == AZ_OK or die sprintf("%s: error rew. member data: %s (%s)", |
---|
11928 | $part->base_name, $err_nm[$sts], $sts); |
---|
11929 | my($newpart) = $newpart_obj->full_name; |
---|
11930 | my($outpart) = IO::File->new; |
---|
11931 | $outpart->open($newpart,'>') or die "Can't create file $newpart: $!"; |
---|
11932 | binmode($outpart) or die "Can't set file $newpart to binmode: $!"; |
---|
11933 | my($size) = 0; |
---|
11934 | while ($sts == AZ_OK) { |
---|
11935 | my($buf_ref); |
---|
11936 | ($buf_ref, $sts) = $mem->readChunk(); |
---|
11937 | $sts == AZ_OK || $sts == AZ_STREAM_END |
---|
11938 | or die sprintf("%s: error reading member: %s (%s)", |
---|
11939 | $part->base_name, $err_nm[$sts], $sts); |
---|
11940 | my($buf_len) = length($$buf_ref); |
---|
11941 | if ($buf_len > 0) { |
---|
11942 | $size += $buf_len; |
---|
11943 | $outpart->print($$buf_ref) or die "Can't write to $newpart: $!"; |
---|
11944 | consumed_bytes($buf_len, 'do_unzip'); |
---|
11945 | } |
---|
11946 | } |
---|
11947 | $any_zero_length = 1 if $size == 0; |
---|
11948 | $newpart_obj->size($size); |
---|
11949 | $outpart->close or die "Can't close $newpart: $!"; |
---|
11950 | $mem->desiredCompressionMethod($oldc); |
---|
11951 | $mem->endRead(); |
---|
11952 | $extractedcount++; |
---|
11953 | } |
---|
11954 | } |
---|
11955 | snmp_count('OpsDecByArZip'); |
---|
11956 | my($retval) = 1; |
---|
11957 | if ($any_unsupp_compmeth) { |
---|
11958 | $retval = 2; |
---|
11959 | do_log(-1, sprintf("do_unzip: %s, unsupported compr. method: %s", |
---|
11960 | $part->base_name, $any_unsupp_compmeth)); |
---|
11961 | } elsif ($any_zero_length) { # possible zip vulnerability exploit |
---|
11962 | $retval = 2; |
---|
11963 | do_log(1, sprintf("do_unzip: %s, zero length members, archive retained", |
---|
11964 | $part->base_name)); |
---|
11965 | } elsif ($encryptedcount) { |
---|
11966 | $retval = 2; |
---|
11967 | do_log(1, sprintf( |
---|
11968 | "do_unzip: %s, %d members are encrypted, %s extracted, archive retained", |
---|
11969 | $part->base_name, $encryptedcount, |
---|
11970 | !$extractedcount ? 'none' : $extractedcount)); |
---|
11971 | } |
---|
11972 | $retval; |
---|
11973 | } |
---|
11974 | |
---|
11975 | # use external decompressor program from the gzip/bzip2/compress family |
---|
11976 | # (there *is* a perl module for bzip2, but is not ready for prime time) |
---|
11977 | sub do_uncompress($$$) { |
---|
11978 | my($part, $tempdir, $decompressor) = @_; |
---|
11979 | ll(4) && do_log(4,sprintf("do_uncompress %s by %s", |
---|
11980 | $part->base_name,$decompressor)); |
---|
11981 | my($decompressor_name) = basename((split(' ',$decompressor))[0]); |
---|
11982 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
11983 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
11984 | $newpart_obj->mime_placement($part->mime_placement."/1"); |
---|
11985 | my($newpart) = $newpart_obj->full_name; |
---|
11986 | my($type_short, $name_declared) = ($part->type_short, $part->name_declared); |
---|
11987 | my(@rn); # collect recommended file names |
---|
11988 | push(@rn,$1) |
---|
11989 | if $part->type_long =~ /^\S+\s+compressed data, was "(.+)"(\z|, from\b)/; |
---|
11990 | for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) { |
---|
11991 | next if $name_d eq ''; |
---|
11992 | my($name) = $name_d; |
---|
11993 | for (!ref $type_short ? ($type_short) : @$type_short) { |
---|
11994 | /^F\z/ and $name=~s/\.F\z//; |
---|
11995 | /^Z\z/ and $name=~s/\.Z\z// || $name=~s/\.tg?z\z/.tar/; |
---|
11996 | /^gz\z/ and $name=~s/\.gz\z// || $name=~s/\.tgz\z/.tar/; |
---|
11997 | /^bz\z/ and $name=~s/\.bz\z// || $name=~s/\.tbz\z/.tar/; |
---|
11998 | /^bz2\z/ and $name=~s/\.bz2?\z// || $name=~s/\.tbz\z/.tar/; |
---|
11999 | /^lzo\z/ and $name=~s/\.lzo\z//; |
---|
12000 | /^rpm\z/ and $name=~s/\.rpm\z/.cpio/; |
---|
12001 | } |
---|
12002 | push(@rn,$name) if !grep { $_ eq $name } @rn; |
---|
12003 | } |
---|
12004 | $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn; |
---|
12005 | my($proc_fh,$pid) = |
---|
12006 | run_command($part->full_name, undef, split(' ',$decompressor)); |
---|
12007 | my($rv,$rerr) = run_command_copy($newpart,$proc_fh); |
---|
12008 | if ($rv) { |
---|
12009 | # unlink($newpart) or die "Can't unlink $newpart: $!"; |
---|
12010 | die sprintf('Error running decompressor %s on %s, %s', |
---|
12011 | $decompressor, $part->base_name, exit_status_str($rv,$rerr)); |
---|
12012 | } |
---|
12013 | 1; |
---|
12014 | } |
---|
12015 | |
---|
12016 | # use Zlib to inflate |
---|
12017 | sub do_gunzip($$) { |
---|
12018 | my($part, $tempdir) = @_; |
---|
12019 | do_log(4, "Inflating gzip archive " . $part->base_name); |
---|
12020 | snmp_count('OpsDecByZlib'); |
---|
12021 | my($gz) = gzopen($part->full_name, "rb") |
---|
12022 | or die sprintf("do_gunzip: Error opening %s: %s", |
---|
12023 | $part->full_name, $gzerrno); |
---|
12024 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
12025 | $newpart_obj->mime_placement($part->mime_placement."/1"); |
---|
12026 | my($newpart) = $newpart_obj->full_name; |
---|
12027 | my($outpart) = IO::File->new; |
---|
12028 | $outpart->open($newpart,'>') or die "Can't create file $newpart: $!"; |
---|
12029 | binmode($outpart) or die "Can't set file $newpart to binmode: $!"; |
---|
12030 | my($buffer); my($size) = 0; |
---|
12031 | while ($gz->gzread($buffer) > 0) { |
---|
12032 | $outpart->print($buffer) or die "Can't write to $newpart: $!"; |
---|
12033 | $size += length($buffer); |
---|
12034 | consumed_bytes(length($buffer), 'do_gunzip'); |
---|
12035 | } |
---|
12036 | $newpart_obj->size($size); |
---|
12037 | $outpart->close or die "Can't close $newpart: $!"; |
---|
12038 | my(@rn); # collect recommended file name |
---|
12039 | my($name_declared) = $part->name_declared; |
---|
12040 | for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) { |
---|
12041 | next if $name_d eq ''; |
---|
12042 | my($name) = $name_d; |
---|
12043 | $name=~s/\.(gz|Z)\z// || $name=~s/\.tgz\z/.tar/; |
---|
12044 | push(@rn,$name) if !grep { $_ eq $name } @rn; |
---|
12045 | } |
---|
12046 | $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn) if @rn; |
---|
12047 | if ($gzerrno != Z_STREAM_END) { |
---|
12048 | do_log(-1,sprintf("do_gunzip: Error reading %s: %s", |
---|
12049 | $part->full_name, $gzerrno)); |
---|
12050 | unlink($newpart) or die "Can't unlink $newpart: $!"; |
---|
12051 | $newpart_obj->size(undef); |
---|
12052 | $gz->gzclose(); |
---|
12053 | return 0; |
---|
12054 | } |
---|
12055 | $gz->gzclose(); |
---|
12056 | 1; |
---|
12057 | } |
---|
12058 | |
---|
12059 | # untar any tar archives with Archive-Tar, extract each file individually |
---|
12060 | sub do_tar($$) { |
---|
12061 | my($part, $tempdir) = @_; |
---|
12062 | snmp_count('OpsDecByArTar'); |
---|
12063 | # Work around bug in Archive-Tar |
---|
12064 | my $tar = eval { Archive::Tar->new($part->full_name) }; |
---|
12065 | if (!defined($tar)) { |
---|
12066 | chomp($@); |
---|
12067 | do_log(4, sprintf("Faulty archive %s: %s", $part->full_name, $@)); |
---|
12068 | return 0; |
---|
12069 | } |
---|
12070 | do_log(4,"Untarring ".$part->base_name); |
---|
12071 | my($item_num) = 0; my($parent_placement) = $part->mime_placement; |
---|
12072 | my(@list) = $tar->list_files(); |
---|
12073 | for (@list) { |
---|
12074 | next if /\/\z/; # ignore directories |
---|
12075 | # this is bad (reads whole file into scalar) |
---|
12076 | # need some error handling, too |
---|
12077 | my $data = $tar->get_content($_); |
---|
12078 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
12079 | $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
12080 | my($newpart) = $newpart_obj->full_name; |
---|
12081 | my($outpart) = IO::File->new; |
---|
12082 | $outpart->open($newpart,'>') or die "Can't create file $newpart: $!"; |
---|
12083 | binmode($outpart) or die "Can't set file $newpart to binmode: $!"; |
---|
12084 | $outpart->print($data) or die "Can't write to $newpart: $!"; |
---|
12085 | $newpart_obj->size(length($data)); |
---|
12086 | consumed_bytes(length($data), 'do_tar'); |
---|
12087 | $outpart->close or die "Can't close $newpart: $!"; |
---|
12088 | } |
---|
12089 | 1; |
---|
12090 | } |
---|
12091 | |
---|
12092 | # use external program to expand RAR archives |
---|
12093 | sub do_unrar($$) { |
---|
12094 | my($part, $tempdir) = @_; |
---|
12095 | ll(4) && do_log(4, "Attempting to expand RAR archive " . $part->base_name); |
---|
12096 | my($decompressor_name) = basename((split(' ',$unrar))[0]); |
---|
12097 | snmp_count("OpsDecBy\u${decompressor_name}Attempt"); |
---|
12098 | my(@common_rar_switches) = qw(-c- -p- -av- -idp); |
---|
12099 | my($err, $retval, $rv1); |
---|
12100 | # unrar exit codes: SUCCESS=0, WARNING=1, FATAL_ERROR=2, CRC_ERROR=3, |
---|
12101 | # LOCK_ERROR=4, WRITE_ERROR=5, OPEN_ERROR=6, USER_ERROR=7, MEMORY_ERROR=8, |
---|
12102 | # CREATE_ERROR=9, USER_BREAK=255 |
---|
12103 | # Check whether we can really unrar it |
---|
12104 | $rv1 = |
---|
12105 | system($unrar, 't', '-inul', @common_rar_switches, '--', $part->full_name); |
---|
12106 | $err = $!; $retval = retcode($rv1); |
---|
12107 | if ($retval == 7) { # USER_ERROR |
---|
12108 | do_log(-1,"do_unrar: $unrar does not recognize all switches, " |
---|
12109 | . "it is probably too old. Retrying without '-av- -idp'. " |
---|
12110 | . "Upgrade: http://www.rarlab.com/"); |
---|
12111 | @common_rar_switches = qw(-c- -p-); # retry without new switches |
---|
12112 | $rv1 = system($unrar, 't', '-inul', @common_rar_switches, '--', |
---|
12113 | $part->full_name); |
---|
12114 | $err = $!; $retval = retcode($rv1); |
---|
12115 | } |
---|
12116 | if (!grep { $_ == $retval } (0,1,3)) { |
---|
12117 | # not one of: SUCCESS, WARNING, CRC_ERROR |
---|
12118 | # NOTE: password protected files in the archive cause CRC_ERROR |
---|
12119 | do_log(4,sprintf("unrar 't' %s, command: %s", |
---|
12120 | exit_status_str($rv1,$err), $unrar)); |
---|
12121 | return 0; |
---|
12122 | } |
---|
12123 | |
---|
12124 | # We have to jump hoops because there is no simple way to |
---|
12125 | # just list all the files |
---|
12126 | ll(4) && do_log(4, "Expanding RAR archive " . $part->base_name); |
---|
12127 | |
---|
12128 | my(@list); my($hypcount) = 0; my($encryptedcount) = 0; |
---|
12129 | my($lcnt) = 0; my($member_name); my($bytes) = 0; my($last_line); |
---|
12130 | my($item_num) = 0; my($parent_placement) = $part->mime_placement; |
---|
12131 | my($proc_fh,$pid) = |
---|
12132 | run_command(undef, "&1", $unrar, 'v', @common_rar_switches, '--', |
---|
12133 | $part->full_name); |
---|
12134 | while (defined($_ = $proc_fh->getline)) { |
---|
12135 | $last_line = $_ if !/^\s*$/; # keep last nonempty line |
---|
12136 | chomp; |
---|
12137 | if (/^unexpected end of archive/) { |
---|
12138 | last; |
---|
12139 | } elsif (/^------/) { |
---|
12140 | $hypcount++; |
---|
12141 | last if $hypcount >= 2; |
---|
12142 | } elsif ($hypcount < 1 && /^Encrypted file:/) { |
---|
12143 | do_log(4,"do_unrar: ".$_); |
---|
12144 | $part->attributes_add('U','C'); |
---|
12145 | } elsif ($hypcount == 1) { |
---|
12146 | $lcnt++; |
---|
12147 | if ($lcnt % 2 == 0) { # information line (every other line) |
---|
12148 | if (!/^\s+(\d+)\s+(\d+)\s+(\d+%|-->|<--)/) { |
---|
12149 | do_log(-1,"do_unrar: can't parse info line for \"$member_name\" $_"); |
---|
12150 | } elsif (defined $member_name) { |
---|
12151 | do_log(5,"do_unrar: member: \"$member_name\", size: $1"); |
---|
12152 | if ($1 > 0) { $bytes += $1; push(@list, $member_name) } |
---|
12153 | } |
---|
12154 | $member_name = undef; |
---|
12155 | } elsif (/^(.)(.*)\z/s) { |
---|
12156 | $member_name = $2; # all but the first character (space or an asterisk) |
---|
12157 | if ($1 eq '*') { # member is encrypted |
---|
12158 | $encryptedcount++; $item_num++; |
---|
12159 | # make a phantom entry - carrying only name and attributes |
---|
12160 | my($newpart_obj)=Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
12161 | $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
12162 | $newpart_obj->name_declared($member_name); |
---|
12163 | $newpart_obj->attributes_add('U','C'); |
---|
12164 | $member_name = undef; # makes no sense extracting encrypted files |
---|
12165 | } |
---|
12166 | } |
---|
12167 | } |
---|
12168 | } |
---|
12169 | # consume all remaining output to avoid broken pipe |
---|
12170 | while (defined($_ = $proc_fh->getline)) { $last_line = $_ if !/^\s*$/ } |
---|
12171 | $err = undef; $proc_fh->close or $err = $!; $retval = retcode($?); |
---|
12172 | if ($retval == 3) { # CRC_ERROR |
---|
12173 | do_log(4,"do_unrar: CRC_ERROR - undecipherable"); |
---|
12174 | $part->attributes_add('U'); |
---|
12175 | } |
---|
12176 | my($fn) = $part->full_name; |
---|
12177 | if (!$bytes && $retval==0 && $last_line =~ /^\Q$fn\E is not RAR archive$/) { |
---|
12178 | do_log(4,"do_unrar: ".$last_line); |
---|
12179 | return 0; |
---|
12180 | } elsif ($last_line !~ /^\s*(\d+)\s+(\d+)/s) { |
---|
12181 | do_log(4,"do_unrar: unable to obtain orig total size: $last_line"); |
---|
12182 | } else { |
---|
12183 | do_log(4,"do_unrar: summary size: $2, sum of sizes: $bytes") |
---|
12184 | if abs($bytes - $2) > 100; |
---|
12185 | $bytes = $2 if $2 > $bytes; |
---|
12186 | } |
---|
12187 | consumed_bytes($bytes, 'do_unrar-pre', 1); # pre-check on estimated size |
---|
12188 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
12189 | if ($retval==0) {} # SUCCESS |
---|
12190 | elsif ($retval==1 && @list && $bytes > 0) {} # WARNING, probably still ok |
---|
12191 | else { # WARNING and suspicious, or really bad |
---|
12192 | die ("unrar: can't get a list of archive members: " . |
---|
12193 | exit_status_str($?,$err) ."; ".$last_line); |
---|
12194 | } |
---|
12195 | if (!@list) { |
---|
12196 | do_log(4,"do_unrar: no archive members, or not an archive at all"); |
---|
12197 | #***return 0 if $exec; |
---|
12198 | } else { |
---|
12199 | # my $rv = store_mgr($tempdir, $part, \@list, $unrar, |
---|
12200 | # qw(p -inul -kb), @common_rar_switches, '--', |
---|
12201 | # $part->full_name); |
---|
12202 | my($proc_fh,$pid) = |
---|
12203 | run_command(undef, "&1", $unrar, qw(x -inul -ver -o- -kb), |
---|
12204 | @common_rar_switches, '--', |
---|
12205 | $part->full_name, "$tempdir/parts/rar/"); |
---|
12206 | my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ } |
---|
12207 | my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); |
---|
12208 | if (!grep { $_ == $retval } (0,1,3)) { # not one of: SUCCESS, WARNING, CRC |
---|
12209 | do_log(-1, 'unrar '.exit_status_str($?,$err)); |
---|
12210 | } |
---|
12211 | my($errn) = lstat("$tempdir/parts/rar") ? 0 : 0+$!; |
---|
12212 | if ($errn != ENOENT) { |
---|
12213 | my($b) = flatten_and_tidy_dir("$tempdir/parts/rar","$tempdir/parts",$part); |
---|
12214 | consumed_bytes($b, 'do_unrar'); |
---|
12215 | } |
---|
12216 | } |
---|
12217 | if ($encryptedcount) { |
---|
12218 | do_log(1, sprintf( |
---|
12219 | "do_unrar: %s, %d members are encrypted, %s extracted, archive retained", |
---|
12220 | $part->base_name, $encryptedcount, !@list ? 'none' : 0+@list )); |
---|
12221 | return 2; |
---|
12222 | } |
---|
12223 | 1; |
---|
12224 | } |
---|
12225 | |
---|
12226 | # use external program to expand LHA archives |
---|
12227 | sub do_lha($$) { |
---|
12228 | my($part, $tempdir) = @_; |
---|
12229 | ll(4) && do_log(4, "Attempting to expand LHA archive " . $part->base_name); |
---|
12230 | my($decompressor_name) = basename((split(' ',$lha))[0]); |
---|
12231 | snmp_count("OpsDecBy\u${decompressor_name}Attempt"); |
---|
12232 | # lha needs extension .exe to understand SFX! |
---|
12233 | symlink($part->full_name, $part->full_name.".exe") |
---|
12234 | or die sprintf("Can't symlink %s %s.exe: %s", |
---|
12235 | $part->full_name, $part->full_name, $!); |
---|
12236 | # Check whether we can really lha it |
---|
12237 | my($checkerr); my($retval) = 1; |
---|
12238 | my($proc_fh,$pid) = run_command(undef, "&1", $lha, 'lq', |
---|
12239 | $part->full_name.".exe"); |
---|
12240 | while (defined($_ = $proc_fh->getline)) { |
---|
12241 | $checkerr = 1 if /Checksum error/i; |
---|
12242 | } |
---|
12243 | my($err); $proc_fh->close or $err = $!; |
---|
12244 | if ($? || $checkerr) { |
---|
12245 | $retval = 0; # consider atomic |
---|
12246 | do_log(4, "do_lha: not a LHA archive($checkerr) ? ". |
---|
12247 | exit_status_str($?,$err)); |
---|
12248 | } else { |
---|
12249 | do_log(4, "Expanding LHA archive " . $part->base_name . ".exe"); |
---|
12250 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
12251 | ($proc_fh,$pid) = |
---|
12252 | run_command(undef, undef, $lha, 'lq', $part->full_name.".exe"); |
---|
12253 | my(@list); |
---|
12254 | while (defined($_ = $proc_fh->getline)) { |
---|
12255 | chomp; |
---|
12256 | next if /\/\z/; # ignore directories |
---|
12257 | push(@list, (split(/\s+/))[-1]); #***??? split on whitespace ??? |
---|
12258 | } |
---|
12259 | $err=undef; $proc_fh->close or $err = $!; |
---|
12260 | $?==0 or do_log(-1, 'do_lha: '.exit_status_str($?,$err)); |
---|
12261 | if (!@list) { |
---|
12262 | do_log(4, "do_lha: no archive members, or not an archive at all"); |
---|
12263 | #*** $retval = 0 if $exec; |
---|
12264 | } else { |
---|
12265 | my $rv = store_mgr($tempdir, $part, \@list, $lha, 'pq', |
---|
12266 | $part->full_name.".exe"); |
---|
12267 | do_log(-1, 'do_lha '.exit_status_str($rv)) if $rv; |
---|
12268 | $retval = 1; # consider decoded |
---|
12269 | } |
---|
12270 | } |
---|
12271 | unlink($part->full_name.".exe") |
---|
12272 | or die "Can't unlink " . $part->full_name . ".exe: $!"; |
---|
12273 | $retval; |
---|
12274 | } |
---|
12275 | |
---|
12276 | # use external program to expand ARC archives; |
---|
12277 | # works with original arc, or a GPL licensed 'nomarch' |
---|
12278 | # (http://rus.members.beeb.net/nomarch.html) |
---|
12279 | sub do_arc($$) { |
---|
12280 | my($part, $tempdir) = @_; |
---|
12281 | my($decompressor_name) = basename((split(' ',$arc))[0]); |
---|
12282 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
12283 | my($is_nomarch) = $arc =~ /nomarch/i; |
---|
12284 | ll(4) && do_log(4,sprintf("Unarcing %s, using %s", |
---|
12285 | $part->base_name, ($is_nomarch ? "nomarch" : "arc") )); |
---|
12286 | my($cmdargs) = ($is_nomarch ? "-l -U" : "ln") . " " . $part->full_name; |
---|
12287 | my($proc_fh,$pid) = |
---|
12288 | run_command(undef, '/dev/null', $arc, split(' ',$cmdargs)); |
---|
12289 | my(@list) = $proc_fh->getlines; |
---|
12290 | my($err); $proc_fh->close or $err = $!; |
---|
12291 | $?==0 or do_log(-1, 'do_arc: '.exit_status_str($?,$err)); |
---|
12292 | |
---|
12293 | #*** no spaces in filenames allowed??? |
---|
12294 | map { s/^([^ \t\r\n]*).*\z/$1/s } @list; # keep only filenames |
---|
12295 | if (@list) { |
---|
12296 | my $rv = store_mgr($tempdir, $part, \@list, $arc, |
---|
12297 | ($is_nomarch ? ('-p', '-U') : 'p'), $part->full_name); |
---|
12298 | do_log(-1, 'arc '.exit_status_str($rv)) if $rv; |
---|
12299 | } |
---|
12300 | 1; |
---|
12301 | } |
---|
12302 | |
---|
12303 | # use external program to expand ZOO archives |
---|
12304 | sub do_zoo($$) { |
---|
12305 | my($part, $tempdir) = @_; |
---|
12306 | do_log(4, "Expanding ZOO archive " . $part->full_name); |
---|
12307 | my($decompressor_name) = basename((split(' ',$zoo))[0]); |
---|
12308 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
12309 | # Zoo needs extension of .zoo! |
---|
12310 | symlink($part->full_name, $part->full_name.".zoo") |
---|
12311 | or die sprintf("Can't symlink %s %s.zoo: %s", |
---|
12312 | $part->full_name, $part->full_name, $!); |
---|
12313 | my($proc_fh,$pid) = |
---|
12314 | run_command(undef, undef, $zoo, 'lf1q', $part->full_name.".zoo"); |
---|
12315 | my(@list) = $proc_fh->getlines; |
---|
12316 | my($err); $proc_fh->close or $err = $!; |
---|
12317 | $?==0 or do_log(-1, 'do_zoo: '.exit_status_str($?,$err)); |
---|
12318 | if (@list) { |
---|
12319 | chomp(@list); |
---|
12320 | my $rv = store_mgr($tempdir, $part, \@list, $zoo, 'xpqqq:', |
---|
12321 | $part->full_name . ".zoo"); |
---|
12322 | do_log(-1, 'zoo '.exit_status_str($rv)) if $rv; |
---|
12323 | } |
---|
12324 | unlink($part->full_name.".zoo") |
---|
12325 | or die "Can't unlink " . $part->full_name . ".zoo: $!"; |
---|
12326 | 1; |
---|
12327 | } |
---|
12328 | |
---|
12329 | # use external program to expand ARJ archives |
---|
12330 | sub do_unarj($$) { |
---|
12331 | my($part, $tempdir) = @_; |
---|
12332 | do_log(4, "Expanding ARJ archive " . $part->base_name); |
---|
12333 | my($decompressor_name) = basename((split(' ',$unrar))[0]); |
---|
12334 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
12335 | # options to arj, ignored by unarj |
---|
12336 | # provide some password in -g to turn fatal error into 'bad password' error |
---|
12337 | $ENV{ARJ_SW} = "-i -jo -b5 -2h -jyc -ja1 -gsecret -w$TEMPBASE"; |
---|
12338 | # unarj needs extension of .arj! |
---|
12339 | symlink($part->full_name, $part->full_name.".arj") |
---|
12340 | or die sprintf("Can't symlink %s %s.arj: %s", |
---|
12341 | $part->full_name, $part->full_name, $!); |
---|
12342 | # obtain total original size of archive members from the index/listing |
---|
12343 | my($proc_fh,$pid) = |
---|
12344 | run_command(undef,'/dev/null', $unarj, 'l', $part->full_name.".arj"); |
---|
12345 | my($last_line); |
---|
12346 | while (defined($_ = $proc_fh->getline)) { $last_line = $_ if !/^\s*$/ } |
---|
12347 | my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); |
---|
12348 | if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err |
---|
12349 | die ("unarj: can't get a list of archive members: ". |
---|
12350 | exit_status_str($?,$err)); |
---|
12351 | } |
---|
12352 | if ($last_line !~ /^\s*(\d+)\s*files\s*(\d+)/s) { |
---|
12353 | do_log(-1,"do_unarj: WARN: unable to obtain orig size of files: $last_line"); |
---|
12354 | } else { |
---|
12355 | consumed_bytes($2, 'do_unarj-pre', 1); # pre-check on estimated size |
---|
12356 | } |
---|
12357 | # unarj has very limited extraction options, arj is much better! |
---|
12358 | mkdir("$tempdir/parts/arj", 0750) or die "Can't mkdir $tempdir/parts/arj: $!"; |
---|
12359 | chdir("$tempdir/parts/arj") or die "Can't chdir to $tempdir/parts/arj: $!"; |
---|
12360 | ($proc_fh,$pid) = |
---|
12361 | run_command(undef, "&1", $unarj, 'e', $part->full_name.".arj"); |
---|
12362 | my($encryptedcount,$skippedcount) = (0,0); |
---|
12363 | while (defined($_ = $proc_fh->getline)) { |
---|
12364 | $encryptedcount++ |
---|
12365 | if /^(Extracting.*\bBad file data or bad password|File is password encrypted, Skipped)\b/s; |
---|
12366 | $skippedcount++ |
---|
12367 | if /(\bexists|^File is password encrypted|^Unsupported .*), Skipped\b/s; |
---|
12368 | } |
---|
12369 | $err = undef; $proc_fh->close or $err = $!; $retval = retcode($?); |
---|
12370 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
12371 | if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err |
---|
12372 | do_log(0, "unarj: error extracting: ".exit_status_str($?,$err)); |
---|
12373 | } |
---|
12374 | # add attributes to the parent object, because we didn't remember names |
---|
12375 | # of its scrambled members |
---|
12376 | $part->attributes_add('U') if $skippedcount; |
---|
12377 | $part->attributes_add('C') if $encryptedcount; |
---|
12378 | my($errn) = lstat("$tempdir/parts/arj") ? 0 : 0+$!; |
---|
12379 | if ($errn != ENOENT) { |
---|
12380 | my($b) = flatten_and_tidy_dir("$tempdir/parts/arj","$tempdir/parts",$part); |
---|
12381 | consumed_bytes($b, 'do_unarj'); |
---|
12382 | snmp_count("OpsDecBy\u${decompressor_name}"); |
---|
12383 | } |
---|
12384 | unlink($part->full_name.".arj") |
---|
12385 | or die "Can't unlink " . $part->full_name . ".arj: $!"; |
---|
12386 | if (!grep { $_ == $retval } (0,1,3)) { # not one of: success, warn, CRC err |
---|
12387 | die ("unarj: can't extract archive members: ".exit_status_str($?,$err)); |
---|
12388 | } |
---|
12389 | if ($encryptedcount || $skippedcount) { |
---|
12390 | do_log(1, sprintf( |
---|
12391 | "do_unarj: %s, %d members are encrypted, %d skipped, archive retained", |
---|
12392 | $part->base_name, $encryptedcount, $skippedcount)); |
---|
12393 | return 2; |
---|
12394 | } |
---|
12395 | 1; |
---|
12396 | } |
---|
12397 | |
---|
12398 | # use Convert-TNEF |
---|
12399 | sub do_tnef($$) { |
---|
12400 | my($part, $tempdir) = @_; |
---|
12401 | do_log(4, "Extracting TNEF attachment " . $part->base_name); |
---|
12402 | snmp_count('OpsDecByTnef'); |
---|
12403 | chdir("$tempdir/parts") or die "Can't chdir to $tempdir/parts: $!"; |
---|
12404 | my $tnef = |
---|
12405 | Convert::TNEF->read_in($part->full_name, {ignore_checksum => "true"}); |
---|
12406 | if (!$tnef) { |
---|
12407 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
12408 | return 0; # Not TNEF - treat as atomic |
---|
12409 | } |
---|
12410 | my($item_num) = 0; my($parent_placement) = $part->mime_placement; |
---|
12411 | for my $a ($tnef->message, $tnef->attachments) { |
---|
12412 | if (my $dh = $a->datahandle) { |
---|
12413 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part); |
---|
12414 | $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
12415 | $newpart_obj->name_declared([$a->name, $a->longname]); |
---|
12416 | $newpart_obj->size($a->size); |
---|
12417 | consumed_bytes($a->size, 'do_tnef'); |
---|
12418 | my($newpart) = $newpart_obj->full_name; |
---|
12419 | my($outpart) = IO::File->new; |
---|
12420 | $outpart->open($newpart,'>') or die "Can't create file $newpart: $!"; |
---|
12421 | binmode($outpart) or die "Can't set file $newpart to binmode: $!"; |
---|
12422 | if (defined(my $file = $dh->path)) { |
---|
12423 | copy($file, $outpart); |
---|
12424 | } else { |
---|
12425 | my($s) = $dh->as_string; |
---|
12426 | $outpart->print($s) or die "Can't write to $newpart: $!"; |
---|
12427 | # consumed_bytes(length($s), 'do_tnef'); |
---|
12428 | } |
---|
12429 | $outpart->close or die "Can't close $newpart: $!"; |
---|
12430 | } |
---|
12431 | } |
---|
12432 | $tnef->purge; |
---|
12433 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
12434 | 1; |
---|
12435 | } |
---|
12436 | |
---|
12437 | # The pax and cpio utilities usually support the following archive formats: |
---|
12438 | # cpio, bcpio, sv4cpio, sv4crc, tar (old tar), ustar (POSIX.2 tar). |
---|
12439 | # The utilities from http://heirloom.sourceforge.net/ support |
---|
12440 | # several other tar/cpio variants such as SCO, Sun, DEC, Cray, SGI |
---|
12441 | sub do_pax_cpio($$$) { |
---|
12442 | my($part, $tempdir, $archiver) = @_; |
---|
12443 | my($archiver_name) = basename((split(' ',$archiver))[0]); |
---|
12444 | snmp_count("OpsDecBy\u${archiver_name}"); |
---|
12445 | ll(4) && do_log(4,sprintf("Expanding archive %s, using %s", |
---|
12446 | $part->base_name, $archiver_name)); |
---|
12447 | my($is_pax) = $archiver_name =~ /^cpio/i ? 0 : 1; |
---|
12448 | do_log(-1,"WARN: Using $archiver_name instead of pax can be a security ". |
---|
12449 | "risk; please add: \$pax='pax'; to amavisd.conf and check that ". |
---|
12450 | "the pax(1) utility is available on the system!") if !$is_pax; |
---|
12451 | my(@cmdargs) = $is_pax ? qw(-v) : qw(-i -t -v); |
---|
12452 | my($proc_fh,$pid) = run_command($part->full_name, undef, $archiver,@cmdargs); |
---|
12453 | my($bytes) = 0; local($1,$2,$3); |
---|
12454 | while (defined($_ = $proc_fh->getline)) { |
---|
12455 | chomp; |
---|
12456 | next if /^\d+ blocks\z/; |
---|
12457 | last if /^(cpio|pax): (.*bytes read|End of archive volume)/; |
---|
12458 | if (!/^ (?: \S+\s+ ){4} |
---|
12459 | (\d+) \s+ |
---|
12460 | ( (?: \s* \S+ ){3} (?: \s+ \d{4}, )? ) \s+ |
---|
12461 | (.+) \z/xs) { |
---|
12462 | do_log(-1,"do_pax_cpio: can't parse toc line: $_"); |
---|
12463 | } else { |
---|
12464 | my($mem,$size) = ($3,$1); |
---|
12465 | $mem = $1 if $is_pax && $mem =~ /^(.*) =[=>] (.*)\z/; # hard or soft link |
---|
12466 | do_log(5,"do_pax_cpio: member: \"$mem\", size: $size"); |
---|
12467 | $bytes += $size if $size > 0; |
---|
12468 | } |
---|
12469 | } |
---|
12470 | # consume remaining output to avoid broken pipe |
---|
12471 | while (defined($proc_fh->getline)) { } |
---|
12472 | my($err); $proc_fh->close or $err = $!; |
---|
12473 | $?==0 or do_log(-1, 'do_pax_cpio/1: '.exit_status_str($?,$err)); |
---|
12474 | consumed_bytes($bytes, 'do_pax_cpio/pre', 1); # pre-check on estimated size |
---|
12475 | mkdir("$tempdir/parts/arch", 0750) |
---|
12476 | or die "Can't mkdir $tempdir/parts/arch: $!"; |
---|
12477 | my($name_clash) = 0; |
---|
12478 | my(%orig_names); # maps filenames to archive member names when possible |
---|
12479 | eval { |
---|
12480 | chdir("$tempdir/parts/arch") |
---|
12481 | or die "Can't chdir to $tempdir/parts/arch: $!"; |
---|
12482 | my(@cmdargs) = $is_pax ? qw(-r -k -p am -s /[^A-Za-z0-9_]/-/gp) |
---|
12483 | : qw(-i -d --no-absolute-filenames --no-preserve-owner); |
---|
12484 | my($proc_fh,$pid) = run_command($part->full_name,"&1",$archiver,@cmdargs); |
---|
12485 | my($output) = ''; |
---|
12486 | while (defined($_ = $proc_fh->getline)) { |
---|
12487 | chomp; |
---|
12488 | if (!$is_pax || !/^(.*) >> (\S*)\z/) { $output .= $_."\n" } |
---|
12489 | else { # parse output from pax -s///p |
---|
12490 | my($member_name,$file_name) = ($1,$2); |
---|
12491 | if (!exists $orig_names{$file_name}) { |
---|
12492 | $orig_names{$file_name} = $member_name; |
---|
12493 | } else { |
---|
12494 | do_log(0,sprintf("do_pax_cpio: member \"%s\" is hidden by a ". |
---|
12495 | "previous archive member \"%s\", file: %s", |
---|
12496 | $member_name, $orig_names{$file_name}, $file_name)); |
---|
12497 | $orig_names{$file_name} = undef; # cause it to exist but undefined |
---|
12498 | $name_clash++; |
---|
12499 | } |
---|
12500 | } |
---|
12501 | } |
---|
12502 | chomp($output); my($err); $proc_fh->close or $err = $!; |
---|
12503 | $?==0 or die (exit_status_str($?,$err).' '.$output); |
---|
12504 | }; |
---|
12505 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
12506 | my($b) = flatten_and_tidy_dir("$tempdir/parts/arch", "$tempdir/parts", |
---|
12507 | $part, 0, \%orig_names); |
---|
12508 | consumed_bytes($b, 'do_pax_cpio'); |
---|
12509 | if ($@ ne '') { chomp($@); do_log(-1,"do_pax_cpio: $@"); 2 } |
---|
12510 | elsif ($name_clash) { 2 } else { 1 } |
---|
12511 | } |
---|
12512 | |
---|
12513 | # ar is a standard Unix binary archiver, also used by Debian packages |
---|
12514 | sub do_ar($$) { |
---|
12515 | my($part, $tempdir) = @_; |
---|
12516 | ll(4) && do_log(4,"Expanding Unix ar archive ".$part->full_name); |
---|
12517 | my($archiver_name) = basename((split(' ',$ar))[0]); |
---|
12518 | snmp_count("OpsDecBy\u${archiver_name}"); |
---|
12519 | my($proc_fh,$pid) = run_command(undef, undef, $ar, 'tv', $part->full_name); |
---|
12520 | my($bytes) = 0; local($1,$2,$3); |
---|
12521 | while (defined($_ = $proc_fh->getline)) { |
---|
12522 | chomp; |
---|
12523 | if (!/^(?:\S+\s+){2}(\d+)\s+((?:\S+\s+){3}\S+)\s+(.*)\z/) { |
---|
12524 | do_log(-1,"do_ar: can't parse contents listing line: $_"); |
---|
12525 | } else { |
---|
12526 | do_log(5,"do_ar: member: \"$3\", size: $1"); |
---|
12527 | $bytes += $1 if $1 > 0; |
---|
12528 | } |
---|
12529 | } |
---|
12530 | # consume remaining output to avoid broken pipe |
---|
12531 | while (defined($proc_fh->getline)) { } |
---|
12532 | my($err); $proc_fh->close or $err = $!; |
---|
12533 | $?==0 or do_log(-1, 'ar-1 '.exit_status_str($?,$err)); |
---|
12534 | |
---|
12535 | consumed_bytes($bytes, 'do_ar-pre', 1); # pre-check on estimated size |
---|
12536 | mkdir("$tempdir/parts/ar", 0750) |
---|
12537 | or die "Can't mkddir $tempdir/parts/ar: $!"; |
---|
12538 | chdir("$tempdir/parts/ar") or die "Can't chdir to $tempdir/parts/ar: $!"; |
---|
12539 | ($proc_fh,$pid) = run_command(undef, "&1", $ar, 'x', $part->full_name); |
---|
12540 | my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ } |
---|
12541 | $err = undef; $proc_fh->close or $err = $!; |
---|
12542 | $?==0 or do_log(-1, 'ar-2 '.exit_status_str($?,$err).' '.$output); |
---|
12543 | chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!"; |
---|
12544 | my($b) = flatten_and_tidy_dir("$tempdir/parts/ar","$tempdir/parts",$part); |
---|
12545 | consumed_bytes($b, 'do_ar'); |
---|
12546 | 1; |
---|
12547 | } |
---|
12548 | |
---|
12549 | sub do_cabextract($$) { |
---|
12550 | my($part, $tempdir) = @_; |
---|
12551 | do_log(4, "Expanding cab archive " . $part->base_name); |
---|
12552 | my($archiver_name) = basename((split(' ',$cabextract))[0]); |
---|
12553 | snmp_count("OpsDecBy\u${archiver_name}"); |
---|
12554 | my($bytes) = 0; |
---|
12555 | my($proc_fh,$pid) = |
---|
12556 | run_command(undef,undef,$cabextract,'-l',$part->full_name); |
---|
12557 | while (defined($_ = $proc_fh->getline)) { |
---|
12558 | chomp; |
---|
12559 | next if /^(File size|----|Viewing cabinet:|\z)/; |
---|
12560 | if (!/^\s* (\d+) \s* \| [^|]* \| \s (.*) \z/x) { |
---|
12561 | do_log(-1, "do_cabextract: can't parse toc line: $_"); |
---|
12562 | } else { |
---|
12563 | do_log(5, "do_cabextract: member: \"$2\", size: $1"); |
---|
12564 | $bytes += $1 if $1 > 0; |
---|
12565 | } |
---|
12566 | } |
---|
12567 | # consume remaining output to avoid broken pipe (just in case) |
---|
12568 | while (defined($proc_fh->getline)) { } |
---|
12569 | my($err); $proc_fh->close or $err = $!; |
---|
12570 | $?==0 or do_log(-1, 'cabextract-1 '.exit_status_str($?,$err)); |
---|
12571 | |
---|
12572 | consumed_bytes($bytes, 'do_cabextract-pre', 1); # pre-check on estimated size |
---|
12573 | mkdir("$tempdir/parts/cab", 0750) or die "Can't mkdir $tempdir/parts/cab: $!"; |
---|
12574 | ($proc_fh,$pid) = run_command(undef, '/dev/null', $cabextract, '-q', '-d', |
---|
12575 | "$tempdir/parts/cab", $part->full_name); |
---|
12576 | my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ } |
---|
12577 | $err = undef; $proc_fh->close or $err = $!; |
---|
12578 | $?==0 or do_log(-1, 'cabextract-2 '.exit_status_str($?,$err).' '.$output); |
---|
12579 | my($b) = flatten_and_tidy_dir("$tempdir/parts/cab", "$tempdir/parts", $part); |
---|
12580 | consumed_bytes($b, 'do_cabextract'); |
---|
12581 | 1; |
---|
12582 | } |
---|
12583 | |
---|
12584 | sub do_ole($$) { |
---|
12585 | my($part, $tempdir) = @_; |
---|
12586 | do_log(4,"Expanding MS OLE document " . $part->base_name); |
---|
12587 | my($archiver_name) = basename((split(' ',$ripole))[0]); |
---|
12588 | snmp_count("OpsDecBy\u${archiver_name}"); |
---|
12589 | mkdir("$tempdir/parts/ole",0750) or die "Can't mkdir $tempdir/parts/ole: $!"; |
---|
12590 | my($proc_fh,$pid) = run_command(undef, "&1", $ripole, '-v', |
---|
12591 | '-i', $part->full_name, '-d',"$tempdir/parts/ole"); |
---|
12592 | my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ } |
---|
12593 | my($err); $proc_fh->close or $err = $!; |
---|
12594 | $?==0 or do_log(0, 'ripOLE '.exit_status_str($?,$err).' '.$output); |
---|
12595 | my($b) = flatten_and_tidy_dir("$tempdir/parts/ole", "$tempdir/parts", $part); |
---|
12596 | if ($b > 0) { |
---|
12597 | do_log(4, "ripOLE extracted $b bytes from an OLE document"); |
---|
12598 | consumed_bytes($b, 'do_ole'); |
---|
12599 | } |
---|
12600 | 2; # always keep the original OLE document |
---|
12601 | } |
---|
12602 | |
---|
12603 | # Check for self-extracting archives. Note that we don't rely on |
---|
12604 | # file magic here since it's not reliable. Instead we will try each |
---|
12605 | # archiver. |
---|
12606 | sub do_executable($$) { |
---|
12607 | my($part, $tempdir) = @_; |
---|
12608 | |
---|
12609 | ll(4) && do_log(4,"Check whether ".$part->base_name. |
---|
12610 | " is a self-extracting archive"); |
---|
12611 | # ZIP? |
---|
12612 | return 2 if eval { do_unzip($part, $tempdir) }; |
---|
12613 | chomp($@); |
---|
12614 | do_log(-1,"do_executable/do_unzip failed, ignoring: $@") if $@ ne ''; |
---|
12615 | |
---|
12616 | # RAR? |
---|
12617 | return 2 if defined $unrar && eval { do_unrar($part, $tempdir) }; |
---|
12618 | chomp($@); |
---|
12619 | do_log(-1,"do_executable/do_unrar failed, ignoring: $@") if $@ ne ''; |
---|
12620 | |
---|
12621 | # LHA? |
---|
12622 | return 2 if defined $lha && eval { do_lha($part, $tempdir) }; |
---|
12623 | chomp($@); |
---|
12624 | do_log(-1,"do_executable/do_lha failed, ignoring: $@") if $@ ne ''; |
---|
12625 | |
---|
12626 | # # ARJ? |
---|
12627 | # return 2 if defined $unarj && eval { do_unarj($part, $tempdir) }; |
---|
12628 | # chomp($@); |
---|
12629 | # do_log(-1,"do_executable/do_unarj failed, ignoring: $@") if $@ ne ''; |
---|
12630 | |
---|
12631 | return 0; |
---|
12632 | } |
---|
12633 | |
---|
12634 | # my($k,$v,$fn); |
---|
12635 | # while (($k,$v) = each(%::)) { |
---|
12636 | # local(*e)=$v; $fn=fileno(\*e); |
---|
12637 | # printf STDERR ("%-10s %-10s %s$eol",$k,$v,$fn) if defined $fn; |
---|
12638 | # } |
---|
12639 | |
---|
12640 | # Given a file handle (typically opened pipe to a subprocess, as returned |
---|
12641 | # from run_command), copy from it to a specified output file in binary mode. |
---|
12642 | sub run_command_copy($$) { |
---|
12643 | my($outfile, $ifh) = @_; |
---|
12644 | my($ofh) = IO::File->new; |
---|
12645 | $ofh->open($outfile,'>') or die "Can't create file $outfile: $!"; |
---|
12646 | binmode($ofh) or die "Can't set file $outfile to binmode: $!"; |
---|
12647 | binmode($ifh) or die "Can't set binmode on pipe: $!"; |
---|
12648 | my($len, $buf, $offset, $written); |
---|
12649 | while ($len = $ifh->sysread($buf, 16384)) { |
---|
12650 | $offset = 0; |
---|
12651 | while ($len > 0) { # handle partial writes |
---|
12652 | $written = syswrite($ofh, $buf, $len, $offset); |
---|
12653 | defined($written) or die "syswrite to $outfile failed: $!"; |
---|
12654 | consumed_bytes($written, 'run_command_copy'); |
---|
12655 | $len -= $written; $offset += $written; |
---|
12656 | } |
---|
12657 | } |
---|
12658 | my($rerr); $ifh->close or $rerr=$!; my($rv) = $?; |
---|
12659 | $ofh->close or die "Can't close $outfile: $!"; |
---|
12660 | ($rv,$rerr); # return subprocess termination status and errno |
---|
12661 | } |
---|
12662 | |
---|
12663 | # extract listed files from archive and store in new file |
---|
12664 | sub store_mgr($$$@) { |
---|
12665 | my($tempdir, $parent_obj, $list, $cmd, @args) = @_; |
---|
12666 | |
---|
12667 | my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement; |
---|
12668 | my(@rv); |
---|
12669 | for my $f (@$list) { |
---|
12670 | next if $f =~ m{/\z}; # ignore directories |
---|
12671 | my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts", |
---|
12672 | $parent_obj); |
---|
12673 | $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num"); |
---|
12674 | $newpart_obj->name_declared($f); # store tainted name |
---|
12675 | my($newpart) = $newpart_obj->full_name; |
---|
12676 | do_log(5,sprintf('store_mgr: extracting "%s" to file %s using %s', |
---|
12677 | $f, $newpart, $cmd)); |
---|
12678 | if ($f =~ m{^\.?[A-Za-z0-9_][A-Za-z0-9/._=~-]*\z}) { # apparently safe arg |
---|
12679 | } else { # this is not too bad, as run_command does not use shell |
---|
12680 | do_log(1, "store_mgr: NOTICE: untainting funny argument \"$f\""); |
---|
12681 | } |
---|
12682 | my($proc_fh,$pid) = run_command(undef,undef,$cmd,@args,untaint($f)); |
---|
12683 | my($rv,$rerr) = run_command_copy($newpart,$proc_fh); |
---|
12684 | do_log(5,"store_mgr: extracted by $cmd, ".exit_status_str($rv,$rerr)); |
---|
12685 | push(@rv, $rv); |
---|
12686 | } |
---|
12687 | @rv = grep { $_ != 0 } @rv; |
---|
12688 | @rv ? $rv[0] : 0; # just return the first nonzero status (if any), or 0 |
---|
12689 | } |
---|
12690 | |
---|
12691 | 1; |
---|
12692 | |
---|
12693 | __DATA__ |
---|
12694 | # |
---|
12695 | # ============================================================================= |
---|
12696 | # This text section governs how a main per-message amavisd-new log entry |
---|
12697 | # is formed. An empty text will prevent a log entry, multi-line text will |
---|
12698 | # produce several log entries, one for each nonempty line. |
---|
12699 | # Syntax is explained in the README.customize file. |
---|
12700 | [?%#D|#|Passed # |
---|
12701 | [? [?%#V|1] |INFECTED (%V)|# |
---|
12702 | [? [?%#F|1] |BANNED (%F)|# |
---|
12703 | [? [? %2|1] |SPAM|# |
---|
12704 | [? [?%#X|1] |BAD-HEADER|CLEAN]]]]# |
---|
12705 | , [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%D|,]# |
---|
12706 | [? %q ||, quarantine: %i]# |
---|
12707 | [? %Q ||, Queue-ID: %Q]# |
---|
12708 | [? %m ||, Message-ID: %m]# |
---|
12709 | [? %r ||, Resent-Message-ID: %r]# |
---|
12710 | , Hits: %c# |
---|
12711 | #, size: %z# |
---|
12712 | #[? %j ||, Subject: "%j"]# |
---|
12713 | #[? %#T ||, tests=[%T|,]]# |
---|
12714 | , %y ms# |
---|
12715 | ] |
---|
12716 | [?%#O|#|Blocked # |
---|
12717 | [? [?%#V|1] |INFECTED (%V)|# |
---|
12718 | [? [?%#F|1] |BANNED (%F)|# |
---|
12719 | [? [? %2|1] |SPAM|# |
---|
12720 | [? [?%#X|1] |BAD-HEADER|CLEAN]]]]# |
---|
12721 | , [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%O|,]# |
---|
12722 | [? %q ||, quarantine: %i]# |
---|
12723 | [? %Q ||, Queue-ID: %Q]# |
---|
12724 | [? %m ||, Message-ID: %m]# |
---|
12725 | [? %r ||, Resent-Message-ID: %r]# |
---|
12726 | , Hits: %c# |
---|
12727 | #, size: %z# |
---|
12728 | #[? %j ||, Subject: "%j"]# |
---|
12729 | #[? %#T ||, tests=[%T|,]]# |
---|
12730 | , %y ms# |
---|
12731 | ] |
---|
12732 | __DATA__ |
---|
12733 | # |
---|
12734 | # ============================================================================= |
---|
12735 | # This text section governs how a main per-recipient amavisd-new log entry |
---|
12736 | # is formed. An empty text will prevent a log entry, multi-line text will |
---|
12737 | # produce several log entries, one for each nonempty line. |
---|
12738 | # Macro %. might be useful, it counts recipients starting from 1. |
---|
12739 | # Syntax is explained in the README.customize file. |
---|
12740 | # |
---|
12741 | [?%#D||Passed # |
---|
12742 | [? [?%#V|1] |INFECTED (%V)|# |
---|
12743 | [? [?%#F|1] |BANNED (%F)|# |
---|
12744 | [? [? %2|1] |SPAM|# |
---|
12745 | [? [?%#X|1] |BAD-HEADER|CLEAN]]]]# |
---|
12746 | , <%o> -> [%D|,], Hits: %c# |
---|
12747 | , tag=%3, tag2=%4, kill=%5# NOTE: macros %3, %4, %5 are experimental |
---|
12748 | , %0/%1/%2/%k# |
---|
12749 | ] |
---|
12750 | [?%#O||Blocked # |
---|
12751 | [? [?%#V|1] |INFECTED (%V)|# |
---|
12752 | [? [?%#F|1] |BANNED (%F)|# |
---|
12753 | [? [? %2|1] |SPAM|# |
---|
12754 | [? [?%#X|1] |BAD-HEADER|CLEAN]]]]# |
---|
12755 | , <%o> -> [%O|,], Hits: %c# |
---|
12756 | , tag=%3, tag2=%4, kill=%5# NOTE: macros %3, %4, %5 are experimental |
---|
12757 | , %0/%1/%2/%k# |
---|
12758 | ] |
---|
12759 | __DATA__ |
---|
12760 | # |
---|
12761 | # ============================================================================= |
---|
12762 | # This is a template for (neutral: non-virus, non-spam, non-banned) DELIVERY |
---|
12763 | # STATUS NOTIFICATIONS to sender. For syntax and customization instructions |
---|
12764 | # see README.customize. Note that only valid header fields are allowed; |
---|
12765 | # non-standard header field heads must begin with "X-" . |
---|
12766 | # The From, To and Date header fields will be provided automatically. |
---|
12767 | # |
---|
12768 | Subject: Undeliverable mail[?%#X||, invalid characters in header] |
---|
12769 | Message-ID: <DSN%n@%h> |
---|
12770 | |
---|
12771 | [? %#X ||INVALID HEADER (INVALID CHARACTERS OR SPACE GAP) |
---|
12772 | |
---|
12773 | [%X\n] |
---|
12774 | ]\ |
---|
12775 | This nondelivery report was generated by the amavisd-new program |
---|
12776 | at host %h. Our internal reference code for your message |
---|
12777 | is %n. |
---|
12778 | |
---|
12779 | [? %#X || |
---|
12780 | WHAT IS AN INVALID CHARACTER IN MAIL HEADER? |
---|
12781 | |
---|
12782 | The RFC 2822 standard specifies rules for forming internet messages. |
---|
12783 | It does not allow the use of characters with codes above 127 to be used |
---|
12784 | directly (non-encoded) in mail header (it also prohibits NUL and bare CR). |
---|
12785 | |
---|
12786 | If characters (e.g. with diacritics) from ISO Latin or other alphabets |
---|
12787 | need to be included in the header, these characters need to be properly |
---|
12788 | encoded according to RFC 2047. This encoding is often done transparently |
---|
12789 | by mail reader (MUA), but if automatic encoding is not available (e.g. |
---|
12790 | by some older MUA) it is the user's responsibility to avoid the use |
---|
12791 | of such characters in mail header, or to encode them manually. Typically |
---|
12792 | the offending header fields in this category are 'Subject', 'Organization', |
---|
12793 | and comment fields in e-mail addresses of the 'From', 'To' and 'Cc'. |
---|
12794 | |
---|
12795 | Sometimes such invalid header fields are inserted automatically |
---|
12796 | by some MUA, MTA, content checker, or other mail handling service. |
---|
12797 | If this is the case, that service needs to be fixed or properly configured. |
---|
12798 | Typically the offending header fields in this category are 'Date', |
---|
12799 | 'Received', 'X-Mailer', 'X-Priority', 'X-Scanned', etc. |
---|
12800 | |
---|
12801 | If you don't know how to fix or avoid the problem, please report it |
---|
12802 | to _your_ postmaster or system manager. |
---|
12803 | ]\ |
---|
12804 | |
---|
12805 | Return-Path: %s |
---|
12806 | Your message[?%m|| %m][?%r|| (Resent-Message-ID: %r)] |
---|
12807 | could not be delivered to:[\n %N] |
---|
12808 | __DATA__ |
---|
12809 | # |
---|
12810 | # ============================================================================= |
---|
12811 | # This is a template for VIRUS/BANNED SENDER NOTIFICATIONS. |
---|
12812 | # For syntax and customization instructions see README.customize. |
---|
12813 | # Note that only valid header fields are allowed; |
---|
12814 | # non-standard header field heads must begin with "X-" . |
---|
12815 | # The From, To and Date header fields will be provided automatically. |
---|
12816 | # |
---|
12817 | Subject: [? %#V |[? %#F |Unknown problem|BANNED (%F)]|VIRUS (%V)] IN MAIL FROM YOU |
---|
12818 | [? %m |#|In-Reply-To: %m] |
---|
12819 | Message-ID: <VS%n@%h> |
---|
12820 | |
---|
12821 | [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED CONTENTS ALERT]|VIRUS ALERT] |
---|
12822 | |
---|
12823 | Our content checker found |
---|
12824 | [? %#V |#| [? %#V |viruses|virus|viruses]: %V] |
---|
12825 | [? %#F |#| banned [? %#F |names|name|names]: %F] |
---|
12826 | [? %#X |#|\n[%X\n]] |
---|
12827 | in email presumably from you (%s), |
---|
12828 | to the following [? %#R |recipients|recipient|recipients]:[ |
---|
12829 | -> %R] |
---|
12830 | |
---|
12831 | Our internal reference code for your message is %n. |
---|
12832 | |
---|
12833 | [? %#V ||Please check your system for viruses, |
---|
12834 | or ask your system administrator to do so. |
---|
12835 | |
---|
12836 | ]# |
---|
12837 | [? %#D |Delivery of the email was stopped! |
---|
12838 | |
---|
12839 | ]# |
---|
12840 | [? %#V |[? %#F ||# |
---|
12841 | The message has been blocked because it contains a component |
---|
12842 | (as a MIME part or nested within) with declared name |
---|
12843 | or MIME type or contents type violating our access policy. |
---|
12844 | |
---|
12845 | To transfer contents that may be considered risky or unwanted |
---|
12846 | by site policies, or simply too large for mailing, please consider |
---|
12847 | publishing your content on the web, and only sending an URL of the |
---|
12848 | document to the recipient. |
---|
12849 | |
---|
12850 | Depending on the recipient and sender site policies, with a little |
---|
12851 | effort it might still be possible to send any contents (including |
---|
12852 | viruses) using one of the following methods: |
---|
12853 | |
---|
12854 | - encrypted using pgp, gpg or other encryption methods; |
---|
12855 | |
---|
12856 | - wrapped in a password-protected or scrambled container or archive |
---|
12857 | (e.g.: zip -e, arj -g, arc g, rar -p, or other methods) |
---|
12858 | |
---|
12859 | Note that if the contents is not intended to be secret, the |
---|
12860 | encryption key or password may be included in the same message |
---|
12861 | for recipient's convenience. |
---|
12862 | |
---|
12863 | We are sorry for inconvenience if the contents was not malicious. |
---|
12864 | |
---|
12865 | The purpose of these restrictions is to cut the most common propagation |
---|
12866 | methods used by viruses and other malware. These often exploit automatic |
---|
12867 | mechanisms and security holes in certain mail readers (Microsoft mail |
---|
12868 | readers and browsers are a common and easy target). By requiring an |
---|
12869 | explicit and decisive action from the recipient to decode mail, |
---|
12870 | the dangers of automatic malware propagation is largely reduced. |
---|
12871 | # |
---|
12872 | # Details of our mail restrictions policy are available at ... |
---|
12873 | |
---|
12874 | ]]# |
---|
12875 | For your reference, here are headers from your email: |
---|
12876 | ------------------------- BEGIN HEADERS ----------------------------- |
---|
12877 | Return-Path: %s |
---|
12878 | [%H |
---|
12879 | ]\ |
---|
12880 | -------------------------- END HEADERS ------------------------------ |
---|
12881 | __DATA__ |
---|
12882 | # |
---|
12883 | # ============================================================================= |
---|
12884 | # This is a template for non-spam (VIRUS,...) ADMINISTRATOR NOTIFICATIONS. |
---|
12885 | # For syntax and customization instructions see README.customize. |
---|
12886 | # Note that only valid header fields are allowed; non-standard header |
---|
12887 | # field heads must begin with "X-" . |
---|
12888 | # |
---|
12889 | Date: %d |
---|
12890 | From: %f |
---|
12891 | Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED (%F)]|VIRUS (%V)]# |
---|
12892 | FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>] |
---|
12893 | To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]] |
---|
12894 | [? %#C |#|Cc: [<%C>|, ]] |
---|
12895 | Message-ID: <VA%n@%h> |
---|
12896 | |
---|
12897 | [? %#V |No viruses were found. |
---|
12898 | |A virus was found: %V |
---|
12899 | |Two viruses were found:\n %V |
---|
12900 | |%#V viruses were found:\n %V |
---|
12901 | ] |
---|
12902 | [? %#F |#\ |
---|
12903 | |A banned name was found:\n %F |
---|
12904 | |Two banned names were found:\n %F |
---|
12905 | |%#F banned names were found:\n %F |
---|
12906 | ] |
---|
12907 | [? %#X |#\ |
---|
12908 | |Bad header was found:[\n %X] |
---|
12909 | ] |
---|
12910 | [? %#W |#\ |
---|
12911 | |Scanner detecting a virus: %W |
---|
12912 | |Scanners detecting a virus: %W |
---|
12913 | ] |
---|
12914 | The mail originated from: <%o> |
---|
12915 | [? %a |#|First upstream SMTP client IP address: \[%a\] %g |
---|
12916 | ] |
---|
12917 | [? %t |#|According to the 'Received:' trace, the message originated at: |
---|
12918 | \[%e\] |
---|
12919 | %t |
---|
12920 | ] |
---|
12921 | [? %#S |Notification to sender will not be mailed. |
---|
12922 | |
---|
12923 | ]# |
---|
12924 | [? %#D |#|The message WILL BE delivered to:[\n%D] |
---|
12925 | ] |
---|
12926 | [? %#N |#|The message WAS NOT delivered to:[\n%N] |
---|
12927 | ] |
---|
12928 | [? %#V |#|[? %#v |#|Virus scanner output:[\n %v] |
---|
12929 | ]] |
---|
12930 | [? %q |Not quarantined.|The message has been quarantined as:\n %q |
---|
12931 | ] |
---|
12932 | ------------------------- BEGIN HEADERS ----------------------------- |
---|
12933 | Return-Path: %s |
---|
12934 | [%H |
---|
12935 | ]\ |
---|
12936 | -------------------------- END HEADERS ------------------------------ |
---|
12937 | __DATA__ |
---|
12938 | # |
---|
12939 | # ============================================================================= |
---|
12940 | # This is a template for VIRUS/BANNED/BAD-HEADER RECIPIENTS NOTIFICATIONS. |
---|
12941 | # For syntax and customization instructions see README.customize. |
---|
12942 | # Note that only valid header fields are allowed; non-standard header |
---|
12943 | # field heads must begin with "X-" . |
---|
12944 | # |
---|
12945 | Date: %d |
---|
12946 | From: %f |
---|
12947 | Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED]|VIRUS (%V)]# |
---|
12948 | IN MAIL TO YOU (from [?%o|(?)|<%o>]) |
---|
12949 | To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]] |
---|
12950 | [? %#C |#|Cc: [<%C>|, ]] |
---|
12951 | Message-ID: <VR%n@%h> |
---|
12952 | |
---|
12953 | [? %#V |[? %#F ||BANNED CONTENTS ALERT]|VIRUS ALERT] |
---|
12954 | |
---|
12955 | Our content checker found |
---|
12956 | [? %#V |#| [? %#V |viruses|virus|viruses]: %V] |
---|
12957 | [? %#F |#| banned [? %#F |names|name|names]: %F] |
---|
12958 | [? %#X |#|\n[%X\n]] |
---|
12959 | |
---|
12960 | in an email to you [? %S |from unknown sender:|from:] |
---|
12961 | %o |
---|
12962 | [? %S |claiming to be: %s|#] |
---|
12963 | |
---|
12964 | [? %a |#|First upstream SMTP client IP address: \[%a\] %g |
---|
12965 | ] |
---|
12966 | [? %t |#|According to the 'Received:' trace, the message originated at: |
---|
12967 | \[%e\] |
---|
12968 | %t |
---|
12969 | ] |
---|
12970 | Our internal reference code for this message is %n. |
---|
12971 | [? %q |Not quarantined.|The message has been quarantined as: |
---|
12972 | %q] |
---|
12973 | |
---|
12974 | Please contact your system administrator for details. |
---|
12975 | __DATA__ |
---|
12976 | # |
---|
12977 | # ============================================================================= |
---|
12978 | # This is a template for SPAM SENDER NOTIFICATIONS. |
---|
12979 | # For syntax and customization instructions see README.customize. |
---|
12980 | # Note that only valid header fields are allowed; |
---|
12981 | # non-standard header field heads must begin with "X-" . |
---|
12982 | # The From, To and Date header fields will be provided automatically. |
---|
12983 | # |
---|
12984 | Subject: Considered UNSOLICITED BULK EMAIL from you |
---|
12985 | [? %m |#|In-Reply-To: %m] |
---|
12986 | Message-ID: <SS%n@%h> |
---|
12987 | |
---|
12988 | Your message to:[ |
---|
12989 | -> %R] |
---|
12990 | |
---|
12991 | was considered unsolicited bulk e-mail (UBE). |
---|
12992 | [? %#X |#|\n[%X\n]] |
---|
12993 | Subject: %j |
---|
12994 | Return-Path: %s |
---|
12995 | Our internal reference code for your message is %n. |
---|
12996 | |
---|
12997 | [? %#D |Delivery of the email was stopped! |
---|
12998 | ]# |
---|
12999 | # |
---|
13000 | # SpamAssassin report: |
---|
13001 | # [%A |
---|
13002 | # ]\ |
---|
13003 | __DATA__ |
---|
13004 | # |
---|
13005 | # ============================================================================= |
---|
13006 | # This is a template for SPAM ADMINISTRATOR NOTIFICATIONS. |
---|
13007 | # For syntax and customization instructions see README.customize. |
---|
13008 | # Note that only valid header fields are allowed; non-standard header |
---|
13009 | # field heads must begin with "X-" . |
---|
13010 | # |
---|
13011 | Date: %d |
---|
13012 | From: %f |
---|
13013 | Subject: SPAM FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>] |
---|
13014 | To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]] |
---|
13015 | [? %#C |#|Cc: [<%C>|, ]] |
---|
13016 | [? %#B |#|Bcc: [<%B>|, ]] |
---|
13017 | Message-ID: <SA%n@%h> |
---|
13018 | |
---|
13019 | Unsolicited bulk email [? %S |from unknown or forged sender:|from:] |
---|
13020 | %o |
---|
13021 | Subject: %j |
---|
13022 | |
---|
13023 | [? %a |#|First upstream SMTP client IP address: \[%a\] %g |
---|
13024 | ] |
---|
13025 | [? %t |#|According to the 'Received:' trace, the message originated at: |
---|
13026 | \[%e\] |
---|
13027 | %t |
---|
13028 | ] |
---|
13029 | [? %#D |#|The message WILL BE delivered to:[\n%D] |
---|
13030 | ] |
---|
13031 | [? %#N |#|The message WAS NOT delivered to:[\n%N] |
---|
13032 | ] |
---|
13033 | [? %q |Not quarantined.|The message has been quarantined as:\n %q |
---|
13034 | ] |
---|
13035 | SpamAssassin report: |
---|
13036 | [%A |
---|
13037 | ]\ |
---|
13038 | |
---|
13039 | ------------------------- BEGIN HEADERS ----------------------------- |
---|
13040 | Return-Path: %s |
---|
13041 | [%H |
---|
13042 | ]\ |
---|
13043 | -------------------------- END HEADERS ------------------------------ |
---|