/[pkg-listmaster]/trunk/amavis/bin/amavisd-new
ViewVC logotype

Contents of /trunk/amavis/bin/amavisd-new

Parent Directory Parent Directory | Revision Log Revision Log


Revision 189 - (hide annotations) (download)
Tue Apr 15 15:06:40 2008 UTC (5 years, 1 month ago) by formorer
File size: 655298 byte(s)
Fix handling of stdin/stderr/stdout. cpio and tar
handling was broken before.....

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