source: npl/mailserver/amavisd-new/amavisd @ 26ffad7

Last change on this file since 26ffad7 was 929bb42, checked in by Edwin Eefting <edwin@datux.nl>, 3 years ago

trying kernel 5.14

  • Property mode set to 100755
File size: 546.2 KB
Line 
1#!/usr/bin/perl -T
2
3#------------------------------------------------------------------------------
4# This is amavisd-new.
5# It is an interface between message transfer agent (MTA) and virus
6# scanners and/or spam scanners, functioning as a mail content filter.
7#
8# It is a performance-enhanced and feature-enriched version of amavisd
9# (which in turn is a daemonized version of AMaViS), initially based
10# on amavisd-snapshot-20020300).
11#
12# All work since amavisd-snapshot-20020300:
13#   Copyright (C) 2002,2003,2004  Mark Martinec,  All Rights Reserved.
14# with contributions from the amavis-* mailing lists and individuals,
15# as acknowledged in the release notes.
16#
17#    This program is free software; you can redistribute it and/or modify
18#    it under the terms of the GNU General Public License as published by
19#    the Free Software Foundation; either version 2 of the License, or
20#    (at your option) any later version.
21#
22#    This program is distributed in the hope that it will be useful,
23#    but WITHOUT ANY WARRANTY; without even the implied warranty of
24#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25#    GNU General Public License for details.
26#
27#    You should have received a copy of the GNU General Public License
28#    along with this program; if not, write to the Free Software
29#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
30
31# Author: Mark Martinec <mark.martinec@ijs.si>
32# Patches and problem reports are welcome.
33#
34# The latest version of this program is available at:
35#   http://www.ijs.si/software/amavisd/
36#------------------------------------------------------------------------------
37
38# Here is a boilerplate from the amavisd(-snapshot) version,
39# which is the version that served as a base code for the initial
40# version of amavisd-new. License terms were the same:
41#
42#   Author:  Chris Mason <cmason@unixzone.com>
43#   Current maintainer: Lars Hecking <lhecking@users.sourceforge.net>
44#   Based on work by:
45#         Mogens Kjaer, Carlsberg Laboratory, <mk@crc.dk>
46#         Juergen Quade, Softing GmbH, <quade@softing.com>
47#         Christian Bricart <shiva@aachalon.de>
48#         Rainer Link <link@foo.fh-furtwangen.de>
49#   This script is part of the AMaViS package.  For more information see:
50#     http://amavis.org/
51#   Copyright (C) 2000 - 2002 the people mentioned above
52#   This software is licensed under the GNU General Public License (GPL)
53#   See:  http://www.gnu.org/copyleft/gpl.html
54#------------------------------------------------------------------------------
55
56#------------------------------------------------------------------------------
57#Index of packages in this file
58#  Amavis::Boot
59#  Amavis::Conf
60#  Amavis::Lock
61#  Amavis::Log
62#  Amavis::Timing
63#  Amavis::Util
64#  Amavis::rfc2821_2822_Tools
65#  Amavis::Lookup::RE
66#  Amavis::Lookup::Label
67#  Amavis::Lookup
68#  Amavis::Expand
69#  Amavis::In::Connection
70#  Amavis::In::Message::PerRecip
71#  Amavis::In::Message
72#  Amavis::Out::EditHeader
73#  Amavis::Out::Local
74#  Amavis::Out
75#  Amavis::UnmangleSender
76#  Amavis::Unpackers::NewFilename
77#  Amavis::Unpackers::Part
78#  Amavis::Unpackers::OurFiler
79#  Amavis::Unpackers::Validity
80#  Amavis::Unpackers::MIME
81#  Amavis::Notify
82#  Amavis::Cache
83#  Amavis
84#optionally compiled-in packages: ---------------------------------------------
85#  Amavis::DB::SNMP
86#  Amavis::DB
87#  Amavis::Cache
88#  Amavis::Lookup::SQLfield
89#  Amavis::Lookup::SQL
90#  Amavis::Lookup::LDAP
91#  Amavis::Lookup::LDAPattr
92#  Amavis::In::AMCL
93#  Amavis::In::SMTP
94#  Amavis::In::QMQPqq
95#  Amavis::AV
96#  Amavis::SpamControl
97#  Amavis::Unpackers
98#------------------------------------------------------------------------------
99
100#
101package Amavis::Boot;
102use strict;
103use re 'taint';
104
105# Fetch all required modules (or nicely report missing ones), and compile them
106# once-and-for-all at the parent process, so that forked children can inherit
107# and share already compiled code in memory. Children will still need to 'use'
108# modules if they want to inherit from their name space.
109#
110sub fetch_modules($$@) {
111  my($reason, $required, @modules) = @_;
112  my(@missing);
113  for my $m (@modules) {
114    local($_) = $m;
115    $_ .= /^auto::/ ? '.al' : '.pm'  if !/\.(pm|pl|al)\z/;
116    s[::][/]g;
117    eval { require $_ } or push(@missing, $m);
118  }
119  die "ERROR: MISSING $reason:\n" . join('', map { "  $_\n" } @missing)
120    if $required && @missing;
121  \@missing;
122}
123
124BEGIN {
125  fetch_modules('REQUIRED BASIC MODULES', 1, qw(
126    Exporter POSIX Fcntl Socket Errno Carp Time::HiRes
127    IO::Handle IO::File IO::Socket IO::Socket::UNIX IO::Socket::INET
128    IO::Wrap IO::Stringy Digest::MD5 Unix::Syslog File::Basename File::Copy
129    Mail::Field Mail::Address Mail::Header Mail::Internet
130    MIME::Base64 MIME::QuotedPrint MIME::Words
131    MIME::Head MIME::Body MIME::Entity MIME::Parser MIME::Decoder
132    MIME::Decoder::Base64 MIME::Decoder::Binary MIME::Decoder::QuotedPrint
133    MIME::Decoder::NBit MIME::Decoder::UU MIME::Decoder::Gzip64
134    Net::Cmd Net::SMTP Net::Server Net::Server::PreForkSimple
135  ));
136  # with earlier versions of Perl one may need to add additional modules
137  # to the list, such as: auto::POSIX::setgid auto::POSIX::setuid ...
138  fetch_modules('OPTIONAL BASIC MODULES', 0, qw(
139    Carp::Heavy auto::POSIX::setgid auto::POSIX::setuid
140    MIME::Decoder::BinHex
141  ));
142}
143
1441;
145
146#
147package Amavis::Conf;
148use strict;
149use re 'taint';
150
151# prototypes
152sub D_REJECT();
153sub D_BOUNCE();
154sub D_DISCARD();
155sub D_PASS();
156
157BEGIN {
158  use Exporter ();
159  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
160  $VERSION = '2.034';
161  @ISA = qw(Exporter);
162  @EXPORT = ();
163  @EXPORT_OK = ();
164  %EXPORT_TAGS = (
165    'dynamic_confvars' => [qw(
166      $policy_bank_name $protocol @inet_acl
167      $log_level $log_templ $log_recip_templ $forward_method $notify_method
168
169      $amavis_auth_user $amavis_auth_pass $auth_reauthenticate_forwarded
170      $auth_required_out $auth_required_inp @auth_mech_avail
171      $local_client_bind_address
172      $localhost_name $smtpd_greeting_banner $smtpd_quit_banner
173      $smtpd_message_size_limit
174
175      $final_virus_destiny $final_spam_destiny
176      $final_banned_destiny $final_bad_header_destiny
177      $warnvirussender $warnspamsender $warnbannedsender $warnbadhsender
178      $warn_offsite
179
180      @av_scanners @av_scanners_backup $first_infected_stops_scan
181      $bypass_decode_parts
182
183      $defang_virus $defang_banned $defang_spam
184      $defang_bad_header $defang_undecipherable $defang_all
185      $undecipherable_subject_tag
186      $sa_spam_report_header $sa_spam_level_char
187      $sa_mail_body_size_limit
188
189      $localpart_is_case_sensitive
190      $recipient_delimiter $replace_existing_extension
191      $hdr_encoding $bdy_encoding $hdr_encoding_qb
192      $notify_xmailer_header $X_HEADER_TAG $X_HEADER_LINE
193      $remove_existing_x_scanned_headers $remove_existing_spam_headers
194
195      $hdrfrom_notify_sender $hdrfrom_notify_recip
196      $hdrfrom_notify_admin $hdrfrom_notify_spamadmin
197      $mailfrom_notify_sender $mailfrom_notify_recip
198      $mailfrom_notify_admin $mailfrom_notify_spamadmin
199      $mailfrom_to_quarantine
200      $virus_quarantine_method $spam_quarantine_method
201      $banned_files_quarantine_method $bad_header_quarantine_method
202      %local_delivery_aliases
203
204      $notify_sender_templ
205      $notify_virus_sender_templ $notify_spam_sender_templ
206      $notify_virus_admin_templ  $notify_spam_admin_templ
207      $notify_virus_recips_templ $notify_spam_recips_templ
208
209      $banned_namepath_re
210      $per_recip_whitelist_sender_lookup_tables
211      $per_recip_blacklist_sender_lookup_tables
212
213      @local_domains_maps @mynetworks_maps
214      @bypass_virus_checks_maps @bypass_spam_checks_maps
215      @bypass_banned_checks_maps @bypass_header_checks_maps
216      @virus_lovers_maps @spam_lovers_maps
217      @banned_files_lovers_maps @bad_header_lovers_maps
218      @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
219      @newvirus_admin_maps @virus_admin_maps
220      @banned_admin_maps @bad_header_admin_maps @spam_admin_maps
221      @virus_quarantine_to_maps
222      @banned_quarantine_to_maps @bad_header_quarantine_to_maps
223      @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps
224      @banned_filename_maps
225      @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps
226      @spam_dsn_cutoff_level_maps @spam_modifies_subj_maps
227      @spam_subject_tag_maps @spam_subject_tag2_maps
228      @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
229      @message_size_limit_maps
230      @addr_extension_virus_maps  @addr_extension_spam_maps
231      @addr_extension_banned_maps @addr_extension_bad_header_maps
232      @debug_sender_maps
233    )],
234    'confvars' => [qw(
235      $myproduct_name $myversion_id $myversion_id_numeric $myversion_date
236      $myversion $myhostname
237      $MYHOME $TEMPBASE $QUARANTINEDIR
238      $daemonize $pid_file $lock_file $db_home
239      $enable_db $enable_global_cache
240      $daemon_user $daemon_group $daemon_chroot_dir $path
241      $DEBUG $DO_SYSLOG $SYSLOG_LEVEL $LOGFILE
242      $max_servers $max_requests $child_timeout
243      %current_policy_bank %policy_bank %interface_policy
244      $unix_socketname $inet_socket_port $inet_socket_bind
245      $insert_received_line $relayhost_is_client $smtpd_recipient_limit
246      $MAXLEVELS $MAXFILES
247      $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR
248      $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR
249      @lookup_sql_dsn
250      $sql_select_policy $sql_select_white_black_list
251      $virus_check_negative_ttl $virus_check_positive_ttl
252      $spam_check_negative_ttl $spam_check_positive_ttl
253      $enable_ldap $default_ldap
254      @keep_decoded_original_maps @map_full_type_to_short_type_maps
255      @viruses_that_fake_sender_maps
256    )],
257    'unpack' => [qw(
258      $file $arc $gzip $bzip2 $lzop $lha $unarj $uncompress $unfreeze
259      $unrar $zoo $pax $cpio $ar $rpm2cpio $cabextract $ripole
260    )],
261    'sa' => [qw(
262      $helpers_home $dspam
263      $sa_local_tests_only $sa_auto_whitelist $sa_timeout $sa_debug
264    )],
265    'platform' => [qw(
266      $can_truncate $unicode_aware $eol
267      &D_REJECT &D_BOUNCE &D_DISCARD &D_PASS
268    )],
269
270    # other variables settable by user in amavisd.conf,
271    # but not directly accessible by the program
272    'hidden_confvars' => [qw(
273      $mydomain
274    )],
275
276    # legacy variables, predeclared for compatibility of amavisd.conf
277    # The rest of the program does not use them directly and they should not be
278    # visible in other modules, but may be referenced throgh @*_maps variables.
279    'legacy_confvars' => [qw(
280      %local_domains @local_domains_acl $local_domains_re @mynetworks
281      %bypass_virus_checks @bypass_virus_checks_acl $bypass_virus_checks_re
282      %bypass_spam_checks @bypass_spam_checks_acl $bypass_spam_checks_re
283      %bypass_banned_checks @bypass_banned_checks_acl $bypass_banned_checks_re
284      %bypass_header_checks @bypass_header_checks_acl $bypass_header_checks_re
285      %virus_lovers @virus_lovers_acl $virus_lovers_re
286      %spam_lovers @spam_lovers_acl $spam_lovers_re
287      %banned_files_lovers @banned_files_lovers_acl $banned_files_lovers_re
288      %bad_header_lovers @bad_header_lovers_acl $bad_header_lovers_re
289      %virus_admin %spam_admin
290      $newvirus_admin $virus_admin $banned_admin $bad_header_admin $spam_admin
291      $warnvirusrecip $warnbannedrecip $warnbadhrecip
292      $virus_quarantine_to $banned_quarantine_to $bad_header_quarantine_to
293      $spam_quarantine_to $spam_quarantine_bysender_to
294      $keep_decoded_original_re $map_full_type_to_short_type_re
295      $banned_filename_re $viruses_that_fake_sender_re
296      $sa_tag_level_deflt $sa_tag2_level_deflt $sa_kill_level_deflt
297      $sa_dsn_cutoff_level $sa_spam_modifies_subj
298      $sa_spam_subject_tag1 $sa_spam_subject_tag
299      %whitelist_sender @whitelist_sender_acl $whitelist_sender_re
300      %blacklist_sender @blacklist_sender_acl $blacklist_sender_re
301      $addr_extension_virus $addr_extension_spam
302      $addr_extension_banned $addr_extension_bad_header
303      $gets_addr_in_quoted_form @debug_sender_acl
304    )],
305  );
306  Exporter::export_tags qw(dynamic_confvars confvars unpack sa platform
307                           hidden_confvars legacy_confvars);
308} # BEGIN
309
310use POSIX qw(uname);
311use Carp ();
312use Errno qw(ENOENT EACCES);
313
314use vars @EXPORT;
315
316sub c($); sub cr($); sub ca($);  # prototypes
317use subs qw(c cr ca);  # access subroutine to new-style config variables
318BEGIN { push(@EXPORT,qw(c cr ca)) }
319
320{ # initialize new-style hash (policy bank) containing dynamic config settings
321  for my $tag (@EXPORT_TAGS{'dynamic_confvars'}) {
322    for my $v (@$tag) {
323      if ($v !~ /^([%\$\@])(.*)\z/) { die "Unsupported variable type: $v" }
324      else {
325        no strict 'refs'; my($type,$name) = ($1,$2);
326        $current_policy_bank{$name} = $type eq '$' ? \${"Amavis::Conf::$name"}
327                                    : $type eq '@' ? \@{"Amavis::Conf::$name"}
328                                    : $type eq '%' ? \%{"Amavis::Conf::$name"}
329                                    : undef;
330      }
331    }
332  }
333  $current_policy_bank{'policy_bank_name'} = '';  # builtin policy
334  $current_policy_bank{'policy_bank_path'} = '';
335  $policy_bank{''} = { %current_policy_bank };    # copy
336}
337
338# new-style access to dynamic config variables
339# return a config variable value - usually a scalar;
340# one level of indirection for scalars is allowed
341sub c($) {
342  my($name) = @_;
343  if (!exists $current_policy_bank{$name}) {
344    Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
345                        $name, $current_policy_bank{'policy_bank_name'}));
346  }
347  my($var) = $current_policy_bank{$name}; my($r) = ref($var);
348  !$r ? $var : $r eq 'SCALAR' ? $$var
349    : $r eq 'ARRAY' ? @$var : $r eq 'HASH' ? %$var : $var;
350}
351
352# return a ref to a config variable value, or undef if var is undefined
353sub cr($) {
354  my($name) = @_;
355  if (!exists $current_policy_bank{$name}) {
356    Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
357                        $name, $current_policy_bank{'policy_bank_name'}));
358  }
359  my($var) = $current_policy_bank{$name};
360  !defined($var) ? undef : !ref($var) ? \$var : $var;
361}
362
363# return a ref to a config variable value (which is supposed to be an array),
364# converting undef to an empty array, and a scalar to a one-element array
365# if necessary
366sub ca($) {
367  my($name) = @_;
368  if (!exists $current_policy_bank{$name}) {
369    Carp::croak(sprintf('No entry "%s" in policy bank "%s"',
370                        $name, $current_policy_bank{'policy_bank_name'}));
371  }
372  my($var) = $current_policy_bank{$name};
373  !defined($var) ? [] : !ref($var) ? [$var] : $var;
374}
375
376$myproduct_name = 'amavisd-new';
377$myversion_id = '2.2.1'; $myversion_date = '20041222';
378
379$myversion = "$myproduct_name-$myversion_id ($myversion_date)";
380$myversion_id_numeric =  # x.yyyzzz, allows numerical comparision, like Perl $]
381  sprintf("%8.6f", $1 + ($2 + $3/1000)/1000)
382  if $myversion_id =~ /^(\d+)(?:\.(\d*)(?:\.(\d*))?)?(.*)$/;
383
384$eol = "\n";  # native record separator in files: LF or CRLF or even CR
385$unicode_aware = $]>=5.008 && length("\x{263a}")==1 && eval { require Encode };
386
387# serves only as a quick default for other configuration settings
388$MYHOME   = '/var/amavis';
389$mydomain = '!change-mydomain-variable!.example.com';#intentionally bad default
390
391# Create debugging output - true: log to stderr; false: log to syslog/file
392$DEBUG = 0;
393
394# Cause Net::Server parameters 'background' and 'setsid' to be set,
395# resulting in the program to detach itself from the terminal
396$daemonize = 1;
397
398# Net::Server pre-forking settings - defaults, overruled by amavisd.conf
399$max_servers  = 2;   # number of pre-forked children
400$max_requests = 10;  # retire a child after that many accepts
401
402$child_timeout = 8*60; # abort child if it does not complete each task in n sec
403
404# Can file be truncated?
405# Set to 1 if 'truncate' works (it is XPG4-UNIX standard feature,
406#                               not required by Posix).
407# Things will go faster with SMTP-in, otherwise (e.g. with milter)
408# it makes no difference as file truncation will not be used.
409$can_truncate = 1;
410
411# expiration time of cached results: time to live in seconds
412# (how long the result of a virus/spam test remains valid)
413$virus_check_negative_ttl=  3*60; # time to remember that mail was not infected
414$virus_check_positive_ttl= 30*60; # time to remember that mail was infected
415$spam_check_negative_ttl = 30*60; # time to remember that mail was not spam
416$spam_check_positive_ttl = 30*60; # time to remember that mail was spam
417#
418# NOTE:
419#   Cache size will be determined by the largest of the $*_ttl values.
420#   Depending on the mail rate, the cache database may grow quite large.
421#   Reasonable compromise for the max value is 15 minutes to 2 hours.
422
423# Customizable notification messages, logging
424
425$SYSLOG_LEVEL = 'mail.debug';
426
427$enable_db = 0;           # load optional modules Amavis::DB & Amavis::DB::SNMP
428$enable_global_cache = 0; # enable use of bdb-based Amavis::Cache
429
430# Where to find SQL server(s) and database to support SQL lookups?
431# A list of triples: (dsn,user,passw). Specify more than one
432# for multiple (backup) SQL servers.
433#
434#@lookup_sql_dsn =
435#   ( ['DBI:mysql:mail:host1', 'some-username1', 'some-password1'],
436#     ['DBI:mysql:mail:host2', 'some-username2', 'some-password2'] );
437
438# The SQL select clause to fetch per-recipient policy settings
439# The %k will be replaced by a comma-separated list of query addresses
440# (e.g. full address, domain only, catchall).  Use ORDER, if there
441# is a chance that multiple records will match - the first match wins
442# If field names are not unique (e.g. 'id'), the later field overwrites the
443# earlier in a hash returned by lookup, which is why we use '*,users.id'.
444$sql_select_policy =
445  'SELECT *,users.id FROM users,policy'
446  . ' WHERE (users.policy_id=policy.id) AND (users.email IN (%k))'
447  . ' ORDER BY users.priority DESC';
448
449# The SQL select clause to check sender in per-recipient whitelist/blacklist
450# The first SELECT argument '?' will be users.id from recipient SQL lookup,
451# the %k will be sender addresses (e.g. full address, domain only, catchall).
452# Only the first occurrence of '?' will be replaced by users.id, subsequent
453# occurrences of '?' will see empty string as an argument. There can be zero
454# or more occurrences of %k, lookup keys will be multiplied accordingly.
455# Up until version 2.2.0 the '?' had to be placed before the '%k';
456# starting with 2.2.1 this restriction is lifted.
457$sql_select_white_black_list =
458  'SELECT wb FROM wblist,mailaddr'
459  . ' WHERE (wblist.rid=?) AND (wblist.sid=mailaddr.id)'
460  . '   AND (mailaddr.email IN (%k))'
461  . ' ORDER BY mailaddr.priority DESC';
462
463#
464# Receiving mail related
465
466# $unix_socketname = '/var/amavis/amavisd.sock'; # traditional amavis client protocol
467# $inet_socket_port = 10024;      # accept SMTP on this TCP port
468# $inet_socket_port = [10024,10026,10027];  # ...possibly on more than one
469$inet_socket_bind = '127.0.0.1';  # limit socket bind to loopback interface
470
471@inet_acl   = qw( 127.0.0.1   ::1 );  # allow SMTP access only from localhost
472@mynetworks = qw( 127.0.0.0/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 );
473
474$notify_method  = 'smtp:[127.0.0.1]:10025';
475$forward_method = 'smtp:[127.0.0.1]:10025';
476
477$virus_quarantine_method        = 'local:virus-%i-%n';
478$spam_quarantine_method         = 'local:spam-%b-%i-%n';
479$banned_files_quarantine_method = 'local:banned-%i-%n';
480$bad_header_quarantine_method   = 'local:badh-%i-%n';
481
482$insert_received_line = 1; # insert 'Received:' header field? (not with milter)
483$remove_existing_x_scanned_headers = 0;
484$remove_existing_spam_headers      = 1;
485
486# encoding (charset in MIME terminology)
487# to be used in RFC 2047-encoded ...
488$hdr_encoding = 'iso-8859-1';  # ... header field bodies
489$bdy_encoding = 'iso-8859-1';  # ... notification body text
490
491# encoding (encoding in MIME terminology)
492$hdr_encoding_qb = 'Q';        # quoted-printable (default)
493#$hdr_encoding_qb = 'B';       # base64           (usual for far east charsets)
494
495$smtpd_recipient_limit = 1100; # max recipients (RCPT TO) - sanity limit
496
497# $myhostname is used by SMTP server module in the initial SMTP welcome line,
498# in inserted 'Received:' lines, Message-ID in notifications, log entries, ...
499$myhostname = (uname)[1];  # should be a FQDN !
500
501$smtpd_greeting_banner = '${helo-name} ${protocol} ${product} service ready';
502$smtpd_quit_banner = '${helo-name} ${product} closing transmission channel';
503
504# $localhost_name is the name of THIS host running amavisd
505# (typically 'localhost'). It is used in HELO SMTP command
506# when reinjecting mail back to MTA via SMTP for final delivery.
507$localhost_name = 'localhost';
508
509# @auth_mech_avail = ('PLAIN','LOGIN');   # empty list disables incoming AUTH
510#$auth_required_inp = 1;   # incoming SMTP authentication required by amavisd?
511#$auth_required_out = 1;   # SMTP authentication required by MTA
512
513# SMTP AUTH username and password for notification submissions
514# (and reauthentication of forwarded mail if requested)
515#$amavis_auth_user = undef;  # perhaps: 'amavisd'
516#$amavis_auth_pass = undef;
517#$auth_reauthenticate_forwarded = undef;  # supply our own credentials also
518                                          # for forwarded (passed) mail
519
520# whom quarantined messages appear to be sent from (envelope sender)
521# $mailfrom_to_quarantine = undef; # original sender if undef, or set explicitly
522
523# where to send quarantined malware
524#   Specify undef to disable, or e-mail address containing '@',
525#   or just a local part, which will be mapped by %local_delivery_aliases
526#   into local mailbox name or directory. The lookup key is a recipient address
527$virus_quarantine_to  = 'virus-quarantine';   # %local_delivery_aliases mapped
528$banned_quarantine_to = 'banned-quarantine';  # %local_delivery_aliases mapped
529$bad_header_quarantine_to = 'bad-header-quarantine'; # %local_delivery_aliases
530$spam_quarantine_to   = 'spam-quarantine';    # %local_delivery_aliases mapped
531
532$banned_admin     = \@virus_admin_maps;  # compatibility
533$bad_header_admin = \@virus_admin_maps;  # compatibility
534
535# similar to $spam_quarantine_to, but the lookup key is the sender address
536$spam_quarantine_bysender_to = undef;  # dflt: no by-sender spam quarantine
537
538# quarantine directory or mailbox file or empty
539#   (only used if $virus_quarantine_to specifies direct local delivery)
540$QUARANTINEDIR = undef;  # no quarantine unless overridden by config
541
542$undecipherable_subject_tag = '***UNCHECKED*** ';
543
544# string to prepend to Subject header field when message qualifies as spam
545# $sa_spam_subject_tag1  = undef;  # example: '***possible SPAM*** '
546# $sa_spam_subject_tag   = undef;  # example: '***SPAM*** '
547$sa_spam_modifies_subj = 1;      # true for compatibility; can be a
548                                 # lookup table indicating per-recip settings
549$sa_spam_level_char = '*';  # character to be used in X-Spam-Level bar;
550                            # empty or undef disables adding this header field
551# $sa_spam_report_header = undef; # insert X-Spam-Report header field?
552$sa_local_tests_only = 0;
553$sa_debug = 0;
554$sa_timeout = 30;           # timeout in seconds for a call to SpamAssassin
555
556# MIME defanging is only activated when enabled and malware is allowed to pass
557# $defang_virus = undef;
558# $defang_banned = undef;
559# $defang_spam = undef;
560# $defang_bad_header = undef;
561# $defang_undecipherable = undef;
562# $defang_all = undef;
563
564$MIN_EXPANSION_FACTOR =   5;  # times original mail size
565$MAX_EXPANSION_FACTOR = 500;  # times original mail size
566
567# See amavisd.conf and README.lookups for details.
568
569# What to do with the message (this is independent of quarantining):
570#   Reject:  tell MTA to generate a non-delivery notification,  MTA gets 5xx
571#   Bounce:  generate a non-delivery notification by ourselves, MTA gets 250
572#   Discard: drop the message and pretend it was delivered,     MTA gets 250
573#   Pass:    deliver/accept the message
574#
575# Bounce and Reject are similar: in both cases sender gets a non-delivery
576# notification, either generated by amavisd-new, or by MTA. The notification
577# issued by amavisd-new may be more informative, while on the other hand
578# MTA may be able to do a true reject on the original SMTP session
579# (e.g. with sendmail milter), or else it just generates normal non-delivery
580# notification / bounce (e.g. with Postfix, Exim). As a consequence,
581# with Postfix and Exim and dual-sendmail setup the Bounce is more informative
582# than Reject, but sendmail-milter users may prefer Reject.
583#
584# Bounce and Discard are similar: in both cases amavisd-new confirms
585# to MTA the message reception with success code 250. The difference is
586# in sender notification: Bounce sends a non-delivery notification to sender,
587# Discard does not, the message is silently dropped. Quarantine and
588# admin notifications are not affected by any of these settings.
589#
590# COMPATIBITITY NOTE: the separation of *_destiny values into
591#   D_BOUNCE, D_REJECT, D_DISCARD and D_PASS made settings $warnvirussender
592#   and $warnspamsender only still useful with D_PASS. The combination of
593#   D_DISCARD + $warn*sender=1 is mapped into D_BOUNCE for compatibility.
594
595# intentionally leave value -1 unassigned for compatibility
596sub D_REJECT () { -3 }
597sub D_BOUNCE () { -2 }
598sub D_DISCARD() {  0 }
599sub D_PASS ()   {  1 }
600
601# The following symbolic constants can be used in *destiny settings:
602#
603# D_PASS     mail will pass to recipients, regardless of contents;
604#
605# D_DISCARD  mail will not be delivered to its recipients, sender will NOT be
606#            notified. Effectively we lose mail (but it will be quarantined
607#            unless disabled). Not a decent thing to do for a mailer.
608#
609# D_BOUNCE   mail will not be delivered to its recipients, a non-delivery
610#            notification (bounce) will be sent to the sender by amavisd-new;
611#            Exception: bounce (DSN) will not be sent if a virus name matches
612#            $viruses_that_fake_sender_maps, or to messages from mailing lists
613#            (Precedence: bulk|list|junk), or for spam exceeding
614#            spam_dsn_cutoff_level
615#
616# D_REJECT   mail will not be delivered to its recipients, sender should
617#            preferably get a reject, e.g. SMTP permanent reject response
618#            (e.g. with milter), or non-delivery notification from MTA
619#            (e.g. Postfix). If this is not possible (e.g. different recipients
620#            have different tolerances to bad mail contents and not using LMTP)
621#            amavisd-new sends a bounce by itself (same as D_BOUNCE).
622#
623# Notes:
624#   D_REJECT and D_BOUNCE are similar, the difference is in who is responsible
625#            for informing the sender about non-delivery, and how informative
626#            the notification can be (amavisd-new knows more than MTA);
627#   With D_REJECT, MTA may reject original SMTP, or send DSN (delivery status
628#            notification, colloquially called 'bounce') - depending on MTA;
629#            Best suited for sendmail milter, especially for spam.
630#   With D_BOUNCE, amavisd-new (not MTA) sends DSN (can better explain the
631#            reason for mail non-delivery but unable to reject the original
632#            SMTP session, is in position to suppress DSN if considered
633#            unsuitable). Best suited for Postfix and other dual-MTA setups.
634
635$final_virus_destiny      = D_DISCARD; # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
636$final_banned_destiny     = D_BOUNCE;  # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
637$final_spam_destiny       = D_BOUNCE;  # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
638$final_bad_header_destiny = D_PASS;    # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
639
640# If you decide to pass viruses (or spam) to certain users using
641# %virus_lovers/@virus_lovers_acl/$virus_lovers_re, (or *spam_lovers*),
642# %bypass_virus_checks/@bypass_virus_checks_acl, or $final_virus_destiny=D_PASS
643# ($final_spam_destiny=D_PASS), you can set the variable $addr_extension_virus
644# ($addr_extension_spam) to some string, and the recipient address will have
645# this string appended as an address extension to the local-part of the
646# address. This extension can be used by final local delivery agent to place
647# such mail in different folders. Leave these variables undefined or empty
648# strings to prevent appending address extensions. Setting has no effect
649# on users which will not be receiving viruses (spam). Recipients which
650# do not match access lists in @local_domains_maps are not affected (i.e.
651# non-local recipients).
652#
653# LDAs usually default to stripping away address extension if no special
654# handling for it is specified, so having this option enabled normally
655# does no harm, provided the $recipients_delimiter character matches
656# the setting at the final MTA's local delivery agent (LDA).
657#
658# $addr_extension_virus  = 'virus';  # for example
659# $addr_extension_spam   = 'spam';
660# $addr_extension_banned = 'banned';
661# $addr_extension_bad_header = 'badh';
662
663# Delimiter between local part of the recipient address and address extension
664# (which can optionally be added, see variables $addr_extension_virus and
665# $addr_extension_spam). E.g. recipient address <user@domain.example> gets
666# changed to <user+virus@domain.example>.
667#
668# Delimiter should match equivalent (final) MTA delimiter setting.
669# (e.g. for Postfix add 'recipient_delimiter = +' to main.cf).
670# Setting it to an empty string or to undef disables this feature
671# regardless of $addr_extension_virus and $addr_extension_spam settings.
672
673# $recipient_delimiter = '+';
674$replace_existing_extension = 1;   # true: replace ext; false: append ext
675
676# Affects matching of localpart of e-mail addresses (left of '@')
677# in lookups: true = case sensitive, false = case insensitive
678$localpart_is_case_sensitive = 0;
679
680# first match wins, more specific entries should precede general ones!
681# the result may be a string or a ref to a list of strings;
682# see also sub decompose_part()
683$map_full_type_to_short_type_re = Amavis::Lookup::RE->new(
684  [qr/^empty\z/                       => 'empty'],
685  [qr/^directory\z/                   => 'dir'],
686  [qr/^can't (stat|read)\b/           => 'dat'],  # file(1) diagnostics
687  [qr/^cannot open\b/                 => 'dat'],  # file(1) diagnostics
688  [qr/^ERROR: Corrupted\b/            => 'dat'],  # file(1) diagnostics
689  [qr/can't read magic file|couldn't find any magic files/ => 'dat'],
690  [qr/^data\z/                        => 'dat'],
691
692  [qr/^ISO-8859.*\btext\b/            => 'txt'],
693  [qr/^Non-ISO.*ASCII\b.*\btext\b/    => 'txt'],
694  [qr/^Unicode\b.*\btext\b/i          => 'txt'],
695  [qr/^'diff' output text\b/          => 'txt'],
696  [qr/^GNU message catalog\b/         => 'mo'],
697  [qr/^PGP encrypted data\b/          => 'pgp'],
698  [qr/^PGP armored data( signed)? message\b/ => ['pgp','pgp.asc'] ],
699  [qr/^PGP armored\b/                 =>        ['pgp','pgp.asc'] ],
700
701### 'file' is a bit too trigger happy to claim something is 'mail text'
702# [qr/^RFC 822 mail text\b/           => 'mail'],
703  [qr/^(ASCII|smtp|RFC 822) mail text\b/ => 'txt'],
704
705  [qr/^JPEG image data\b/             =>['image','jpg'] ],
706  [qr/^GIF image data\b/              =>['image','gif'] ],
707  [qr/^PNG image data\b/              =>['image','png'] ],
708  [qr/^TIFF image data\b/             =>['image','tif'] ],
709  [qr/^PCX\b.*\bimage data\b/         =>['image','pcx'] ],
710  [qr/^PC bitmap data\b/              =>['image','bmp'] ],
711
712  [qr/^MP2\b/                         =>['audio','mpa','mp2'] ],
713  [qr/^MP3\b/                         =>['audio','mpa','mp3'] ],
714  [qr/^MPEG video stream data\b/      =>['movie','mpv'] ],
715  [qr/^MPEG system stream data\b/     =>['movie','mpg'] ],
716  [qr/^MPEG\b/                        =>['movie','mpg'] ],
717  [qr/^Microsoft ASF\b/               =>['movie','wmv'] ],
718  [qr/^RIFF\b.*\bAVI\b/               =>['movie','avi'] ],
719  [qr/^RIFF\b.*\bWAVE audio\b/        =>['audio','wav'] ],
720
721  [qr/^Macromedia Flash data\b/       => 'swf'],
722  [qr/^HTML document text\b/          => 'html'],
723  [qr/^XML document text\b/           => 'xml'],
724  [qr/^exported SGML document text\b/ => 'sgml'],
725  [qr/^PostScript document text\b/    => 'ps'],
726  [qr/^PDF document\b/                => 'pdf'],
727  [qr/^Rich Text Format data\b/       => 'rtf'],
728  [qr/^Microsoft Office Document\b/i  => 'doc'],  # OLE2: doc, ppt, xls, ...
729  [qr/^LaTeX\b.*\bdocument text\b/    => 'lat'],
730  [qr/^TeX DVI file\b/                => 'dvi'],
731  [qr/\bdocument text\b/              => 'txt'],
732  [qr/^compiled Java class data\b/    => 'java'],
733  [qr/^MS Windows 95 Internet shortcut text\b/ => 'url'],
734
735  [qr/^frozen\b/                      => 'F'],
736  [qr/^gzip compressed\b/             => 'gz'],
737  [qr/^bzip compressed\b/             => 'bz'],
738  [qr/^bzip2 compressed\b/            => 'bz2'],
739  [qr/^lzop compressed\b/             => 'lzo'],
740  [qr/^compress'd/                    => 'Z'],
741  [qr/^Zip archive\b/i                => 'zip'],
742  [qr/^RAR archive\b/i                => 'rar'],
743  [qr/^LHa.*\barchive\b/i             => 'lha'],  # or .lzh
744  [qr/^ARC archive\b/i                => 'arc'],
745  [qr/^ARJ archive\b/i                => 'arj'],
746  [qr/^Zoo archive\b/i                => 'zoo'],
747  [qr/^(\S+\s+)?tar archive\b/i       => 'tar'],
748  [qr/^(\S+\s+)?cpio archive\b/i      => 'cpio'],
749  [qr/^Debian binary package\b/i      => 'deb'], # standard Unix archive (ar)
750  [qr/^current ar archive\b/i         => 'a'],   # standard Unix archive (ar)
751  [qr/^RPM\b/                         => 'rpm'],
752  [qr/^(Transport Neutral Encapsulation Format|TNEF)\b/i => 'tnef'],
753  [qr/^Microsoft cabinet file\b/      => 'cab'],
754
755  [qr/^(uuencoded|xxencoded)\b/i      => 'uue'],
756  [qr/^binhex\b/i                     => 'hqx'],
757  [qr/^(ASCII|text)\b/i               => 'asc'],
758  [qr/^Emacs.*byte-compiled Lisp data/i => 'asc'],  # BinHex with an empty line
759  [qr/\bscript text executable\b/     => 'txt'],
760
761  [qr/^MS-DOS\b.*\bexecutable\b/      => ['exe','exe-ms'] ],
762  [qr/^MS Windows\b.*\bexecutable\b/  => ['exe','exe-ms'] ],
763  [qr/^PA-RISC.*\bexecutable\b/       => ['exe','exe-unix'] ],
764  [qr/^ELF .*\bexecutable\b/          => ['exe','exe-unix'] ],
765  [qr/^COFF format .*\bexecutable\b/  => ['exe','exe-unix'] ],
766  [qr/^executable \(RISC System\b/    => ['exe','exe-unix'] ],
767  [qr/^VMS\b.*\bexecutable\b/         => ['exe','exe-vms'] ],
768
769  [qr/\bexecutable\b/i                => 'exe'],
770  [qr/^MS Windows\b.*\bDLL\b/         => 'dll'],
771  [qr/\bshared object, \b/i           => 'so'],
772  [qr/\brelocatable, \b/i             => 'o'],
773  [qr/\btext\b/i                      => 'asc'],
774  [qr/.*/                             => 'dat'],  # catchall
775
776);
777
778# MS Windows PE 32-bit Intel 80386 GUI executable not relocatable
779# MS-DOS executable (EXE), OS/2 or MS Windows
780# PA-RISC1.1 executable dynamically linked
781# PA-RISC1.1 shared executable dynamically linked
782# ELF 64-bit LSB executable, Alpha (unofficial), version 1 (FreeBSD), for FreeBSD 5.0.1, dynamically linked (uses shared libs), stripped
783# ELF 64-bit LSB executable, Alpha (unofficial), version 1 (SYSV), for GNU/Linux 2.2.5, dynamically linked (uses shared libs), stripped
784# ELF 64-bit MSB executable, SPARC V9, version 1 (FreeBSD), for FreeBSD 5.0, dynamically linked (uses shared libs), stripped
785# ELF 64-bit MSB shared object, SPARC V9, version 1 (FreeBSD), stripped
786# ELF 32-bit LSB executable, Intel 80386, version 1, dynamically`
787# ELF 32-bit MSB executable, SPARC, version 1, dynamically linke`
788# COFF format alpha executable paged stripped - version 3.11-10
789# COFF format alpha executable paged dynamically linked stripped`
790# COFF format alpha demand paged executable or object module stripped - version 3.11-10
791# COFF format alpha paged dynamically linked not stripped shared`
792# executable (RISC System/6000 V3.1) or obj module
793# VMS VAX executable
794
795# Define aliase names in this module to make it simpler to call
796# these routines from amavisd.conf
797*read_text           = \&Amavis::Util::read_text;
798*read_l10n_templates = \&Amavis::Util::read_l10n_templates;
799*read_hash           = \&Amavis::Util::read_hash;
800*ask_daemon          = \&Amavis::AV::ask_daemon;
801*sophos_savi         = \&Amavis::AV::ask_sophos_savi;
802*ask_clamav          = \&Amavis::AV::ask_clamav;
803sub new_RE { Amavis::Lookup::RE->new(@_) }
804
805sub build_default_maps() {
806  @local_domains_maps = (
807    \%local_domains, \@local_domains_acl, \$local_domains_re);
808  @mynetworks_maps = (\@mynetworks);
809  @bypass_virus_checks_maps = (
810    \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);
811  @bypass_spam_checks_maps = (
812    \%bypass_spam_checks, \@bypass_spam_checks_acl, \$bypass_spam_checks_re);
813  @bypass_banned_checks_maps = (
814    \%bypass_banned_checks, \@bypass_banned_checks_acl, \$bypass_banned_checks_re);
815  @bypass_header_checks_maps = (
816    \%bypass_header_checks, \@bypass_header_checks_acl, \$bypass_header_checks_re);
817  @virus_lovers_maps = (
818    \%virus_lovers, \@virus_lovers_acl, \$virus_lovers_re);
819  @spam_lovers_maps = (
820    \%spam_lovers, \@spam_lovers_acl, \$spam_lovers_re);
821  @banned_files_lovers_maps = (
822    \%banned_files_lovers, \@banned_files_lovers_acl, \$banned_files_lovers_re);
823  @bad_header_lovers_maps = (
824    \%bad_header_lovers, \@bad_header_lovers_acl, \$bad_header_lovers_re);
825  @warnvirusrecip_maps  = (\$warnvirusrecip);
826  @warnbannedrecip_maps = (\$warnbannedrecip);
827  @warnbadhrecip_maps   = (\$warnbadhrecip);
828  @newvirus_admin_maps  = (\$newvirus_admin);
829  @virus_admin_maps     = (\%virus_admin, \$virus_admin);
830  @banned_admin_maps    = (\$banned_admin);
831  @bad_header_admin_maps= (\$bad_header_admin);
832  @spam_admin_maps      = (\%spam_admin,  \$spam_admin);
833  @virus_quarantine_to_maps = (\$virus_quarantine_to);
834  @banned_quarantine_to_maps = (\$banned_quarantine_to);
835  @bad_header_quarantine_to_maps = (\$bad_header_quarantine_to);
836  @spam_quarantine_to_maps = (\$spam_quarantine_to);
837  @spam_quarantine_bysender_to_maps = (\$spam_quarantine_bysender_to);
838  @keep_decoded_original_maps = (\$keep_decoded_original_re);
839  @map_full_type_to_short_type_maps = (\$map_full_type_to_short_type_re);
840  @banned_filename_maps = (\$banned_filename_re);
841  @viruses_that_fake_sender_maps = (\$viruses_that_fake_sender_re, 1);
842  @spam_tag_level_maps  = (\$sa_tag_level_deflt);
843  @spam_tag2_level_maps = (\$sa_tag2_level_deflt);
844  @spam_kill_level_maps = (\$sa_kill_level_deflt);
845  @spam_dsn_cutoff_level_maps = (\$sa_dsn_cutoff_level);
846  @spam_modifies_subj_maps = (\$sa_spam_modifies_subj);
847  @spam_subject_tag_maps   = (\$sa_spam_subject_tag1);  # note: inconsistent
848  @spam_subject_tag2_maps  = (\$sa_spam_subject_tag);   # note: inconsistent
849  @whitelist_sender_maps = (
850    \%whitelist_sender, \@whitelist_sender_acl, \$whitelist_sender_re);
851  @blacklist_sender_maps = (
852    \%blacklist_sender, \@blacklist_sender_acl, \$blacklist_sender_re);
853  @score_sender_maps = ();  # new variable, no backwards compatibility needed
854  @message_size_limit_maps = ();  # new variable
855  @addr_extension_virus_maps  = (\$addr_extension_virus);
856  @addr_extension_spam_maps   = (\$addr_extension_spam);
857  @addr_extension_banned_maps = (\$addr_extension_banned);
858  @addr_extension_bad_header_maps = (\$addr_extension_bad_header);
859  @debug_sender_maps = (\@debug_sender_acl);
860}
861
862# prepend a lookup table label object for logging purposes
863sub label_default_maps() {
864  for my $varname (qw(
865    @local_domains_maps @mynetworks_maps
866    @bypass_virus_checks_maps @bypass_spam_checks_maps
867    @bypass_banned_checks_maps @bypass_header_checks_maps
868    @virus_lovers_maps @spam_lovers_maps
869    @banned_files_lovers_maps @bad_header_lovers_maps
870    @warnvirusrecip_maps @warnbannedrecip_maps @warnbadhrecip_maps
871    @newvirus_admin_maps @virus_admin_maps
872    @banned_admin_maps @bad_header_admin_maps @spam_admin_maps
873    @virus_quarantine_to_maps
874    @banned_quarantine_to_maps @bad_header_quarantine_to_maps
875    @spam_quarantine_to_maps @spam_quarantine_bysender_to_maps
876    @keep_decoded_original_maps @map_full_type_to_short_type_maps
877    @banned_filename_maps @viruses_that_fake_sender_maps
878    @spam_tag_level_maps @spam_tag2_level_maps @spam_kill_level_maps
879    @spam_dsn_cutoff_level_maps @spam_modifies_subj_maps
880    @spam_subject_tag_maps @spam_subject_tag2_maps
881    @whitelist_sender_maps @blacklist_sender_maps @score_sender_maps
882    @message_size_limit_maps
883    @addr_extension_virus_maps @addr_extension_spam_maps
884    @addr_extension_banned_maps @addr_extension_bad_header_maps
885    @debug_sender_maps ))
886  {
887    my($g) = $varname; $g =~ s{\@}{Amavis::Conf::};  # qualified variable name
888    my($label) = $varname; $label=~s/^\@//; $label=~s/_maps$//;
889    { no strict 'refs';
890      unshift(@$g,  # NOTE: a symbolic reference
891              Amavis::Lookup::Label->new($label))  if @$g;  # no label if empty
892    }
893  }
894}
895
896# read and evaluate configuration files (one or more)
897sub read_config(@) {
898  my(@config_files) = @_;
899  for my $config_file (@config_files) {
900    my($msg);
901    my($errn) = stat($config_file) ? 0 : 0+$!;
902    if    ($errn == ENOENT) { $msg = "does not exist" }
903    elsif ($errn)      { $msg = "is inaccessible: $!" }
904    elsif (-d _)       { $msg = "is a directory" }
905    elsif (!-f _)      { $msg = "is not a regular file" }
906    elsif ($> && -o _) { $msg = "is owned by EUID $>, should be owned by root"}
907    elsif ($> && -w _) { $msg = "is writable by EUID $>, EGID $)" }
908    if (defined $msg)  { die "Config file \"$config_file\" $msg," }
909    $! = undef; my($rv) = do $config_file;
910    if (!defined($rv)) {
911      if ($@ ne '') { die "Error in config file \"$config_file\": $@" }
912      else          { die "Error reading config file \"$config_file\": $!" }
913    }
914  }
915  $daemon_chroot_dir = ''  if !defined $daemon_chroot_dir;  # avoids warnings
916  # some sensible defaults for essential settings
917  $TEMPBASE     = $MYHOME                   if !defined $TEMPBASE;
918  $helpers_home = $MYHOME                   if !defined $helpers_home;
919  $db_home      = "$MYHOME/db"              if !defined $db_home;
920  $lock_file    = "$MYHOME/amavisd.lock"    if !defined $lock_file;
921  $pid_file     = "$MYHOME/amavisd.pid"     if !defined $pid_file;
922
923  $X_HEADER_TAG = 'X-Virus-Scanned'               if !defined $X_HEADER_TAG;
924  $X_HEADER_LINE= "$myproduct_name at $mydomain"  if !defined $X_HEADER_LINE;
925  #my($pname)= "$pname" if !defined $pname;
926    # $pname = "Content filter" if !defined $pname;
927     #my($pname) = "\"Content-filter at $myhostname\"" if !defined $CONTENT;
928  $hdrfrom_notify_sender = "<postmaster\@$myhostname>"
929    if !defined $hdrfrom_notify_sender;
930  $hdrfrom_notify_recip = $mailfrom_notify_recip ne ''
931    ? "<$mailfrom_notify_recip>"
932    : $hdrfrom_notify_sender  if !defined $hdrfrom_notify_recip;
933  $hdrfrom_notify_admin = $mailfrom_notify_admin ne ''
934    ? "<$mailfrom_notify_admin>"
935    : $hdrfrom_notify_sender  if !defined $hdrfrom_notify_admin;
936  $hdrfrom_notify_spamadmin = $mailfrom_notify_spamadmin ne ''
937    ? "<$mailfrom_notify_spamadmin>"
938    : $hdrfrom_notify_sender  if !defined $hdrfrom_notify_spamadmin;
939
940  # compatibility with deprecated $warn*sender and old *_destiny values
941  # map old values <0, =0, >0 into D_REJECT/D_BOUNCE, D_DISCARD, D_PASS
942  for ($final_virus_destiny, $final_banned_destiny, $final_spam_destiny) {
943    if ($_ > 0) { $_ = D_PASS }
944    elsif ($_ < 0 && $_ != D_BOUNCE && $_ != D_REJECT) {  # compatibility
945      # favour Reject with sendmail milter, Bounce with others
946      $_ = c('forward_method') eq '' ? D_REJECT : D_BOUNCE;
947    }
948  }
949  if ($final_virus_destiny == D_DISCARD && c('warnvirussender') )
950    { $final_virus_destiny = D_BOUNCE }
951  if ($final_spam_destiny == D_DISCARD && c('warnspamsender') )
952    { $final_spam_destiny = D_BOUNCE }
953  if ($final_banned_destiny == D_DISCARD && c('warnbannedsender') )
954    { $final_banned_destiny = D_BOUNCE }
955  if ($final_bad_header_destiny == D_DISCARD && c('warnbadhsender') )
956    { $final_bad_header_destiny = D_BOUNCE }
957}
958
9591;
960
961#
962package Amavis::Lock;
963use strict;
964use re 'taint';
965
966BEGIN {
967  use Exporter ();
968  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
969  $VERSION = '2.034';
970  @ISA = qw(Exporter);
971  @EXPORT = qw(&lock &unlock);
972}
973use Fcntl qw(LOCK_SH LOCK_EX LOCK_UN);
974
975use subs @EXPORT;
976
977sub lock($) {
978  my($file_handle) = @_;
979  flock($file_handle, LOCK_EX) or die "Can't lock $file_handle: $!";
980  # NOTE: a lock is on a file, not on a file handle
981}
982
983sub unlock($) {
984  my($file_handle) = @_;
985  flock($file_handle, LOCK_UN) or die "Can't unlock $file_handle: $!";
986}
987
9881;
989
990#
991package Amavis::Log;
992use strict;
993use re 'taint';
994
995BEGIN {
996  use Exporter ();
997  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
998  $VERSION = '2.034';
999  @ISA = qw(Exporter);
1000  %EXPORT_TAGS = ();
1001  @EXPORT = ();
1002  @EXPORT_OK = qw(&init &write_log);
1003}
1004use subs @EXPORT_OK;
1005
1006use POSIX qw(locale_h strftime);
1007use Unix::Syslog qw(:macros :subs);
1008use IO::File ();
1009use File::Basename;
1010
1011BEGIN {
1012  import Amavis::Conf qw(:platform $myversion $myhostname $daemon_user);
1013  import Amavis::Lock;
1014}
1015
1016use vars qw($loghandle);  # log file handle
1017use vars qw($myname);
1018use vars qw($syslog_facility $syslog_priority %syslog_priority);
1019use vars qw($log_to_stderr $do_syslog $logfile);
1020
1021sub init($$$$$) {
1022  my($ident, $syslog_level);
1023  ($ident, $log_to_stderr, $do_syslog, $syslog_level, $logfile) = @_;
1024
1025  # Avoid taint bug in some versions of Perl (likely in 5.004, 5.005).
1026  # The 5.6.1 is fine. To test, run this one-liner:
1027  #   perl -Te '"$0 $$"; $r=$$; print eval{kill(0,$$);1}?"OK\n":"BUG\n"'
1028  $myname = $0;
1029# $myname = $1  if basename($0) =~ /^(.*)\z/;
1030
1031  if ($syslog_level =~ /^\s*([a-z0-9]+)\.([a-z0-9]+)\s*\z/i) {
1032    $syslog_facility = eval("LOG_\U$1");
1033    $syslog_priority = eval("LOG_\U$2");
1034  }
1035  $syslog_facility = LOG_DAEMON   if $syslog_facility !~ /^\d+\z/;
1036  $syslog_priority = LOG_WARNING  if $syslog_priority !~ /^\d+\z/;
1037  if ($do_syslog) {
1038    openlog($ident, LOG_PID, $syslog_facility);
1039  } elsif ($logfile eq '') {
1040    die 'No $LOGFILE is specified (and not logging via syslog)';
1041  } else {
1042    $loghandle = IO::File->new($logfile,'>>')
1043      or die "Failed to open log file $logfile: $!";
1044    $loghandle->autoflush(1);
1045    my($uid) = $daemon_user=~/^(\d+)$/ ? $1 : (getpwnam($daemon_user))[2];
1046    if ($> == 0 && $uid) {
1047      chown($uid,-1,$logfile)
1048        or die "Can't chown logfile $logfile to $uid: $!";
1049    }
1050  }
1051  my($msg) = "starting.  $myname at $myhostname $myversion";
1052  $msg .= ", eol=\"$eol\""            if $eol ne "\n";
1053  $msg .= ", Unicode aware"           if $unicode_aware;
1054  $msg .= ", LC_ALL=$ENV{LC_ALL}"     if $ENV{LC_ALL}   ne '';
1055  $msg .= ", LC_TYPE=$ENV{LC_TYPE}"   if $ENV{LC_TYPE}  ne '';
1056  $msg .= ", LC_CTYPE=$ENV{LC_CTYPE}" if $ENV{LC_CTYPE} ne '';
1057  $msg .= ", LANG=$ENV{LANG}"         if $ENV{LANG}     ne '';
1058  write_log(0, $msg, undef);
1059}
1060
1061# Log either to syslog or a file
1062sub write_log($$$) {
1063  my($level,$errmsg,$am_id) = @_;
1064
1065  my($old_locale) = setlocale(LC_TIME,"C");  # English dates required!
1066  my($really_log_to_stderr) = $log_to_stderr || (!$do_syslog && !$loghandle);
1067  my($prefix) = '';
1068  if ($really_log_to_stderr || !$do_syslog) {  # create syslog-like prefix
1069    $prefix = sprintf("%s %s %s[%s]: ",
1070              strftime("%b %e %H:%M:%S", localtime), $myhostname, $myname, $$);
1071  }
1072  $am_id = !defined $am_id ? '' : "($am_id) ";
1073  $errmsg = Amavis::Util::sanitize_str($errmsg);
1074# if (length($errmsg) > 2000) {  # crop at some arbitrary limit (< LINE_MAX)
1075#   $errmsg = substr($errmsg,0,2000) . "...";
1076# }
1077  if ($really_log_to_stderr) {
1078    print STDERR $prefix, $am_id, $errmsg, $eol;
1079  } elsif ($do_syslog) {
1080    my($prio) = $syslog_priority;  # never go below this priority level
1081    # syslog priorities: DEBUG, INFO, NOTICE, WARNING, ERR, CRIT, ALERT, EMERG
1082    if    ($level <= -3) { $prio = LOG_CRIT    if $prio > LOG_CRIT    }
1083    elsif ($level <= -2) { $prio = LOG_ERR     if $prio > LOG_ERR     }
1084    elsif ($level <= -1) { $prio = LOG_WARNING if $prio > LOG_WARNING }
1085    elsif ($level <=  0) { $prio = LOG_NOTICE  if $prio > LOG_NOTICE  }
1086    elsif ($level <=  2) { $prio = LOG_INFO    if $prio > LOG_INFO    }
1087    else                 { $prio = LOG_DEBUG   if $prio > LOG_DEBUG   }
1088    my($pre) = '';
1089    my($logline_size) = 980;  # less than  (1023 - prefix)
1090    while (length($am_id . $pre . $errmsg) > $logline_size) {
1091      my($avail) = $logline_size - length($am_id . $pre . "...");
1092      syslog($prio, "%s", $am_id . $pre . substr($errmsg,0,$avail) . "...");
1093      $pre = "...";
1094      $errmsg = substr($errmsg, $avail);
1095    }
1096    syslog($prio, "%s", $am_id . $pre . $errmsg);
1097  } else {
1098    lock($loghandle);
1099    seek($loghandle,0,2) or die "Can't position log file to its tail: $!";
1100    print $loghandle $prefix, $am_id, $errmsg, $eol;
1101    unlock($loghandle);
1102  }
1103  setlocale(LC_TIME, $old_locale);
1104}
1105
11061;
1107
1108#
1109package Amavis::Timing;
1110use strict;
1111use re 'taint';
1112
1113BEGIN {
1114  use Exporter ();
1115  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
1116  $VERSION = '2.034';
1117  @ISA = qw(Exporter);
1118  %EXPORT_TAGS = ();
1119  @EXPORT = ();
1120  @EXPORT_OK = qw(&init &section_time &report &get_time_so_far);
1121}
1122use subs @EXPORT_OK;
1123
1124use Time::HiRes ();
1125
1126use vars qw(@timing);
1127
1128# clear array @timing and enter start time
1129sub init() {
1130  @timing = (); section_time('init');
1131}
1132
1133# enter current time reading into array @timing
1134sub section_time($) {
1135  push(@timing,shift,Time::HiRes::time);
1136}
1137
1138# returns a string - a report of elapsed time by section
1139sub report() {
1140  section_time('rundown');
1141  my($notneeded, $t0) = (shift(@timing), shift(@timing));
1142  my($total) = $t0 <= 0 ? 0 : $timing[$#timing] - $t0;
1143  if ($total < 0.0000001) { $total = 0.0000001 }
1144  my(@sections);
1145  while (@timing) {
1146    my($section, $t) = (shift(@timing), shift(@timing));
1147    my($dt) = $t-$t0;
1148    $dt = 0  if $dt < 0;  # just in case (clock jumps)
1149    my($dtp) = $dt > $total ? 100 : $dt*100.0/$total;
1150    push(@sections, sprintf("%s: %.0f (%.0f%%)", $section, $dt*1000, $dtp));
1151    $t0 = $t;
1152  }
1153  sprintf("TIMING [total %.0f ms] - %s", $total * 1000, join(", ",@sections));
1154}
1155
1156# returns value in seconds of elapsed time for processing of this mail so far
1157sub get_time_so_far() {
1158  my($notneeded, $t0) = @timing;
1159  my($total) = $t0 <= 0 ? 0 : Time::HiRes::time - $t0;
1160  $total < 0 ? 0 : $total;
1161}
1162
1163use vars qw($t_was_busy $t_busy_cum $t_idle_cum $t0);
1164
1165sub idle_proc(@) {
1166  my($t1) = Time::HiRes::time;
1167  if (defined $t0) {
1168    ($t_was_busy ? $t_busy_cum : $t_idle_cum) += $t1 - $t0;
1169    Amavis::Util::ll(5) && Amavis::Util::do_log(5,
1170      sprintf("idle_proc, @_: was %s, %.1f ms, total idle %.3f s, busy %.3f s",
1171        $t_was_busy ? "busy" : "idle", 1000 * ($t1 - $t0),
1172        $t_idle_cum, $t_busy_cum));
1173  }
1174  $t0 = $t1;
1175}
1176
1177sub go_idle(@) {
1178  if ($t_was_busy) { idle_proc(@_); $t_was_busy = 0 }
1179}
1180
1181sub go_busy(@) {
1182  if (!$t_was_busy) { idle_proc(@_); $t_was_busy = 1 }
1183}
1184
1185sub report_load() {
1186  return  if $t_busy_cum + $t_idle_cum <= 0;
1187  Amavis::Util::do_log(3, sprintf(
1188     "load: %.0f %%, total idle %.3f s, busy %.3f s",
1189     100*$t_busy_cum / ($t_busy_cum + $t_idle_cum), $t_idle_cum, $t_busy_cum));
1190}
1191
11921;
1193
1194#
1195package Amavis::Util;
1196use strict;
1197use re 'taint';
1198
1199BEGIN {
1200  use Exporter ();
1201  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
1202  $VERSION = '2.034';
1203  @ISA = qw(Exporter);
1204  %EXPORT_TAGS = ();
1205  @EXPORT = ();
1206  @EXPORT_OK = qw(&untaint &min &max &safe_encode &q_encode
1207                  &snmp_count &snmp_counters_init &snmp_counters_get
1208                  &am_id &new_am_id &ll &do_log &debug_oneshot
1209                  &retcode &exit_status_str &prolong_timer &sanitize_str
1210                  &strip_tempdir &rmdir_recursively
1211                  &read_text &read_l10n_templates &read_hash
1212                  &run_command &run_command_consumer);
1213}
1214use subs @EXPORT_OK;
1215use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
1216             WEXITSTATUS WTERMSIG WSTOPSIG);
1217use Errno qw(ENOENT EACCES);
1218use Digest::MD5;
1219# use Encode;  # Perl 5.8  UTF-8 support
1220# use Encode::CN;  # example: explicitly load Chinese module
1221
1222BEGIN {
1223  import Amavis::Conf qw(:platform $DEBUG c cr ca);
1224  import Amavis::Log qw(write_log);
1225  import Amavis::Timing qw(section_time);
1226}
1227
1228# Return untainted copy of a string (argument can be a string or a string ref)
1229sub untaint($) {
1230  no re 'taint';
1231  my($str);
1232  local($1);  # avoid Perl taint bug: tainted global $1 propagates taintedness
1233  $str = $1  if (ref($_[0]) ? ${$_[0]} : $_[0]) =~ /^(.*)\z/s;
1234  $str;
1235}
1236
1237# Returns the smallest number from the list, or undef
1238sub min(@) {
1239  my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_;  # accept list, or a list ref
1240  my($m);  for (@$r) { $m = $_  if defined $_ && (!defined $m || $_ < $m) }
1241  $m;
1242}
1243
1244# Returns the largest number from the list, or undef
1245sub max(@) {
1246  my($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_;  # accept list, or a list ref
1247  my($m);  for (@$r) { $m = $_  if defined $_ && (!defined $m || $_ > $m) }
1248  $m;
1249}
1250
1251# A wrapper for Encode::encode, avoiding a bug in Perl 5.8.0 which causes
1252# Encode::encode to loop and fill memory when given a tainted string
1253sub safe_encode($$;$) {
1254  if (!$unicode_aware) { $_[1] }  # just return the second argument
1255  else {
1256    my($encoding,$str,$check) = @_;
1257    $check = 0  if !defined($check);
1258    # taintedness of the string, with UTF-8 flag unconditionally off
1259    my($taint) = Encode::encode('ascii',substr($str,0,0));
1260    $taint . Encode::encode($encoding,untaint($str),$check);  # preserve taint
1261  }
1262}
1263
1264# Do the Q-encoding manually, the MIME::Words::encode_mimeword does not
1265# encode spaces and does not limit to 75 ch, which violates the RFC 2047
1266sub q_encode($$$) {
1267  my($octets,$encoding,$charset) = @_;
1268  my($prefix) = '=?' . $charset . '?' . $encoding . '?';
1269  my($suffix) = '?='; local($1,$2,$3);
1270  # FWS | utext (= NO-WS-CTL|rest of US-ASCII)
1271  $octets =~ /^ ( [\001-\011\013\014\016-\177]* [ \t] )?  (.*?)
1272                ( [ \t] [\001-\011\013\014\016-\177]* )? \z/sx;
1273  my($head,$rest,$tail) = ($1,$2,$3);
1274  # Q-encode $rest according to RFC 2047
1275  # more restricted than =?_ so that it may be used in 'phrase'
1276  $rest =~ s{([^ 0-9a-zA-Z!*/+-])}{sprintf('=%02X',ord($1))}egs;
1277  $rest =~ tr/ /_/;   # turn spaces into _ (rfc2047 allows it)
1278  my($s) = $head; my($len) = 75 - (length($prefix)+length($suffix)) - 2;
1279  while ($rest ne '') {
1280    $s .= ' '  if $s !~ /[ \t]\z/;  # encoded words must be separated by FWS
1281    $rest =~ /^ ( .{0,$len} [^=] (?: [^=] | \z ) ) (.*) \z/sx;
1282    $s .= $prefix.$1.$suffix; $rest = $2;
1283  }
1284  $s.$tail;
1285}
1286
1287# Set or get Amavis internal message id.
1288# This message id performs a similar function to queue-id in MTA responses.
1289# It may only be used in generating text part of SMTP responses,
1290# or in generating log entries.
1291use vars qw($amavis_task_id);  # internal message id (accessible via &am_id)
1292
1293sub am_id(;$) {
1294  if (@_) {                    # set, if argument present
1295    $amavis_task_id = shift;
1296    $0 = "amavisd ($amavis_task_id)";
1297  }
1298  $amavis_task_id;             # return current value
1299}
1300
1301sub new_am_id($;$$) {
1302  my($str, $cnt, $seq) = @_;
1303  my($id);
1304# my($ctx) = Digest::MD5->new; # 128 bits (32 hex digits)
1305# $ctx->add($str.$$);
1306# $id = substr($ctx->hexdigest, 0, 6);
1307  $id = defined $str ? $str : sprintf("%05d", $$);
1308  $id .= sprintf("-%02d", $cnt)  if defined $cnt;
1309  $id .= "-$seq"  if $seq > 1;
1310  am_id($id);
1311}
1312
1313use vars qw(@counter_names);
1314# elements may be counter names (increment is 1), or pairs: [name,increment]
1315sub snmp_counters_init() { @counter_names = () }
1316sub snmp_count(@) { push(@counter_names, @_) }
1317sub snmp_counters_get() { \@counter_names }
1318
1319use vars qw($debug_oneshot);
1320sub debug_oneshot(;$$) {
1321  if (@_) {
1322    my($new_debug_oneshot) = shift;
1323    if (($new_debug_oneshot ? 1 : 0) != ($debug_oneshot ? 1 : 0)) {
1324      do_log(0, "DEBUG_ONESHOT: TURNED ".($new_debug_oneshot ? "ON" : "OFF"));
1325      do_log(0, shift)  if @_;  # caller-provided extra log entry, usually
1326                                # the one that caused debug_oneshot call
1327    }
1328    $debug_oneshot = $new_debug_oneshot;
1329  }
1330  $debug_oneshot;
1331}
1332
1333# is a message log level below the current log level?
1334sub ll($) {
1335  my($level) = @_;
1336  $level = 0  if $level > 0 && ($DEBUG || $debug_oneshot);
1337  $level <= c('log_level');
1338}
1339
1340# write log entry
1341sub do_log($$) {
1342  my($level, $errmsg) = @_;
1343  if (ll($level)) {
1344    $level = 0  if $level > 0 && ($DEBUG || $debug_oneshot);
1345    write_log($level, $errmsg, am_id());
1346  }
1347}
1348
1349sub retcode($) {  # (this subroutine is being phased out)
1350  my $code = shift;
1351  return WEXITSTATUS($code)    if WIFEXITED($code);
1352  return 128 + WTERMSIG($code) if WIFSIGNALED($code);
1353  return 255;
1354}
1355
1356sub exit_status_str($;$) {
1357  my($stat,$err) = @_; my($str);
1358  if (WIFEXITED($stat)) {
1359    $str = sprintf("exit %d", WEXITSTATUS($stat));
1360  } elsif (WIFSTOPPED($stat)) {
1361    $str = sprintf("stopped, signal %d", WSTOPSIG($stat));
1362  } else {
1363    $str = sprintf("DIED on signal %d (%04x)", WTERMSIG($stat),$stat);
1364  }
1365  $str .= ', '.$err  if $err ne '';
1366  $str;
1367}
1368
1369sub prolong_timer($;$) {
1370  my($which_section, $child_remaining_time) = @_;
1371  if (!defined($child_remaining_time)) {
1372    $child_remaining_time = alarm(0);  # check how much time is left
1373  }
1374  do_log(4, "prolong_timer after $which_section: "
1375            . "remaining time = $child_remaining_time s");
1376  $child_remaining_time = 60  if $child_remaining_time < 60;
1377  alarm($child_remaining_time);        # restart/prolong the timer
1378}
1379
1380# Mostly for debugging and reporting purposes:
1381# Convert nonprintable characters in the argument
1382# to \[rnftbe], or \octal code, and '\' to '\\',
1383# and Unicode characters to \x{xxxx}, returning the sanitized string.
1384sub sanitize_str {
1385  my($str, $keep_eol) = @_;
1386  my(%map) = ("\r" => '\\r', "\n" => '\\n', "\f" => '\\f', "\t" => '\\t',
1387              "\b" => '\\b', "\e" => '\\e', "\\" => '\\\\');
1388  if ($keep_eol) {
1389    $str =~ s/([^\012\040-\133\135-\176])/  # and \240-\376 ?
1390              exists($map{$1}) ? $map{$1} :
1391                     sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg;
1392  } else {
1393    $str =~ s/([^\040-\133\135-\176])/      # and \240-\376 ?
1394              exists($map{$1}) ? $map{$1} :
1395                     sprintf(ord($1)>255 ? '\\x{%04x}' : '\\%03o', ord($1))/eg;
1396  }
1397  $str;
1398}
1399
1400# Checks tempdir after being cleaned.
1401# It may only contain subdirectory 'parts' and file email.txt, nothing else.
1402#
1403sub check_tempdir($) {
1404  my($dir) = shift;
1405  local(*DIR); my($f);
1406  opendir(DIR,$dir) or die "Can't open directory $dir: $!";
1407  while (defined($f = readdir(DIR))) {
1408    if (!-d ("$dir/$f")) {
1409      die "Unexpected file $dir/$f"  if $f ne 'email.txt';
1410    } elsif ($f eq '.' || $f eq '..' || $f eq 'parts') {
1411    } else {
1412      die "Unexpected subdirectory $dir/$f";
1413    }
1414  }
1415  closedir(DIR) or die "Can't close directory $dir: $!";
1416  1;
1417}
1418
1419# Remove all files and subdirectories from the temporary directory, leaving
1420# only the directory itself, file email.txt, and empty subdirectory ./parts .
1421# Leaving directories for reuse represents an important saving in time,
1422# as directory creation + deletion is quite an expensive operation,
1423# requiring atomic file system operation, including flushing buffers to disk.
1424#
1425sub strip_tempdir($) {
1426  my($dir) = shift;
1427  do_log(4, "strip_tempdir: $dir");
1428  my($errn) = lstat("$dir/parts") ? 0 : 0+$!;
1429  if ($errn != ENOENT) {
1430    if    ( -l _) { die "strip_tempdir: $dir/parts is a symbolic link" }
1431    elsif (!-d _) { die "strip_tempdir: $dir/parts is not a directory" }
1432    rmdir_recursively("$dir/parts", 1);
1433  }
1434  # All done. Check for any remains in the top directory just in case
1435  check_tempdir($dir);
1436  1;
1437}
1438
1439#
1440# Removes a directory, along with its contents
1441sub rmdir_recursively($;$);  # prototype
1442sub rmdir_recursively($;$) {
1443  my($dir, $exclude_itself) = @_;
1444  local(*DIR); my($errn); my($cnt) = 0; my($dir_is_open) = 0;
1445  do_log(4,"rmdir_recursively: $dir, excl=$exclude_itself");
1446  eval {
1447    $errn = opendir(DIR,$dir) ? 0 : 0+$!;
1448    if ($errn == ENOENT) { die "Directory $dir does not exist," }
1449    elsif ($errn == EACCES) {  # relax protection on directory, then try again
1450      do_log(3,"rmdir_recursively: enabling read access to directory $dir");
1451      chmod(0750,$dir) or die "Can't change protection-1 on dir $dir: $!";
1452      $errn = opendir(DIR,$dir) ? 0 : 0+$!;  # try again
1453    }
1454    if ($errn) { die "Can't open directory $dir: $!" }
1455    $dir_is_open = 1; my($f);
1456    while (defined($f = readdir(DIR))) {
1457      my($fname) = "$dir/$f";
1458      $errn = lstat($fname) ? 0 : 0+$!;
1459      if ($errn == ENOENT) { die "File \"$fname\" does not exist," }
1460      elsif ($errn == EACCES) {  # relax protection on the directory and retry
1461        do_log(3,"rmdir_recursively: enabling access to files in dir $dir");
1462        chmod(0750,$dir) or die "Can't change protection-2 on dir $dir: $!";
1463        $errn = lstat($fname) ? 0 : 0+$!;  # try again
1464      }
1465      if ($errn) { die "File \"$fname\" inaccessible: $!" }
1466      next  if ($f eq '.' || $f eq '..') && -d _;
1467      if (-d _) { rmdir_recursively(untaint($fname), 0) }
1468      else {
1469        $cnt++;
1470        if (unlink(untaint($fname))) {  # ok
1471        } else {  # relax protection on the directory, then try again
1472          do_log(3,"rmdir_recursively: enabling write access to dir $dir");
1473          my($what) = -l _ ? 'symlink' :-d _ ? 'directory' :'non-regular file';
1474          chmod(0750,$dir) or die "Can't change protection-3 on dir $dir: $!";
1475          unlink(untaint($fname)) or die "Can't remove $what $fname: $!";
1476        }
1477      }
1478    }
1479  };
1480  if ($@ eq '') {
1481    closedir(DIR) or die "Can't close directory $dir: $!"  if $dir_is_open;
1482  } else {
1483    closedir(DIR)  if $dir_is_open;  # ignoring status
1484    die "rmdir_recursively: $@\n";
1485  }
1486  section_time("unlink-$cnt-files");
1487  if (!$exclude_itself) {
1488    rmdir($dir) or die "rmdir_recursively: Can't remove directory $dir: $!";
1489    section_time('rmdir');
1490  }
1491  1;
1492}
1493
1494# read a multiline string from file - may be called from amavisd.conf
1495sub read_text($;$) {
1496  my($filename, $encoding) = @_;
1497  my($inp) = IO::File->new;
1498  $inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
1499  if ($unicode_aware && $encoding ne '') {
1500    binmode($inp, ":encoding($encoding)")
1501      or die "Can't set :encoding($encoding) on file $filename: $!";
1502  }
1503  my($str) = '';  # must not be undef, work around a Perl UTF8 bug
1504  while (<$inp>) { $str .= $_ }
1505  $inp->close or die "Can't close $filename: $!";
1506  $str;
1507}
1508
1509# attempt to read all user-visible replies from a l10n dir
1510# This function auto-fills $notify_sender_templ, $notify_virus_sender_templ,
1511# $notify_virus_admin_templ, $notify_virus_recips_templ,
1512# $notify_spam_sender_templ and $notify_spam_admin_templ from files named
1513# template-dsn.txt, template-virus-sender.txt, template-virus-admin.txt,
1514# template-virus-recipient.txt, template-spam-sender.txt,
1515# template-spam-admin.txt.  If this is available, it uses the charset
1516# file to do automatic charset conversion. Used by the Debian distribution.
1517sub read_l10n_templates($;$) {
1518  my($dir) = @_;
1519  if (@_ > 1)  # compatibility with Debian
1520    { my($l10nlang, $l10nbase) = @_; $dir = "$l10nbase/$l10nlang" }
1521  my($file_chset) = Amavis::Util::read_text("$dir/charset");
1522  if ($file_chset =~ m{^(?:#[^\n]*\n)*([^./\n\s]+)(\s*[#\n].*)?$}s) {
1523    $file_chset = untaint($1);
1524  } else {
1525    die "Invalid charset $file_chset\n";
1526  }
1527  $Amavis::Conf::notify_sender_templ =
1528    Amavis::Util::read_text("$dir/template-dsn.txt", $file_chset);
1529  $Amavis::Conf::notify_virus_sender_templ =
1530    Amavis::Util::read_text("$dir/template-virus-sender.txt", $file_chset);
1531  $Amavis::Conf::notify_virus_admin_templ =
1532    Amavis::Util::read_text("$dir/template-virus-admin.txt", $file_chset);
1533  $Amavis::Conf::notify_virus_recips_templ =
1534    Amavis::Util::read_text("$dir/template-virus-recipient.txt", $file_chset);
1535  $Amavis::Conf::notify_spam_sender_templ =
1536    Amavis::Util::read_text("$dir/template-spam-sender.txt", $file_chset);
1537  $Amavis::Conf::notify_spam_admin_templ =
1538    Amavis::Util::read_text("$dir/template-spam-admin.txt", $file_chset);
1539}
1540
1541#use CDB_File;
1542#sub tie_hash($$) {
1543# my($hashref, $filename) = @_;
1544# CDB_File::create(%$hashref, $filename, "$filename.tmp$$")
1545#   or die "Can't create cdb $filename: $!";
1546# my($cdb) = tie(%$hashref,'CDB_File',$filename)
1547#   or die "Tie to $filename failed: $!";
1548# $hashref;
1549#}
1550
1551# read a lookup hash from file - may be called from amavisd.conf .
1552#
1553# Format: one key per line, anything from '#' to the end of line
1554# is considered a comment, but '#' within correctly quoted rfc2821
1555# addresses is not treated as a comment (e.g. a hash sign within
1556# "strange # \"foo\" address"@example.com is part of the string).
1557# Lines may contain a pair: key value, separated by whitespace, or key only,
1558# in which case a value 1 is implied. Trailing whitespace is discarded,
1559# empty lines (containing only whitespace and comment) are ignored.
1560# Addresses (lefthand-side) are converted from rfc2821-quoted form
1561# into internal (raw) form and inserted as keys into a given hash.
1562# NOTE: the format is partly compatible with Postfix maps (not aliases):
1563#   no continuation lines are honoured, Postfix maps do not allow
1564#   rfc2821-quoted addresses containing whitespace, Postfix only allow
1565#   comments starting at the beginning of the line.
1566#
1567# The $hashref argument is returned for convenience, so that one can do
1568# for example:
1569#   $per_recip_whitelist_sender_lookup_tables = {
1570#     '.my1.example.com' => read_hash({},'/var/amavis/my1-example-com.wl'),
1571#     '.my2.example.com' => read_hash({},'/var/amavis/my2-example-com.wl') }
1572# or even simpler:
1573#   $per_recip_whitelist_sender_lookup_tables = {
1574#     '.my1.example.com' => read_hash('/var/amavis/my1-example-com.wl'),
1575#     '.my2.example.com' => read_hash('/var/amavis/my2-example-com.wl') }
1576#
1577sub read_hash(@) {
1578  unshift(@_,{})  if !ref $_[0];  # first argument is optional, defaults to {}
1579  my($hashref, $filename, $keep_case) = @_;
1580  my($lpcs) = c('localpart_is_case_sensitive');
1581  my($inp) = IO::File->new;
1582  $inp->open($filename,'<') or die "Can't open file $filename for reading: $!";
1583  while (<$inp>) {  # carefully handle comments, '#' within "" does not count
1584    chomp;
1585    my($lhs) = ''; my($rhs) = ''; my($at_rhs) = 0;
1586    for my $t ( /\G ( " (?: \\. | [^"\\] )* " |
1587                      [^#" \t]+ | [ \t]+ | . )/gcsx) {
1588      last  if $t eq '#';
1589      if (!$at_rhs && $t =~ /^[ \t]+\z/) { $at_rhs = 1 }
1590      else { ($at_rhs ? $rhs : $lhs) .= $t }
1591    }
1592    $rhs =~ s/[ \t]+\z//;  # trim trailing whitespace
1593    next  if $lhs eq '' && $rhs eq '';
1594    my($addr) = Amavis::rfc2821_2822_Tools::unquote_rfc2821_local($lhs);
1595    my($localpart,$domain) = Amavis::rfc2821_2822_Tools::split_address($addr);
1596    $localpart = lc($localpart)  if !$lpcs;
1597    $addr = $localpart . lc($domain);
1598    $hashref->{$addr} = $rhs eq '' ? 1 : $rhs;
1599    # do_log(5, "read_hash: address: <$addr>: ".$hashref->{$addr});
1600  }
1601  $inp->close or die "Can't close $filename: $!";
1602  $hashref;
1603}
1604
1605# Run specified command as a subprocess (like qx operator, but more careful
1606# with error reporting and cancels :utf8 mode). Return a file handle open
1607# for reading from the subprocess. Use IO::Handle to ensure the subprocess
1608# will be automatically reclaimed in case of failure.
1609#
1610sub run_command($$@) {
1611  my($stdin_from, $stderr_to, $cmd, @args) = @_;
1612  my($cmd_text) = join(' ', $cmd, @args);
1613  $stdin_from = '/dev/null'  if $stdin_from eq '';
1614  my($msg) = join(' ', $cmd, @args, "<$stdin_from");
1615  $msg .= " 2>$stderr_to"  if $stderr_to ne '';
1616  my($pid); my($proc_fh) = IO::File->new;
1617  eval { $pid = $proc_fh->open('-|') };  # fork, catching errors
1618  if ($@ ne '') { chomp($@); die "run_command (open pipe): $@" }
1619  defined($pid) or die "run_command: can't fork: $!";
1620  if (!$pid) {                           # child
1621    eval {  # must not use die in forked process, or we end up with
1622            # two running daemons! Close unneeded files.
1623#     use Devel::Symdump ();
1624#     my($dumpobj) = Devel::Symdump->rnew;
1625#     for my $k ($dumpobj->ios) {
1626#       no strict 'refs';
1627#       my($fn) = fileno($k);
1628#       if ($fn == 1 || $fn == 2) {
1629#         do_log(2,sprintf("KEEPING %s, fileno=%s", $k, $fn));
1630#       } else {
1631#         $! = undef; close(*{$k}{IO}) and do_log(0, "DID CLOSE $k (fileno=$fn)");
1632#       }
1633#     }
1634      close(STDIN)       or die "Can't close STDIN: $!";
1635      close(main::stdin) or die "Can't close main::stdin: $!";
1636      open(STDIN, "<$stdin_from")
1637        or die "Can't reopen STDIN on $stdin_from: $!";
1638      fileno(STDIN) == 0 or die ("run_command: STDIN not fd0: ".fileno(STDIN));
1639      if ($stderr_to ne '') {
1640        close(STDERR) or die "Can't close STDERR: $!";
1641        open(STDERR, ">$stderr_to")
1642          or die "Can't open STDERR to $stderr_to: $!";
1643        fileno(STDERR) == 2
1644          or die ("run_command: STDERR not fd2: ".fileno(STDERR));
1645      }
1646      # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC
1647      { no warnings;
1648        exec {$cmd} ($cmd,@args) or die "Failed to exec $cmd_text: $!";
1649      }
1650    };
1651    chomp($@);
1652    do_log(-2,"run_command: child process [$$]: $@\n");
1653    { no warnings;
1654      kill('KILL',$$)  # must not exit, we have to avoid DESTROY handlers
1655        or do_log(-3,"run_command: TROUBLE - Panic1, can't die: $!");
1656      # still kicking? die!
1657      exec('/usr/bin/false'); exec('/bin/false'); exec('false'); exec('true');
1658      do_log(-3,"run_command: TROUBLE - Panic2, can't die");
1659      exit 1;                  # better safe than sorry
1660                               # NOTREACHED
1661    }
1662  }
1663  # parent
1664  do_log(5,"run_command: [$pid] $msg");
1665  binmode($proc_fh) or die "Can't set pipe to binmode: $!";  # dflt Perl 5.8.1
1666  ($proc_fh, $pid);  # return pipe file handle to the subprocess and its PID
1667}
1668
1669# Run specified command as a subprocess. Return a file handle open for
1670# WRITING to the subprocess. Use IO::Handle to ensure the subprocess
1671# will be automatically reclaimed in case of failure.
1672#
1673sub run_command_consumer($$@) {
1674  my($stdout_to, $stderr_to, $cmd, @args) = @_;
1675  my($cmd_text) = join(' ', $cmd, @args);
1676  $stdout_to = '/dev/null'  if $stdout_to eq '';
1677  my($msg) = join(' ', $cmd, @args, ">$stdout_to");
1678  $msg .= " 2>$stderr_to"  if $stderr_to ne '';
1679  my($pid); my($proc_fh) = IO::File->new;
1680  eval { $pid = $proc_fh->open('|-') };  # fork, catching errors
1681  if ($@ ne '') { chomp($@); die "run_command_consumer (open pipe): $@" }
1682  defined($pid) or die "run_command_consumer: can't fork: $!";
1683  if (!$pid) {                           # child
1684    eval {  # must not use die in forked process, or we end up with
1685            # two running daemons! Close unneeded files.
1686      close(main::stderr) or die "Can't close main::stderr: $!";
1687      close(main::stdout) or die "Can't close main::stdout: $!";
1688      close(main::STDOUT) or die "Can't close main::STDOUT: $!";
1689      open(STDOUT, ">$stdout_to")
1690        or die "Can't reopen STDOUT on $stdout_to: $!";
1691      fileno(STDOUT) == 1
1692        or die ("run_command_consumer: STDOUT not fd1: ".fileno(STDOUT));
1693      if ($stderr_to ne '') {
1694        close(STDERR) or die "Can't close STDERR: $!";
1695        open(STDERR, ">$stderr_to")
1696          or die "Can't open STDERR to $stderr_to: $!";
1697        fileno(STDERR) == 2
1698          or die ("run_command_consumer: STDERR not fd2: ".fileno(STDERR));
1699      }
1700      # BEWARE of Perl older that 5.6.0: sockets and pipes were not FD_CLOEXEC
1701      { no warnings;
1702        exec {$cmd} ($cmd,@args) or die "Failed to exec $cmd_text: $!";
1703      }
1704    };
1705    chomp($@);
1706    do_log(-2,"run_command_consumer: child process [$$]: $@\n");
1707    { no warnings;
1708      kill('KILL',$$)  # must not exit, we have to avoid DESTROY handlers
1709        or do_log(-3,"run_command_consumer: TROUBLE - Panic1, can't die: $!");
1710      # still kicking? die!
1711      exec('/usr/bin/false'); exec('/bin/false'); exec('false'); exec('true');
1712      do_log(-3,"run_command_consumer: TROUBLE - Panic2, can't die");
1713      exit 1;                  # better safe than sorry
1714                               # NOTREACHED
1715    }
1716  }
1717  # parent
1718  do_log(5,"run_command_consumer: [$pid] $msg");
1719  binmode($proc_fh) or die "Can't set pipe to binmode: $!";  # dflt Perl 5.8.1
1720  ($proc_fh, $pid);  # return pipe file handle to the subprocess and its PID
1721}
1722
17231;
1724
1725#
1726package Amavis::rfc2821_2822_Tools;
1727use strict;
1728use re 'taint';
1729
1730BEGIN {
1731  use Exporter ();
1732  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
1733  $VERSION = '2.034';
1734  @ISA = qw(Exporter);
1735  %EXPORT_TAGS = ();
1736  @EXPORT = qw(
1737    &iso8601_timestamp &iso8601_utc_timestamp &rfc2822_timestamp
1738    &received_line &parse_received
1739    &fish_out_ip_from_received &split_address &split_localpart &make_query_keys
1740    &quote_rfc2821_local &qquote_rfc2821_local &unquote_rfc2821_local
1741    &one_response_for_all
1742    &EX_OK &EX_NOUSER &EX_UNAVAILABLE &EX_TEMPFAIL &EX_NOPERM);
1743}
1744
1745use subs @EXPORT;
1746
1747use POSIX qw(locale_h strftime);
1748
1749BEGIN {
1750  eval { require 'sysexits.ph' };  # try to use the installed version
1751  # define the most important constants if undefined
1752  do { sub EX_OK()           {0} } unless defined(&EX_OK);
1753  do { sub EX_NOUSER()      {67} } unless defined(&EX_NOUSER);
1754  do { sub EX_UNAVAILABLE() {69} } unless defined(&EX_UNAVAILABLE);
1755  do { sub EX_TEMPFAIL()    {75} } unless defined(&EX_TEMPFAIL);
1756  do { sub EX_NOPERM()      {77} } unless defined(&EX_NOPERM);
1757}
1758
1759BEGIN {
1760  import Amavis::Conf qw(:platform $myhostname c cr ca);
1761  import Amavis::Util qw(ll do_log);
1762}
1763
1764# Given a Unix time, return the local time zone offset at that time
1765# as a string +HHMM or -HHMM, appropriate for the RFC2822 date format.
1766# Works also for non-full-hour zone offsets, and on systems where strftime
1767# can not return TZ offset as a number;  (c) Mark Martinec, GPL
1768#
1769sub get_zone_offset($) {
1770  my($t) = @_;
1771  my($d) = 0;   # local zone offset in seconds
1772  for (1..3) {  # match the date (with a safety loop limit just in case)
1773    my($r) = sprintf("%04d%02d%02d", (localtime($t))[5, 4, 3]) cmp
1774             sprintf("%04d%02d%02d", (gmtime($t + $d))[5, 4, 3]);
1775    if ($r == 0) { last } else { $d += $r * 24 * 3600 }
1776  }
1777  my($sl,$su) = (0,0);
1778  for ((localtime($t))[2,1,0])   { $sl = $sl * 60 + $_ }
1779  for ((gmtime($t + $d))[2,1,0]) { $su = $su * 60 + $_ }
1780  $d += $sl - $su;  # add HMS difference (in seconds)
1781  my($sign) = $d >= 0 ? '+' : '-';
1782  $d = -$d  if $d < 0;
1783  $d = int(($d + 30) / 60.0);  # give minutes, rounded
1784  sprintf("%s%02d%02d", $sign, int($d / 60), $d % 60);
1785}
1786
1787# Given a Unix time (seconds since 1970-01-01T00:00Z),
1788# provide date-time timestamp (local time) as specified in ISO 8601 (EN 28601)
1789sub iso8601_timestamp($;$) {
1790  my($t,$suppress_zone) = @_;
1791  # can't use %z because some systems do not support it (is treated as %Z)
1792  my($s) = strftime("%Y%m%dT%H%M%S", localtime($t));
1793  $s .= get_zone_offset($t)  unless $suppress_zone;
1794  $s;
1795}
1796
1797# Given a Unix time (seconds since 1970-01-01T00:00Z),
1798# provide date-time timestamp (UTC) as specified in ISO 8601 (EN 28601)
1799sub iso8601_utc_timestamp($;$) {
1800  my($t,$suppress_zone) = @_;
1801  my($s) = strftime("%Y%m%dT%H%M%S", gmtime($t));
1802  $s .= 'Z'  unless $suppress_zone;
1803  $s;
1804}
1805
1806# Given a Unix time, provide date-time timestamp as specified in RFC 2822
1807# (local time), to be used in headers such as 'Date:' and 'Received:'
1808#
1809sub rfc2822_timestamp($) {
1810  my($t) = @_;
1811  my(@lt) = localtime($t);
1812  # can't use %z because some systems do not support it (is treated as %Z)
1813  my($old_locale) = setlocale(LC_TIME,"C");  # English dates required!
1814  my($zone_name) = strftime("%Z",@lt);
1815  my($s) = strftime("%a, %e %b %Y %H:%M:%S ", @lt);
1816  $s .= get_zone_offset($t);
1817  $s .= " (" . $zone_name . ")"  if $zone_name !~ /^\s*\z/;
1818  setlocale(LC_TIME, $old_locale);  # restore the locale
1819  $s;
1820}
1821
1822sub received_line($$$$) {
1823  my($conn, $msginfo, $id, $folded) = @_;
1824  my($smtp_proto, $recips) = ($conn->smtp_proto, $msginfo->recips);
1825  my($client_ip) = $conn->client_ip;
1826  if ($client_ip =~ /:/ && $client_ip !~ /^IPv6:/i) {
1827    $client_ip = 'IPv6:' . $client_ip;
1828  }
1829  my($s) = sprintf("from %s%s\n by %s%s (amavisd-new, %s)",
1830    ($conn->smtp_helo eq '' ? 'unknown' : $conn->smtp_helo),
1831    ($client_ip eq '' ? '' : " ([$client_ip])"),
1832    c('localhost_name'),
1833    ($conn->socket_ip eq '' ? ''
1834      : sprintf(" (%s [%s])", $myhostname, $conn->socket_ip) ),
1835    ($conn->socket_port eq '' ? 'unix socket' : "port ".$conn->socket_port) );
1836  # must not use proto name QMQPqq in 'with'
1837  $s .= "\n with $smtp_proto"  if $smtp_proto=~/^(ES|S|L)MTPS?A?\z/i; # rfc3848
1838  $s .= "\n id $id"  if $id ne '';
1839  # do not disclose recipients if more than one
1840  $s .= "\n for " . qquote_rfc2821_local(@$recips)  if @$recips == 1;
1841  $s .= ";\n " . rfc2822_timestamp($msginfo->rx_time);
1842  $s =~ s/\n//g  if !$folded;
1843  $s;
1844}
1845
1846sub parse_received($) {
1847  my($received) = @_;
1848  local($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11);
1849  $received =~ s/\n([ \t])/$1/g;  # unfold
1850  $received =~ s/[\n\r]//g;       # delete remaining newlines if any
1851  my(%fields);
1852  while ($received =~ m{\G\s*
1853            ( \b(from|by) \s+ ( (?: \[ (?: \\. | [^\]\\] )* \] | [^;\s\[] )+ )
1854              (?: \s* \( (?: ( [^\s\[]+ ) \s+ )?
1855                         \[ ( (?: \\. | [^\]\\] )* ) \] \s*
1856                      \) )?
1857              (?: .*? ) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b )  # junk
1858            | \b(via|with|id|for) \s+
1859              ( (?:  "  (?: \\. | [^"\\]  )* "
1860                  |  \[ (?: \\. | [^\]\\] )* \]
1861                  |  \\. | .
1862                )+? (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b ) )
1863            | (;) \s* ( .*? ) \s* \z                                   # time
1864            | (.*?) (?= \(|;|\z|\b(?:from|by|via|with|id|for)\b )      # junk
1865            ) ( (?: \s+ | (?: \( (?: \\. | [^)\\] )* \) ) )* ) }xgcsi)
1866  {
1867    my($v1, $v2, $v3, $comment) = ('') x 4;
1868    my($item, $field) = ($1, lc($2 || $6 || $8));
1869    if ($field eq 'from' || $field eq 'by') {
1870      ($v1, $v2, $v3, $comment) = ($3, $4, $5, $11);
1871    } elsif ($field eq ';') {  # time
1872      ($v1, $comment) = ($9, $11);
1873    } elsif ($10 eq '') {      # via|with|id|for
1874      ($v1, $comment) = ($7, $11);
1875    } else {                   # junk
1876      ($v1, $comment) = ($10, $11);
1877    }
1878    $comment =~ s/^\s+//;
1879    $comment =~ s/\s+\z//;
1880    $item    =~ s/^\Q$field\E\s*//i;
1881    if (!exists $fields{$field}) {
1882      $fields{$field} = [$item, $v1, $v2, $v3, $comment];
1883      do_log(5, "parse_received: $field = $item/$v1/$v2/$v3")  if $field ne '';
1884    }
1885  }
1886  \%fields;
1887}
1888
1889sub fish_out_ip_from_received($) {
1890  my($received) = @_;
1891  my($ip);
1892  my($fields_ref) = parse_received($received);
1893  if (defined $fields_ref && exists $fields_ref->{'from'}) {
1894    my($item, $v1, $v2, $v3, $comment) = @{$fields_ref->{'from'}};
1895    for ($v3, $v2, $v1, $comment, $item) {
1896      if (/   \[ (\d{1,3} (?: \. \d{1,3}){3}) \] /x) {
1897        $ip = $1;  last;
1898      } elsif (/ (\d{1,3} (?: \. \d{1,3}){3}) (?!\d) /x) {
1899        $ip = $1;  last;
1900      } elsif (/ \[ (IPv6:)? ( ([0-9a-zA-Z]* : ){2,} [0-9a-zA-Z:.]* ) \] /xi) {
1901        $ip = $2;  last;
1902      }
1903    }
1904    do_log(5, "fish_out_ip_from_received: $ip, $item");
1905  }
1906  !defined($ip) ? undef : $ip;  # undef need not be tainted
1907}
1908
1909# Splits unquoted fully qualified e-mail address, or an address
1910# with missing domain part. Returns a pair: (localpart, domain).
1911# The domain part (if nonempty) includes the '@' as the first character.
1912# If the syntax is badly broken, everything ends up as the localpart.
1913# The domain part can be an address literal, as specified by rfc2822.
1914# Does not handle explicit route paths.
1915#
1916sub split_address($) {
1917  my($mailbox) = @_;
1918  $mailbox =~ /^ (.*?) ( \@ (?:  \[  (?: \\. | [^\]\\] )*  \]
1919                                 |  [^@"<>\[\]\\\s] )*
1920                       ) \z/xs ? ($1, $2) : ($mailbox, '');
1921}
1922
1923# split_localpart() splits localpart of an e-mail address at the first
1924# occurrence of the address extension delimiter character. (based on
1925# equivalent routine in Postfix)
1926#
1927# Reserved addresses are not split: postmaster, mailer-daemon,
1928# double-bounce. Addresses that begin with owner-, or addresses
1929# that end in -request are not split when the owner_request_special
1930# parameter is set.
1931
1932sub split_localpart($$) {
1933  my($localpart, $delimiter) = @_;
1934  my($owner_request_special) = 1;  # configurable ???
1935  my($extension);
1936  if ($localpart =~ /^(postmaster|mailer-daemon|double-bounce)\z/i) {
1937    # do not split these, regardless of what the delimiter is
1938  } elsif ($delimiter eq '-' && $owner_request_special &&
1939           $localpart =~ /^owner-|-request\z/i) {
1940    # don't split owner-foo or foo-request
1941  } elsif ($localpart =~ /^(.+?)\Q$delimiter\E(.*)\z/s) {
1942    ($localpart, $extension) = ($1, $2);
1943    # do not split the address if the result would have a null localpart
1944  }
1945  ($localpart, $extension);
1946}
1947
1948# For a given email address (e.g. for User+Foo@sub.exAMPLE.CoM)
1949# prepare and return a list of lookup keys in the following order:
1950#   User+Foo@sub.exAMPLE.COM   (as-is, no lowercasing)
1951#   user+foo@sub.example.com
1952#   user@sub.example.com (only if $recipient_delimiter nonempty)
1953#   user+foo(@) (only if $include_bare_user)
1954#   user(@)     (only if $include_bare_user and $recipient_delimiter nonempty)
1955#   (@)sub.example.com
1956#   (@).sub.example.com
1957#   (@).example.com
1958#   (@).com
1959#   (@).
1960# Note about (@): if $at_with_user is true the user-only keys (without domain)
1961# get an '@' character appended (e.g. 'user+foo@'). Usual for lookup_hash.
1962# If $at_with_user is false the domain-only (no user localpart) keys
1963# get a '@' prepended (e.g. '@.example.com'). Usual for lookup_sql.
1964#
1965# The domain part is lowercased in all but the first resulting item;
1966# the localpart is lowercased iff $localpart_is_case_sensitive is true.
1967#
1968sub make_query_keys($$$) {
1969  my($addr,$at_with_user,$include_bare_user) = @_;
1970  my($localpart,$domain) = split_address($addr); $domain = lc($domain);
1971  my($saved_full_localpart) = $localpart;
1972  $localpart = lc($localpart)  if !c('localpart_is_case_sensitive');
1973  # chop off leading @, and trailing dots
1974  $domain = $1  if $domain =~ /^\@?(.*?)\.*\z/s;
1975  my($extension); my($delim) = c('recipient_delimiter');
1976  if ($delim ne '') {
1977    ($localpart,$extension) = split_localpart($localpart,$delim);
1978  }
1979  my($append_to_user,$prepend_to_domain) = $at_with_user ? ('@','') : ('','@');
1980  my(@keys);  # a list of query keys
1981  push(@keys, $addr);                        # as is
1982  push(@keys, $localpart.$delim.$extension.'@'.$domain)
1983    if $extension ne '';                     # user+foo@example.com
1984  push(@keys, $localpart.'@'.$domain);       # user@example.com
1985  if ($include_bare_user) {  # typically enabled for local users only
1986    push(@keys, $localpart.$delim.$extension.$append_to_user)
1987      if $extension ne '';                   # user+foo(@)
1988    push(@keys, $localpart.$append_to_user); # user(@)
1989  }
1990  push(@keys, $prepend_to_domain.$domain);   # (@)sub.example.com
1991  if ($domain =~ /\[/) {     # don't split address literals
1992    push(@keys, $prepend_to_domain.'.');     # (@).
1993  } else {
1994    my(@dkeys); my($d) = $domain;
1995    for (;;) {               # (@).sub.example.com (@).example.com (@).com (@).
1996      push(@dkeys, $prepend_to_domain.'.'.$d);
1997      last  if $d eq '';
1998      $d = ($d =~ /^([^.]*)\.(.*)\z/s) ? $2 : '';
1999    }
2000    if (@dkeys > 10) { @dkeys = @dkeys[$#dkeys-9 .. $#dkeys] }  # sanity limit
2001    push(@keys,@dkeys);
2002  }
2003  my($keys_ref) = [];   # remove duplicates
2004  for my $k (@keys) { push(@$keys_ref,$k)  if !grep {$k eq $_} @$keys_ref }
2005  ll(5) && do_log(5,"query_keys: ".join(', ',@$keys_ref));
2006  # the rhs replacement strings are similar to what would be obtained
2007  # by lookup_re() given the following regular expression:
2008  # /^( ( ( [^@]*? ) ( \Q$delim\E [^@]* )? ) (?: \@ (.*) ) )$/xs
2009  my($rhs) = [   # a list of right-hand side replacement strings
2010    $addr,                  # $1 = User+Foo@Sub.Example.COM
2011    $saved_full_localpart,  # $2 = User+Foo
2012    $localpart,             # $3 = user
2013    $delim.$extension,      # $4 = +foo
2014    $domain,                # $5 = sub.example.com
2015  ];
2016  ($keys_ref, $rhs);
2017}
2018
2019# quote_rfc2821_local() quotes the local part of a mailbox address
2020# (given in internal (unquoted) form), and returns external (quoted)
2021# mailbox address, as per rfc2821.
2022#
2023# Internal (unquoted) form is used internally by amavisd-new and other mail sw,
2024# external (quoted) form is used in SMTP commands and message headers.
2025#
2026# The quote_rfc2821_local() conversion is necessary because addresses
2027# we get from certain MTAs are raw, with stripped-off quoting.
2028# To re-insert message back via SMTP, the local-part of the address needs
2029# to be quoted again if it contains reserved characters or otherwise
2030# does not obey the dot-atom syntax, as specified in rfc2821.
2031# Failing to do that gets us into trouble: amavis accepts message from MTA,
2032# but is unable to hand it back to MTA after checking, receiving
2033# '501 Bad address syntax' with every attempt.
2034#
2035sub quote_rfc2821_local($) {
2036  my($mailbox) = @_;
2037  # atext: any character except controls, SP, and specials (rfc2821/rfc2822)
2038  my($atext) = "a-zA-Z0-9!#\$%&'*/=?^_`{|}~+-";
2039  # my($specials) = '()<>\[\]\\\\@:;,."';
2040  my($localpart,$domain) = split_address($mailbox);
2041  if ($localpart !~ /^[$atext]+(\.[$atext]+)*\z/so) {  # not dot-atom
2042    $localpart =~ s/(["\\])/\\$1/g;                    # quoted-pair
2043    $localpart = '"' . $localpart . '"';  # make a qcontent out of it
2044  }
2045  $domain = ''  if $domain eq '@';        # strip off empty domain entirely
2046  $localpart . $domain;
2047}
2048
2049# wraps the result of quote_rfc2821_local into angle brackets <...> ;
2050# If given a list, it returns a list (possibly converted to
2051# comma-separated scalar), quoting each element;
2052#
2053sub qquote_rfc2821_local(@) {
2054  my(@r) = map { $_ eq '' ? '<>' : ('<' . quote_rfc2821_local($_) . '>') } @_;
2055  wantarray ? @r : join(', ', @r);
2056}
2057
2058# unquote_rfc2821_local() strips away the quoting from the local part
2059# of an external (quoted) mailbox address, and returns internal (unquoted)
2060# mailbox address, as per rfc2821.
2061#
2062# Internal (unquoted) form is used internally by amavisd-new and other mail sw,
2063# external (quoted) form is used in SMTP commands and message headers.
2064#
2065sub unquote_rfc2821_local($) {
2066  my($mailbox) = @_;
2067  # the angle-bracket stripping is not really a duty of this subroutine,
2068  # as it should have been already done elsewhere, but for the time being
2069  # we do it here:
2070  $mailbox = $1  if $mailbox =~ /^ \s* < ( .* ) > \s* \z/xs;
2071  my($localpart,$domain) = split_address($mailbox);
2072  $localpart =~ s/ " | \\ (.) | \\ \z /$1/xsg;  # unquote quoted-pairs
2073  $localpart . $domain;
2074}
2075
2076# Prepare a single SMTP response and an exit status as per sysexits.h
2077# from individual per-recipient response codes, taking into account
2078# sendmail milter specifics. Returns a triple: (smtp response, exit status,
2079# an indication whether DSN is needed).
2080#
2081sub one_response_for_all($$$) {
2082  my($msginfo, $dsn_per_recip_capable, $am_id) = @_;
2083  my($smtp_resp, $exit_code, $dsn_needed);
2084
2085  my($delivery_method) = $msginfo->delivery_method;
2086  my($sender)          = $msginfo->sender;
2087  my($per_recip_data)  = $msginfo->per_recip_data;
2088  my($any_not_done)    = scalar(grep { !$_->recip_done } @$per_recip_data);
2089  if ($delivery_method ne '' && $any_not_done)
2090    { die "Explicit forwarding, but not all recips done" }
2091  if (!@$per_recip_data) {  # no recipients, nothing to do
2092    $smtp_resp = "250 2.5.0 Ok, id=$am_id"; $exit_code = EX_OK;
2093    do_log(5, "one_response_for_all <$sender>: no recipients, '$smtp_resp'");
2094  }
2095  if (!defined $smtp_resp) {
2096    for my $r (@$per_recip_data) {  # any 4xx code ?
2097      if ($r->recip_smtp_response =~ /^4/)  # pick the first 4xx code
2098        { $smtp_resp = $r->recip_smtp_response; last }
2099    }
2100    if (!defined $smtp_resp) {
2101      for my $r (@$per_recip_data) {        # any invalid code ?
2102        if ($r->recip_done && $r->recip_smtp_response !~ /^[245]/) {
2103          $smtp_resp = '451 4.5.0 Bad SMTP response code??? "'
2104                       . $r->recip_smtp_response . '"';
2105          last;                             # pick the first
2106        }
2107      }
2108    }
2109    if (defined $smtp_resp) {
2110      $exit_code = EX_TEMPFAIL;
2111      do_log(5, "one_response_for_all <$sender>: 4xx found, '$smtp_resp'");
2112    }
2113  }
2114  # NOTE: a 2xx SMTP response code is set both by internal Discard
2115  # and by a genuine successful delivery. To distinguish between the two
2116  # we need to check $r->recip_destiny as well.
2117  #
2118  if (!defined $smtp_resp) {
2119    # if destiny for _all_ recipients is D_DISCARD => Discard
2120    my($notall);
2121    for my $r (@$per_recip_data) {
2122      if ($r->recip_destiny == D_DISCARD)  # pick the first DISCARD code
2123        { $smtp_resp = $r->recip_smtp_response  if !defined $smtp_resp }
2124      else { $notall++; last }  # one is not a discard, nogood
2125    }
2126    if ($notall) { $smtp_resp = undef }
2127    if (defined $smtp_resp) {
2128      # helper program will interpret 99 as discard
2129      $exit_code = $delivery_method eq '' ? 99 : EX_OK;
2130      do_log(5, "one_response_for_all <$sender>: all DISCARD, '$smtp_resp'");
2131    }
2132  }
2133  if (!defined $smtp_resp) {
2134    # destiny for _all_ recipients is Discard or Reject => 5xx
2135    # (and there is at least one Reject)
2136    my($notall, $done_level);
2137    my($bounce_cnt) = 0;
2138    for my $r (@$per_recip_data) {
2139      my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response);
2140      if ($dest == D_DISCARD) {
2141        # ok, this one is discard, let's see the rest
2142      } elsif ($resp =~ /^5/ && $dest != D_BOUNCE) {
2143        # prefer to report SMTP response code of genuine rejects
2144        # from MTA, over internal rejects by content filters
2145        if (!defined $smtp_resp || $r->recip_done > $done_level)
2146          { $smtp_resp  = $resp; $done_level = $r->recip_done }
2147      } else { $notall++; last }  # one is Pass or Bounce, nogood
2148    }
2149    if ($notall) { $smtp_resp = undef }
2150    if (defined $smtp_resp) {
2151      $exit_code = EX_UNAVAILABLE;
2152      do_log(5, "one_response_for_all <$sender>: REJECTs, '$smtp_resp'");
2153    }
2154  }
2155  if (!defined $smtp_resp) {
2156    # mixed destiny => 2xx, but generate dsn for bounces and rejects
2157    my($rej_cnt) = 0; my($bounce_cnt) = 0; my($drop_cnt) = 0;
2158    for my $r (@$per_recip_data) {
2159      my($dest, $resp) = ($r->recip_destiny, $r->recip_smtp_response);
2160      if ($resp =~ /^2/ && $dest == D_PASS)  # genuine successful delivery
2161        { $smtp_resp = $resp  if !defined $smtp_resp }
2162      $drop_cnt++  if $dest == D_DISCARD;
2163      if ($resp =~ /^5/)
2164        { if ($dest == D_BOUNCE) { $bounce_cnt++ } else { $rej_cnt++ } }
2165    }
2166    $exit_code = EX_OK;
2167    if (!defined $smtp_resp) {                 # no genuine Pass/2xx
2168        # declare success, we'll handle bounce
2169      $smtp_resp = "250 2.5.0 Ok, id=$am_id";
2170      if ($any_not_done) { $smtp_resp .= ", continue delivery" }
2171      elsif ($delivery_method eq '') { $exit_code = 99 }  # milter DISCARD
2172    }
2173    if ($rej_cnt + $bounce_cnt + $drop_cnt > 0) {
2174      $smtp_resp .= ", ";
2175      $smtp_resp .= "but "  if $rej_cnt+$bounce_cnt+$drop_cnt<@$per_recip_data;
2176      $smtp_resp .= join ", and ",
2177        map { my($cnt, $nm) = @$_;
2178              !$cnt ? () : $cnt == @$per_recip_data ? $nm : "$cnt $nm"
2179        } ([$rej_cnt,'REJECT'], [$bounce_cnt,'BOUNCE'], [$drop_cnt,'DISCARD']);
2180    }
2181    $dsn_needed =
2182      ($bounce_cnt > 0 || ($rej_cnt > 0 && !$dsn_per_recip_capable)) ? 1 : 0;
2183    ll(5) && do_log(5,"one_response_for_all <$sender>: "
2184             . ($rej_cnt + $bounce_cnt + $drop_cnt > 0 ? 'mixed' : 'success')
2185             . ", dsn_needed=$dsn_needed, '$smtp_resp'");
2186  }
2187  ($smtp_resp, $exit_code, $dsn_needed);
2188}
2189
21901;
2191
2192#
2193package Amavis::Lookup::RE;
2194use strict;
2195use re 'taint';
2196
2197BEGIN {
2198  use Exporter ();
2199  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
2200  $VERSION = '2.034';
2201  @ISA = qw(Exporter);
2202}
2203BEGIN { import Amavis::Util qw(ll do_log) }
2204
2205# Make an object out of the supplied lookup list
2206# to make it distinguishable from simple ACL array
2207sub new($$) { my($class) = shift; bless [@_], $class }
2208
2209# lookup_re() performs a lookup for an e-mail address against
2210# a list made up of regular expressions.
2211#
2212# The full unmodified e-mail address is always used, so splitting to localpart
2213# and domain or lowercasing is NOT performed. The regexp is powerful enough
2214# that this can be accomplished by its mechanisms. The routine is useful for
2215# other RE tests besides the usual e-mail addresses, such as looking for
2216# banned file names.
2217#
2218# Each element of the list can be ref to a pair, or directly a regexp
2219# ('Regexp' object created by qr operator, or just a (less efficient)
2220# string containing a regular expression). If it is a pair, the first
2221# element is treated as a regexp, and the second provides a value in case
2222# the regexp matches. If not a pair, the implied result of a match is 1.
2223#
2224# The regular expression is taken as-is, no implicit anchoring or setting
2225# case insensitivity is done, so use qr'(?i)^user@example\.com$',
2226# and not a sloppy qr'user@example.com', which can easily backfire.
2227# Also, if qr is used with a delimiter other than ', make sure to quote
2228# the @ and $ .
2229#
2230# The pattern allows for capturing of parenthesized substrings, which can
2231# then be referenced from the result string using the $1, $2, ... notation,
2232# as with the Perl m// operator. The number after the $ may be a multi-digit
2233# decimal number. To avoid possible ambiguity the ${n} or $(n) form may be used
2234# Substring numbering starts with 1. Nonexistent references evaluate to empty
2235# strings. If any substitution is done, the result inherits the taintedness
2236# of $addr. Keep in mind that $ and @ characters needs to be backslash-quoted
2237# in qq() strings. Example:
2238#   $virus_quarantine_to = new_RE(
2239#     [ qr'^(.*)@example\.com$'i => 'virus-${1}@example.com' ],
2240#     [ qr'^(.*)(@[^@]*)?$'i     => 'virus-${1}${2}' ] );
2241#
2242# Example (equivalent to the example in lookup_acl):
2243#    $acl_re = Amavis::Lookup::RE->new(
2244#                       qr'@me\.ac\.uk$'i, [qr'[@.]ac\.uk$'i=>0], qr'\.uk$'i );
2245#    ($r,$k) = $acl_re->lookup_re('user@me.ac.uk');
2246# or $r = lookup(0, 'user@me.ac.uk', $acl_re);
2247#
2248# 'user@me.ac.uk'   matches me.ac.uk, returns true and search stops
2249# 'user@you.ac.uk'  matches .ac.uk, returns false (because of =>0) and search stops
2250# 'user@them.co.uk' matches .uk, returns true and search stops
2251# 'user@some.com'   does not match anything, falls through and returns false (undef)
2252
2253sub lookup_re($$;$) {
2254  my($self, $addr,$get_all) = @_;
2255  local($1,$2,$3,$4); my(@matchingkey,@result);
2256  for my $e (@$self) {
2257    my($key,$r);
2258    if (ref($e) eq 'ARRAY') {  # a pair: (regexp,result)
2259      ($key,$r) = ($e->[0], @$e < 2 ? 1 : $e->[1]);
2260    } else {                   # a single regexp (not a pair), implies result 1
2261      ($key,$r) = ($e, 1);
2262    }
2263  # do_log(5, "lookup_re: key=\"$addr\", matching against RE $key");
2264    ""=~/x{0}/;  # braindead Perl: serves as explicit deflt for an empty regexp
2265    my(@rhs) = $addr =~ /$key/;  # match, capturing parenthesized subpatterns
2266    if (@rhs) {  # regexp matches
2267      # do the righthand side replacements if any $n, ${n} or $(n) is specified
2268      if (!ref($r) && $r=~/\$/) {
2269        my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
2270                          { my($j)=$2+$3+$4; $j<1 ? '' : $rhs[$j-1] }gxse;
2271        # bring taintedness of input to the result
2272        $r .= substr($addr,0,0)  if $any;
2273      }
2274      push(@result,$r); push(@matchingkey,$key);
2275      last  if !$get_all;
2276    }
2277  }
2278  if (!ll(5)) {
2279    # don't bother preparing log report which will not be printed
2280  } elsif (!@result) {
2281    do_log(5,"lookup_re($addr), no matches");
2282  } else {  # pretty logging
2283    my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
2284                e => "\e", a => "\a", t => "\t");
2285    my(@mk) = @matchingkey;
2286    for my $mk (@mk)  # undo the \-quoting, will be redone by logging routines
2287      { $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : $1 }egsx }
2288    if (!$get_all) {  # first match wins
2289      do_log(5,sprintf('lookup_re(%s) matches key "%s", result=%s',
2290                        $addr,$mk[0],$result[0]));
2291    } else {  # want all matches
2292      do_log(5,"lookup_re($addr) matches keys: ".
2293               join(', ', map {sprintf('"%s"=>%s',$mk[$_],$result[$_])}
2294                              (0..$#result)));
2295    }
2296  }
2297  if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
2298  else           { !wantarray ? \@result   : (\@result,   \@matchingkey)   }
2299}
2300
23011;
2302
2303#
2304package Amavis::Lookup::Label;
2305use strict;
2306use re 'taint';
2307
2308# Make an object out of the supplied string, to serve as label
2309# in log messages generated by sub lookup
2310sub new($$) { my($class) = shift; my($str) = shift; bless \$str, $class }
2311sub display($) { my($self) = shift; $$self }
2312
23131;
2314
2315#
2316package Amavis::Lookup;
2317use strict;
2318use re 'taint';
2319
2320BEGIN {
2321  use Exporter ();
2322  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
2323  $VERSION = '2.034';
2324  @ISA = qw(Exporter);
2325  %EXPORT_TAGS = ();
2326  @EXPORT = ();
2327  @EXPORT_OK = qw(&lookup &lookup_ip_acl);
2328}
2329use subs @EXPORT_OK;
2330
2331BEGIN {
2332  import Amavis::Util qw(ll do_log);
2333  import Amavis::Conf qw(:platform c cr ca);
2334  import Amavis::Timing qw(section_time);
2335  import Amavis::rfc2821_2822_Tools qw(split_address make_query_keys);
2336}
2337
2338# lookup_hash() performs a lookup for an e-mail address against a hash map.
2339# If a match is found (a hash key exists in the Perl hash) the function returns
2340# whatever the map returns, otherwise undef is returned. First match wins,
2341# aborting further search sequence.
2342#
2343sub lookup_hash($$;$) {
2344  my($addr, $hash_ref,$get_all) = @_;
2345  (ref($hash_ref) eq 'HASH')
2346    or die "lookup_hash: arg2 must be a hash ref: $hash_ref";
2347  local($1,$2,$3,$4); my(@matchingkey,@result);
2348  my($keys_ref,$rhs_ref) = make_query_keys($addr,1,1);
2349  for my $key (@$keys_ref) {   # do the search
2350    if (defined $$hash_ref{$key}) {  # got it
2351      push(@result,$$hash_ref{$key}); push(@matchingkey,$key);
2352      last  if !$get_all;
2353    }
2354  }
2355  # do the right-hand side replacements if any $n, ${n} or $(n) is specified
2356  for my $r (@result) {  # remember that $r is just an alias to array elements
2357    if (!ref($r) && $r=~/\$/) {  # is a plain string containing a '$'
2358      my($any) = $r =~ s{ \$ ( (\d+) | \{ (\d+) \} | \( (\d+) \) ) }
2359                        { my($j)=$2+$3+$4; $j<1 ? '' : $rhs_ref->[$j-1] }gxse;
2360      # bring taintedness of input to the result
2361      $r .= substr($addr,0,0)  if $any;
2362    }
2363  }
2364  if (!ll(5)) {
2365    # only bother with logging when needed
2366  } elsif (!@result) {
2367    do_log(5,"lookup_hash($addr), no matches");
2368  } elsif (!$get_all) {  # first match wins
2369    do_log(5,sprintf('lookup_hash(%s) matches key "%s", result=%s',
2370                      $addr,$matchingkey[0],$result[0]));
2371  } else {  # want all matches
2372    do_log(5,"lookup_hash($addr) matches keys: ".
2373             join(', ', map {sprintf('"%s"=>%s',$matchingkey[$_],$result[$_])}
2374                            (0..$#result)));
2375  }
2376  if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
2377  else           { !wantarray ? \@result   : (\@result,   \@matchingkey)   }
2378}
2379
2380# lookup_acl() performs a lookup for an e-mail address against
2381# access control list.
2382#
2383# Domain name of the supplied address is compared with each member of the
2384# access list in turn, the first match wins (terminates the search),
2385# and its value decides whether the result is true (yes, permit, pass)
2386# or false (no, deny, drop). Falling through without a match
2387# produces false (undef). Search is case-insensitive.
2388#
2389# If a list member contains a '@', the full e-mail address is compared,
2390# otherwise if a list member has a leading dot, the domain name part is
2391# matched only, and the domain as well as its subdomains can match. If there
2392# is no leading dot, the domain must match exactly (subdomains do not match).
2393#
2394# The presence of character '!' prepended to the list member decides
2395# whether the result will be true (without a '!') or false (with '!')
2396# in case this list member matches and terminates the search.
2397#
2398# Because search stops at the first match, it only makes sense
2399# to place more specific patterns before the more general ones.
2400#
2401# Although not a special case, it is good to remember that '.' always matches,
2402# so '.' would stop the search and return true, whereas '!.' would stop the
2403# search and return false (0) (which is normally not very useful,
2404# as false (undef) is also implied at the end of the list).
2405#
2406# Examples:
2407#
2408# given: @acl = qw( me.ac.uk !.ac.uk .uk )
2409#   'me.ac.uk' matches me.ac.uk, returns true and search stops
2410#
2411# given: @acl = qw( me.ac.uk !.ac.uk .uk )
2412#   'you.ac.uk' matches .ac.uk, returns false (because of '!') and search stops
2413#
2414# given: @acl = qw( me.ac.uk !.ac.uk .uk )
2415#   'them.co.uk' matches .uk, returns true and search stops
2416#
2417# given: @acl = qw( me.ac.uk !.ac.uk .uk )
2418#   'some.com' does not match anything, falls through and returns false (undef)
2419#
2420# given: @acl = qw( me.ac.uk !.ac.uk .uk !. )
2421#   'some.com' similar to previous, except it returns 0 instead of undef
2422#
2423# given: @acl = qw( me.ac.uk !.ac.uk .uk . )
2424#   'some.com' matches catchall ".", and returns true. The ".uk" is redundant
2425#
2426# more complex example: @acl = qw(
2427#   !The.Boss@dept1.xxx.com .dept1.xxx.com
2428#   .dept2.xxx.com .dept3.xxx.com lab.dept4.xxx.com
2429#   sub.xxx.com !.sub.xxx.com
2430#   me.d.aaa.com him.d.aaa.com !.d.aaa.com .aaa.com
2431# );
2432
2433sub lookup_acl($$) {
2434  my($addr, $acl_ref) = @_;
2435  (ref($acl_ref) eq 'ARRAY')
2436    or die "lookup_acl: arg2 must be a list ref: $acl_ref";
2437  return undef  if !@$acl_ref;  # empty list can't match anything
2438  my($lpcs) = c('localpart_is_case_sensitive');
2439  my($localpart,$domain) = split_address($addr); $domain = lc($domain);
2440  $localpart = lc($localpart)  if !$lpcs;
2441  local($1,$2);
2442  # chop off leading @ and trailing dots
2443  $domain = $1  if $domain =~ /^\@?(.*?)\.*\z/s;
2444  my($lcaddr) = $localpart . '@' . $domain;
2445  my($found, $matchingkey, $result);
2446  for my $e (@$acl_ref) {
2447    $result = 1; $matchingkey = $e; my($key) = $e;
2448    if ($key =~ /^(!+)(.*)\z/s) {      # starts with an exclamation mark(s)
2449      $key = $2;
2450      $result = 1-$result  if (length($1) & 1);  # negate if odd
2451    }
2452    if ($key =~ /^(.*?)\@([^@]*)\z/s) {   # contains '@', check full address
2453      $found++  if $localpart eq ($lpcs?$1:lc($1)) && $domain eq lc($2);
2454    } elsif ($key =~ /^\.(.*)\z/s) {   # leading dot: domain or subdomain
2455      my($key_t) = lc($1);
2456      $found++  if $domain eq $key_t || $domain =~ /(\.|\z)\Q$key_t\E\z/s;
2457    } else {                           # match domain (but not its subdomains)
2458      $found++  if $domain eq lc($key);
2459    }
2460    last  if $found;
2461  }
2462  $matchingkey = $result = undef  if !$found;
2463  do_log(5, "lookup_acl($addr)".
2464    (!$found?", no match":" matches key \"$matchingkey\", result=$result"));
2465  !wantarray ? $result : ($result, $matchingkey);
2466}
2467
2468# Perform a lookup for an e-mail address against any number of supplied maps:
2469# - SQL map,
2470# - LDAP map,
2471# - hash map,
2472# - (access control) list,
2473# - a list of regular expressions,
2474# - a (defined) scalar always matches, and returns itself as the 'map' value
2475#   (useful as a catchall for final 'pass' or 'fail');
2476# (see lookup_hash, lookup_acl, lookup_sql and lookup_ldap for details).
2477#
2478# when $get_all is 0 (the common usage):
2479#   If a match is found (a defined value), returns whatever the map returns,
2480#   otherwise returns undef. FIRST match aborts further search sequence.
2481# when $get_all is true:
2482#   Collects a list of results from ALL matching tables, and within each
2483#   table from ALL matching key. Returns a ref to the list of results
2484#   (and a ref to a list of matching keys if returning a pair).
2485#   The first element of both lists is supposed to be what lookup() would
2486#   have returned if $get_all were 0. The order of returned elements
2487#   corresponds to the order of the search.
2488#
2489sub lookup($$@) {
2490  my($get_all, $addr, @tables) = @_;
2491  my($label, @result,@matchingkey);
2492  for my $tb (@tables) {
2493    my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection
2494    if (!ref($t) || ref($t) eq 'SCALAR') {   # a scalar always matches
2495      my($r) = ref($t) ? $$t : $t;  # allow direct or indirect reference
2496      if (defined $r) {
2497        do_log(5,"lookup: (scalar) matches, result=\"$r\"");
2498        push(@result,$r); push(@matchingkey,"(constant:$r)");
2499      }
2500    } elsif (ref($t) eq 'HASH') {
2501      my($r,$mk) = lookup_hash($addr,$t,$get_all);
2502      if (!defined $r)  {}
2503      elsif (!$get_all) { push(@result,$r);  push(@matchingkey,$mk)  }
2504      elsif (@$r)       { push(@result,@$r); push(@matchingkey,@$mk) }
2505    } elsif (ref($t) eq 'ARRAY') {
2506      my($r,$mk) = lookup_acl($addr,$t);
2507      if (defined $r)   { push(@result,$r);  push(@matchingkey,$mk)  }
2508    } elsif ($t->isa('Amavis::Lookup::Label')) {  # logging label
2509      # just a convenience for logging purposes, not a real lookup method
2510      $label = $t->display;  # grab the name, and proceed with the next table
2511    } elsif ($t->isa('Amavis::Lookup::RE')) {
2512      my($r,$mk) = $t->lookup_re($addr,$get_all);
2513      if (!defined $r)  {}
2514      elsif (!$get_all) { push(@result,$r);  push(@matchingkey,$mk)  }
2515      elsif (@$r)       { push(@result,@$r); push(@matchingkey,@$mk) }
2516    } elsif ($t->isa('Amavis::Lookup::SQL')) {
2517      my($r,$mk) = $t->lookup_sql($addr,$get_all);
2518      if (!defined $r)  {}
2519      elsif (!$get_all) { push(@result,$r);  push(@matchingkey,$mk)  }
2520      elsif (@$r)       { push(@result,@$r); push(@matchingkey,@$mk) }
2521    } elsif ($t->isa('Amavis::Lookup::SQLfield')) {
2522      my($r,$mk) = $t->lookup_sql_field($addr,$get_all);
2523      if (!defined $r)  {}
2524      elsif (!$get_all) { push(@result,$r);  push(@matchingkey,$mk)  }
2525      elsif (@$r)       { push(@result,@$r); push(@matchingkey,@$mk) }
2526    } elsif ($t->isa('Amavis::Lookup::LDAP')) {
2527      my($r,$mk) = $t->lookup_ldap($addr,$get_all);
2528      if (!defined $r)  {}
2529      elsif (!$get_all) { push(@result,$r);  push(@matchingkey,$mk) }
2530      elsif (@$r)       { push(@result,@$r); push(@matchingkey,@$mk) }
2531    } elsif ($t->isa('Amavis::Lookup::LDAPattr')) {
2532      my($r,$mk) = $t->lookup_ldap_attr($addr,$get_all);
2533      if (!defined $r)  {}
2534      elsif (!$get_all) { push(@result,$r);  push(@matchingkey,$mk) }
2535      elsif (@$r)       { push(@result,@$r); push(@matchingkey,@$mk) }
2536    } else {
2537      die "TROUBLE: lookup table is an unknown object: " . ref($t);
2538    }
2539    last  if @result && !$get_all;
2540  }
2541  # pretty logging
2542  if ($label ne '') { $label = " ($label)" }
2543  if (!ll(4)) {
2544    # don't bother preparing log report which will not be printed
2545  } elsif (!@tables) {
2546    do_log(4,"lookup$label => undef, \"$addr\", no lookup tables");
2547  } elsif (!@result) {
2548    do_log(4,"lookup$label => undef, \"$addr\" does not match");
2549  } elsif (!$get_all) {  # first match wins
2550    do_log(4,sprintf(
2551      'lookup%s => %-6s "%s" matches, result=%s, matching_key="%s"',
2552      $label, $result[0]?'true,':'false,', $addr,
2553      (ref $result[0] ne 'ARRAY' ? '"'.$result[0].'"'
2554                                 : '('.join(',',@{$result[0]}).')'),
2555      $matchingkey[0]));
2556  } else {  # want all matches
2557    do_log(4,sprintf('lookup%s, %d matches for "%s", results: %s',
2558      $label, scalar(@result), $addr,
2559      join(', ', map {sprintf('"%s"=>%s', $matchingkey[$_],
2560                              (ref $result[$_] ne 'ARRAY'
2561                                 ? '"'.$result[$_].'"'
2562                                 : '('.join(',',@{$result[$_]}).')') )}
2563                     (0..$#result) )));
2564  }
2565  if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
2566  else           { !wantarray ? \@result   : (\@result,   \@matchingkey)   }
2567}
2568
2569# ip_to_vec() takes IPv6 or IPv4 IP address with optional prefix length
2570# (or IPv4 mask), parses and validates it, and returns it as a 128-bit
2571# vector string that can be used as operand to Perl bitwise string operators.
2572# Syntax and other errors in the argument throw exception (die).
2573# If the second argument $allow_mask is 0, the prefix length or mask
2574# specification is not allowed as part of the IP address.
2575#
2576# The IPv6 syntax parsing and validation adheres to rfc3513.
2577# All the following IPv6 address forms are supported:
2578#   x:x:x:x:x:x:x:x        preferred form
2579#   x:x:x:x:x:x:d.d.d.d    alternative form
2580#   ...::...               zero-compressed form
2581#   addr/prefix-length     prefix length may be specified (defaults to 128)
2582# Optionally an "IPv6:" prefix may be prepended to the IPv6 address
2583# as specified by rfc2821. No brackets are allowed enclosing the address.
2584#
2585# The following IPv4 forms are allowed:
2586#   d.d.d.d
2587#   d.d.d.d/prefix-length  CIDR mask length is allowed (defaults to 32)
2588#   d.d.d.d/m.m.m.m        network mask (gets converted to prefix-length)
2589# If prefix-length or a mask is specified with an IPv4 address, the address
2590# may be shortened to d.d.d/n or d.d/n or d/n. Such truncation is allowed
2591# for compatibility with earlier version, but is deprecated and is not
2592# allowed for IPv6 addresses.
2593#
2594# IPv4 addresses and masks are converted to IPv4-mapped IPv6 addresses
2595# of the form ::FFFF:d.d.d.d,  The CIDR mask length (0..32) is converted
2596# to IPv6 prefix-length (96..128). The returned vector strings resulting
2597# from IPv4 and IPv6 forms are indistinguishable.
2598#
2599# NOTE:
2600#   d.d.d.d is equivalent to ::FFFF:d.d.d.d (IPv4-mapped IPv6 address)
2601#   which is not the same as ::d.d.d.d      (IPv4-compatible IPv6 address)
2602#
2603# A triple is returned:
2604#  - IP address represented as a 128-bit vector (a string)
2605#  - network mask derived from prefix length, a 128-bit vector (string)
2606#  - prefix length as an integer (0..128)
2607#
2608sub ip_to_vec($;$) {
2609  my($ip,$allow_mask) = @_;
2610  my($ip_len); my(@ip_fields);
2611  local($1,$2,$3,$4,$5,$6);
2612  $ip =~ s/^[ \t]+//; $ip =~ s/[ \t\n]+\z//s;  # trim
2613  my($ipa) = $ip;
2614  ($ipa,$ip_len) = ($1,$2)  if $allow_mask && $ip =~ m{^([^/]*)/(.*)\z}s;
2615  if ($ipa =~ m{^(IPv6:)?(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z}si){
2616    # IPv6 alternative form x:x:x:x:x:x:d.d.d.d
2617    !grep {$_ > 255} ($3,$4,$5,$6)
2618      or die "Invalid decimal field value in IPv6 address: $ip";
2619    $ipa = $2 . sprintf("%02X%02X:%02X%02X", $3,$4,$5,$6);
2620  } elsif ($ipa =~ m{^\d{1,3}(?:\.\d{1,3}){0,3}\z}) {  # IPv4 form
2621    my(@d) = split(/\./,$ipa,-1);
2622    !grep {$_ > 255} @d  or die "Invalid field value in IPv4 address: $ip";
2623    defined($ip_len) || @d==4
2624      or die "IPv4 address $ip contains fewer than 4 fields";
2625    $ipa = '::FFFF:' . sprintf("%02X%02X:%02X%02X", @d);  # IPv4-mapped IPv6
2626    if (!defined($ip_len)) { $ip_len = 32;   # no length, defaults to /32
2627    } elsif ($ip_len =~ /^\d{1,9}\z/) {      # /n, IPv4 CIDR notation
2628    } elsif ($ip_len =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
2629      !grep {$_ > 255} ($1,$2,$3,$4)
2630        or die "Illegal field value in IPv4 mask: $ip";
2631      my($mask1) = pack('C4',$1,$2,$3,$4);   # /m.m.m.m
2632      my($len) = unpack("%b*",$mask1);       # count ones
2633      my($mask2) = pack('B32', '1' x $len);  # reconstruct mask from count
2634      $mask1 eq $mask2
2635        or die "IPv4 mask not representing valid CIDR mask: $ip";
2636      $ip_len = $len;
2637    } else {
2638      die "Invalid IPv4 network mask or CIDR prefix length: $ip";
2639    }
2640    $ip_len<=32 or die "IPv4 network prefix length greater than 32: $ip";
2641    $ip_len += 128-32;  # convert IPv4 net mask length to IPv6 prefix length
2642  }
2643  $ip_len = 128  if !defined($ip_len);
2644  $ip_len<=128 or die "IPv6 network prefix length greater than 128: $ip";
2645  $ipa =~ s/^IPv6://i;
2646  # now we presumably have an IPv6 preferred form x:x:x:x:x:x:x:x
2647  if ($ipa !~ /^(.*?)::(.*)\z/s) {  # zero-compressing form used?
2648    @ip_fields = split(/:/,$ipa,-1);  # no
2649  } else {                         # expand zero-compressing form
2650    my(@a) = split(/:/,$1,-1); my(@b) = split(/:/,$2,-1);
2651    my($missing_cnt) = 8-(@a+@b);  $missing_cnt = 1  if $missing_cnt<1;
2652    @ip_fields = (@a, (0) x $missing_cnt, @b);
2653  }
2654  !grep { !/^[0-9a-zA-Z]{1,4}\z/ } @ip_fields
2655    or die "Invalid syntax of IPv6 address: $ip";
2656  @ip_fields<8 and die "IPv6 address $ip contains fewer than 8 fields";
2657  @ip_fields>8 and die "IPv6 address $ip contains more than 8 fields";
2658  my($vec) = pack("n8", map {hex} @ip_fields);
2659  $ip_len=~/^\d{1,3}\z/
2660    or die "Invalid prefix length syntax in IP address: $ip";
2661  $ip_len<=128 or die "Invalid prefix length in IPv6 address: $ip";
2662  my($mask) = pack('B128', '1' x $ip_len);
2663# do_log(5,sprintf("ip_to_vec: %s => %s/%d\n", $ip,unpack("B*",$vec),$ip_len));
2664  ($vec,$mask,$ip_len);
2665}
2666
2667# lookup_ip_acl() performs a lookup for an IPv4 or IPv6 address
2668# against access control list or a hash of network or host addresses.
2669#
2670# IP address is compared to each member of the access list in turn,
2671# the first match wins (terminates the search), and its value decides
2672# whether the result is true (yes, permit, pass) or false (no, deny, drop).
2673# Falling through without a match produces false (undef).
2674#
2675# The presence of character '!' prepended to the list member decides
2676# whether the result will be true (without a '!') or false (with '!')
2677# in case this list member matches and terminates the search.
2678#
2679# Because search stops at the first match, it only makes sense
2680# to place more specific patterns before the more general ones.
2681#
2682# For IPv4 a network address can be specified in classless notation
2683# n.n.n.n/k, or using a mask n.n.n.n/m.m.m.m . Missing mask implies /32,
2684# i.e. a host address. For IPv6 addresses all rfc3513 forms are allowed.
2685# See also comments at ip_to_vec().
2686#
2687# Although not a special case, it is good to remember that '::/0'
2688# always matches any IPv4 or IPv6 address.
2689#
2690# The '0/0' is equivalent to '::FFFF:0:0/0' and matches any IPv4 address
2691# (including IPv4-mapped IPv6 addresses), but not other IPv6 addresses!
2692#
2693# Example
2694#   given: @acl = qw( !192.168.1.12 172.16.3.3 !172.16.3.0/255.255.255.0
2695#                     10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
2696#                     !0.0.0.0/8 !:: 127.0.0.0/8 ::1 );
2697# matches rfc1918 private address space except host 192.168.1.12
2698# and net 172.16.3/24 (but host 172.16.3.3 within 172.16.3/24 still matches).
2699# In addition the 'unspecified' (null) IPv4 and IPv6 addresses return false,
2700# and IPv4 and IPv6 loopback addresses match and return true.
2701#
2702sub lookup_ip_acl($@) {
2703  my($ip, @nets_ref) = @_;
2704  my($ip_vec,$ip_mask) = ip_to_vec($ip,0);
2705  my($label,$found,$fullkey,$result);
2706  for my $tb (@nets_ref) {
2707    my($t) = ref($tb) eq 'REF' ? $$tb : $tb; # allow one level of indirection
2708    if (!ref($t) || ref($t) eq 'SCALAR') {   # a scalar always matches
2709      my($r) = ref($t) ? $$t : $t;  # allow direct or indirect reference
2710      $result = $r; $fullkey = "(constant:$r)";
2711      $found++  if defined $result;
2712    } elsif (ref($t) eq 'HASH') {
2713      # match the canonical IP address: dot-quad IPv4, or preferred IPv6 form
2714      my($ip_c);  # IP address in the canonical preferred form: x:x:x:x:x:x:x:x
2715      my($ip_dq); # IPv4 in a dotted-quad form if IPv4-mapped, or undef
2716      $ip_c = join(':', map {sprintf('%04x',$_)} unpack('n8',$ip_vec));
2717      my($ipv4_vec,$ipv4_mask) = ip_to_vec('::FFFF:0:0/96',1);
2718      if ( ($ip_vec & $ipv4_mask) eq ($ipv4_vec & $ipv4_mask) ) {
2719        # is an IPv4-mapped IPv6 address, format it in a dot-quad form
2720        $ip_dq = join('.', unpack('C4',substr($ip_vec,12,4)));  # last 32 bits
2721      }
2722      do_log(5, "lookup_ip_acl keys: \"$ip_dq\", \"$ip_c\"");
2723      if (defined $ip_dq) {  # try dot-quad if applicable
2724        $fullkey = $ip_dq; $result = $t->{$fullkey};
2725        $found++  if defined $result;
2726      }
2727      if (!$found) {         # try the 'preferred IPv6 form'
2728        $fullkey = $ip_c; $result = $t->{$fullkey};
2729        $found++  if defined $result;
2730      }
2731    } elsif (ref($t) eq 'ARRAY') {
2732      for my $net (@$t) {
2733        $fullkey = $net; my($key) = $fullkey; $result = 1;
2734        if ($key =~ /^(!+)(.*)\z/s) {  # starts with exclamation mark(s)
2735          $key = $2;
2736          $result = 1 - $result  if (length($1) & 1);  # negate if odd
2737        }
2738        my($acl_ip_vec, $acl_mask) = ip_to_vec($key,1);
2739        $found++  if ($ip_vec & $acl_mask) eq ($acl_ip_vec & $acl_mask);
2740        last  if $found;
2741      }
2742    } elsif ($t->isa('Amavis::Lookup::Label')) {  # logging label
2743      # just a convenience for logging purposes, not a real lookup method
2744      $label = $t->display;  # grab the name, and proceed with the next table
2745    } else {
2746      die "TROUBLE: lookup table is an unknown object: " . ref($t);
2747    }
2748    last  if $found;
2749  }
2750  $fullkey = $result = undef  if !$found;
2751  if ($label ne '') { $label = " ($label)" }
2752  ll(4) && do_log(4, "lookup_ip_acl$label: key=\"$ip\""
2753         . (!$found ? ", no match" : " matches \"$fullkey\", result=$result"));
2754  !wantarray ? $result : ($result, $fullkey);
2755}
2756
27571;
2758
2759#
2760package Amavis::Expand;
2761use strict;
2762use re 'taint';
2763
2764BEGIN {
2765  use Exporter ();
2766  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
2767  $VERSION = '2.034';
2768  @ISA = qw(Exporter);
2769  %EXPORT_TAGS = ();
2770  @EXPORT = ();
2771  @EXPORT_OK = qw(&expand);
2772}
2773use subs @EXPORT_OK;
2774BEGIN {
2775  import Amavis::Util qw(ll do_log);
2776}
2777
2778# Given a string reference and a hashref of predefined (builtin) macros,
2779# expand() performs a macro expansion and returns a ref to the resulting string
2780#
2781# This is a simple, yet fully fledged macro processor with proper lexical
2782# analysis, call stack, implied quoting levels, user supplied builtin macros,
2783# two builtin flow-control macros: selector and iterator, plus a macro #,
2784# which discards input tokens until NEWLINE (like 'dnl' in m4).
2785# Also recognized are the usual \c and \nnn forms for specifying special
2786# characters, where c can be any of: r, n, f, b, e, a, t.  Lexical analysis
2787# of the input string is preformed only once, macro result values are not
2788# in danger of being lexically parsed and are treated as plain characters,
2789# loosing any special meaning they might have. No new macros can be defined
2790# by processing input string (at least in this version).
2791#
2792# Simple caller-provided macros have a single character name (usually a letter)
2793# and can evaluate to a string (possibly empty or undef), or an array of
2794# strings. It can also be a subroutine reference, in which case the subroutine
2795# will be called whenever macro value is needed. The subroutine must return
2796# a scalar: a string, or an array reference. The result will be treated as if
2797# it were specified directly.
2798#
2799# Two forms of simple macro calls are known: %x and %#x (where x is a single
2800# letter macro name, i.e. a key in a user-supplied hash):
2801#   %x   evaluates to the hash value associated with the name x;
2802#        if the value is an array ref, the result is a single concatenated
2803#        string of values separated with comma-space pairs;
2804#   %#x  evaluates to a number: if the macro value is a scalar, returns 0
2805#        for all-whitespace value, and 1 otherwise. If a value is an array ref,
2806#        evaluates to the number of elements in the array.
2807# A simple macro is evaluated only in nonquoted context, i.e. top-level
2808# text or in the first argument of a selector (see below). A literal percent
2809# character can be produced by %% or \%.
2810#
2811# More powerful expansion is provided by two builtin macros, using syntax:
2812#   [? arg1 | arg2 | ... ]    a selector
2813#   [  arg1 | arg2 | ... ]    an iterator
2814# where [, [?, | and ] are required tokens. To take away the special meaning
2815# of these characters they can be quoted by a backslash, e.g. \[ or \\ .
2816# Arguments are arbitrary text, possibly multiline, whitespace counts.
2817# Nested macro calls are permitted, proper bracket nesting must be observed.
2818#
2819# SELECTOR lets its first argument be evaluated immediately, and implicitly
2820# protects the remaining arguments. The first argument chooses which of the
2821# remaining arguments is selected as a result value. The result is only then
2822# evaluated, remaining arguments are discarded without evaluation. The first
2823# argument is usually a number (with optional leading and trailing whitespace).
2824# If it is a non-numeric string, it is treated as 0 for all-whitespace, and
2825# as 1 otherwise. Value 0 selects the very next (second) argument, value 1
2826# selects the one after it, etc. If the value is greater than the number
2827# of available arguments, the last one (but never the first) is selected.
2828# If there is only one (the first) alternative available but the value is
2829# greater than 0, an empty string is returned.
2830#   Examples:
2831#     [? 2   | zero | one | two | three ]  -> two
2832#     [? foo | none | any | two | three ]  -> any
2833#     [? 24  | 0    | one | many ]         -> many
2834#     [? 2   |No recipients]               -> (empty string)
2835#     [? %#R |No recipients|One recipient|%#R recipients]
2836#     [? %q  |No quarantine|Quarantined as %q]
2837# Note that a selector macro call can be considered a form of if-then-else,
2838# except that the 'then' and 'else' parts are swapped!
2839#
2840# ITERATOR in its full form takes three arguments (and ignores any extra
2841# arguments after that):
2842#     [ %x | body-usually-containing-%x | separator ]
2843# All iterator's arguments are implicitly quoted, iterator performs its own
2844# substitutions (described below). The result of an iterator call is a body
2845# (the second argument) repeated as many times as there are elements in the
2846# array denoted by the first argument. In each instance of a body
2847# all occurrences of token %x in the body are replaced with each successive
2848# element of the array. Resulting body instances are then glued together
2849# with a string given as the third argument. The result is finally evaluated
2850# as any top-level text for possible further expansion.
2851#
2852# There are two simplified forms of iterator call:
2853#     [ body | separator ]
2854# or  [ body ]
2855# where missing separator is considered a null string, and the missing formal
2856# argument name is obtained by looking for the first token of the form %x
2857# in the body.
2858#   Examples:
2859#     [%V| ]     a space-separated list of virus names
2860#
2861#     [%V|\n]    a newline-separated list of virus names
2862#
2863#     [%V|
2864#     ]          same thing: a newline-separated list of virus names
2865#
2866#     [
2867#         %V]    a list of virus names, each preceeded by NL and spaces
2868#
2869#     [ %R |%s --> <%R>|, ]  a comma-space separated list of sender/recipient
2870#                name pairs where recipient is iterated over the list
2871#                of recipients. (Only the (first) token %x in the first
2872#                argument is significant, other characters are ignored.)
2873#
2874#     [%V|[%R|%R + %V|, ]|; ]  produce all combinations of %R + %V elements
2875#
2876# A combined example:
2877#     [? %#C |#|Cc: [<%C>|, ]]
2878#     [? %#C ||Cc: [<%C>|, ]\n]#     ... same thing
2879# evaluates to an empty string if there are no elements in the %C array,
2880# otherwise it evaluates to a line:  Cc: <addr1>, <addr2>, ...\n
2881# The '#' removes input characters until and including newline after it.
2882# It can be used for clarity to allow newlines be placed in the source text
2883# but not resulting in empty lines in the expanded text. In the second example
2884# above, a backslash at the end of the line would achieve the same result,
2885# although the method is different: \NEWLINE is removed during initial lexical
2886# analysis, while # is an internal macro which, when called, actively discards
2887# tokens following it, until NEWLINE (or end of input) is encountered.
2888# Whitespace (including newlines) around the first argument %#C of selector
2889# call is ignored and can be used for clarity.
2890#
2891# These all produce the same result:
2892#     To: [%T|%T|, ]
2893#     To: [%T|, ]
2894#     To: %T
2895#
2896# See further practical examples in the supplied notification messages;
2897# see also README.customize file.
2898#
2899#   Author: Mark Martinec <Mark.Martinec@ijs.si>, 2002
2900#
2901sub expand($$) {
2902  my($str_ref)       = shift;  # a ref to a source string to be macro expanded;
2903  my($builtins_href) = shift;  # a hashref, mapping builtin macro names (single
2904                               # char) to macro values: strings or array refs
2905  my($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) =
2906    \('[', '[?', ']', '|', '#');  # lexical elements to be used as references
2907  my(%lexmap);  # maps string to reference in order to protect lexels
2908  for (keys(%$builtins_href))
2909    { $lexmap{"%$_"}  = \"%$_"; $lexmap{"%#$_"} = \"%#$_" }
2910  for ($lex_lbr, $lex_lbrq, $lex_rbr, $lex_sep, $lex_h) { $lexmap{$$_} = $_ }
2911  # parse lexically
2912  my(@tokens) = $$str_ref =~ /\G \# | \[\?? | [\]|] | % \#? . | \\ [^0-7] |
2913                          \\ [0-7]{1,3} | [^\[\]\\|%\n#]+ | [^\n]+? | \n /gcsx;
2914  # replace lexical element strings with object references,
2915  # unquote backslash-quoted characters and %%, and drop backslash-newlines
2916  my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
2917              e => "\e", a => "\a", t => "\t");
2918  for (@tokens) {
2919    if (exists $lexmap{$_}) { $_ = $lexmap{$_} }       # replace with refs
2920    elsif ($_ eq "\\\n")    { $_ = '' }                # drop \NEWLINE
2921    elsif (/^%(%)\z/)       { $_ = $1 }                #  %% -> %
2922    elsif (/^(%#?.)\z/s)    { $_ = \$1 }               # unknown builtins
2923    elsif (/^\\([0-7]{1,3})\z/) { $_ = chr(oct($1)) }  # \nnn
2924    elsif (/^\\(.)\z/s)     { $_ = (exists($esc{$1}) ? $esc{$1} : $1) }
2925  }
2926  my($level) = 0; my($quote_level) = 0; my(@macro_type, @arg);
2927  my($output_str) = ''; my($whereto) = \$output_str;
2928  while (@tokens) {
2929    my($t) = shift(@tokens);
2930    if ($t eq '') {                                    # ignore leftovers
2931    } elsif ($quote_level>0 && ref($t) && ($t == $lex_lbr || $t == $lex_lbrq)){
2932      $quote_level++;
2933      ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
2934    } elsif (ref($t) && $t == $lex_lbr) {   # begin iterator macro call
2935      $quote_level++; $level++;
2936      unshift(@arg, [[]]); unshift(@macro_type, ''); $whereto = $arg[0][0];
2937    } elsif (ref($t) && $t == $lex_lbrq) {  # begin selector macro call
2938      $level++; unshift(@arg, [[]]); unshift(@macro_type, '');
2939      $whereto = $arg[0][0]; $macro_type[0] = 'select';
2940    } elsif ($quote_level > 1 && ref($t) && $t == $lex_rbr) {
2941      $quote_level--;
2942      ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
2943    } elsif ($level > 0 && ref($t) && $t == $lex_sep) {  # next argument
2944      if ($quote_level == 0 && $macro_type[0] eq 'select' && @{$arg[0]} == 1) {
2945        $quote_level++;
2946      }
2947      if ($quote_level == 1) {
2948        unshift(@{$arg[0]}, []); $whereto = $arg[0][0];  # begin next arg
2949      } else {
2950        ref($whereto) eq 'ARRAY' ? push(@$whereto, $t) : ($$whereto .= $t);
2951      }
2952    } elsif ($quote_level > 0 && ref($t) && $t == $lex_rbr) {
2953      $quote_level--;  # quote level just dropped to 0, this is now a call
2954      $level--  if $level > 0;
2955      my(@result);
2956      if ($macro_type[0] eq 'select') {
2957        my($sel, @alternatives) = reverse @{$arg[0]};  # list of refs
2958        $sel = !ref($sel) ? '' : join('', @$sel);      # turn ref into string
2959        if    ($sel =~ /^\s*\z/)         { $sel = 0 }
2960        elsif ($sel =~ /^\s*(\d+)\s*\z/) { $sel = 0+$1 }  # make numeric
2961        else { $sel = 1 }
2962        # provide an empty second alternative if we only have one specified
2963        push(@alternatives, [])  if @alternatives < 2 && $sel > 0;
2964        if ($sel < 0) { $sel = 0 }
2965        elsif ($sel > $#alternatives) { $sel = $#alternatives }
2966        @result = @{$alternatives[$sel]};
2967      } else {                                         # iterator
2968        my($cvar_r, $sep_r, $body_r, $cvar);  # give meaning to arguments
2969        if (@{$arg[0]} >= 3) { ($cvar_r,$body_r,$sep_r) = reverse @{$arg[0]} }
2970        else { ($body_r, $sep_r) = reverse @{$arg[0]}; $cvar_r = $body_r }
2971        # find the formal argument name (iterator)
2972        for (@$cvar_r) {
2973          if (ref && $$_ =~ /^%(.)\z/s) { $cvar = $1; last }
2974        }
2975        if (exists($builtins_href->{$cvar})) {
2976          my($values_r) = $builtins_href->{$cvar};
2977          while (ref($values_r) eq 'CODE') { $values_r = &$values_r }
2978          $values_r = [$values_r]  if !ref($values_r);
2979          my($ind);
2980          my($re) = qr/^%\Q$cvar\E\z/;
2981          for my $val (@$values_r) {
2982            push(@result, @$sep_r)  if ++$ind > 1 && ref($sep_r);
2983            push(@result, map { (ref && $$_ =~ /$re/) ? $val : $_ } @$body_r);
2984          }
2985        }
2986      }
2987      shift(@macro_type);  # pop the call stack
2988      shift(@arg);
2989      $whereto = $level > 0 ? $arg[0][0] : \$output_str;
2990      unshift(@tokens, @result);  # active macro call, reevaluate result
2991    } else {  # quoted, plain string, simple macro call, or a misplaced token
2992      my($s) = '';
2993      if ($quote_level > 0 || !ref($t)) {
2994        $s = $t;  # quoted or string
2995      } elsif ($t == $lex_h) {  # discard tokens to (and including) newline
2996        while (@tokens) { last  if shift(@tokens) eq "\n" }
2997      } elsif ($$t =~ /^%\#(.)\z/s) {  # provide number of elements
2998        if (!exists($builtins_href->{$1})) { $s = 0 }  # no such
2999        else {
3000          $s = $builtins_href->{$1};
3001          while (ref($s) eq 'CODE') { $s = &$s }  # subroutine callback
3002               # for array: number of elements; for scalar: nonwhite=1, other 0
3003          $s = ref($s) ? @$s : ($s !~ /^\s*\z/);
3004        }
3005      } elsif ($$t =~ /^%(.)\z/s) {    # provide values of a builtin macro
3006        if (!exists($builtins_href->{$1})) { $s = '' }  # no such
3007        else {
3008          $s = $builtins_href->{$1};
3009          while (ref($s) eq 'CODE') { $s = &$s }  # subroutine callback
3010          $s = join(', ', @$s)  if ref $s;
3011        }
3012      } else { $s = $$t }  # misplaced token, e.g. a top level | or ]
3013      ref($whereto) eq 'ARRAY' ? push(@$whereto, $s) : ($$whereto .= $s);
3014    }
3015  }
3016  \$output_str;
3017}
3018
30191;
3020
3021#
3022package Amavis::In::Connection;
3023
3024# Keeps relevant information about how we received the message:
3025# client connection information, SMTP envelope and SMTP parameters
3026
3027use strict;
3028use re 'taint';
3029
3030BEGIN {
3031  use Exporter ();
3032  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
3033  $VERSION = '2.034';
3034  @ISA = qw(Exporter);
3035}
3036
3037sub new
3038  { my($class) = @_; bless {}, $class }
3039sub client_ip       # client IP address (immediate SMTP client, i.e. our MTA)
3040  { my($self)=shift; !@_ ? $self->{client_ip} : ($self->{client_ip}=shift) }
3041sub socket_ip       # IP address of our interface that received connection
3042  { my($self)=shift; !@_ ? $self->{socket_ip} : ($self->{socket_ip}=shift) }
3043sub socket_port     # TCP port of our interface that received connection
3044  { my($self)=shift; !@_ ? $self->{socket_port}:($self->{socket_port}=shift) }
3045sub proto           # TCP/UNIX
3046  { my($self)=shift; !@_ ? $self->{proto}     : ($self->{proto}=shift) }
3047sub smtp_proto      # SMTP/ESMTP(A|S|SA)/LMTP(A|S|SA) # rfc3848, or QMQP/QMQPqq
3048  { my($self)=shift; !@_ ? $self->{smtp_proto}: ($self->{smtp_proto}=shift) }
3049sub smtp_helo       # (E)SMTP HELO/EHLO parameter
3050  { my($self)=shift; !@_ ? $self->{smtp_helo} : ($self->{smtp_helo}=shift) }
3051
30521;
3053
3054#
3055package Amavis::In::Message::PerRecip;
3056
3057use strict;
3058use re 'taint';
3059
3060BEGIN {
3061  use Exporter ();
3062  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
3063  $VERSION = '2.034';
3064  @ISA = qw(Exporter);
3065}
3066
3067# per-recipient data are kept in an array of n-tuples:
3068#   (recipient-address, destiny, done, smtp-response-text, remote-mta, ...)
3069sub new     # NOTE: this class is a list, not a hash
3070  { my($class) = @_; bless [(undef) x 11], $class }
3071
3072# subs to set or access individual elements of a n-tuple by name
3073sub recip_addr       # recipient envelope e-mail address
3074  { my($self)=shift; !@_ ? $$self[0] : ($$self[0]=shift) }
3075sub recip_addr_modified
3076  { my($self)=shift; !@_ ? $$self[1] : ($$self[1]=shift) }
3077sub recip_destiny    # D_REJECT, D_BOUNCE, D_DISCARD, D_PASS
3078  { my($self)=shift; !@_ ? $$self[2] : ($$self[2]=shift) }
3079sub recip_done       # false: not done, true: done (1: faked, 2: truly sent)
3080  { my($self)=shift; !@_ ? $$self[3] : ($$self[3]=shift) }
3081sub recip_smtp_response # rfc2821 response (3-digit + enhanced resp + text)
3082  { my($self)=shift; !@_ ? $$self[4] : ($$self[4]=shift) }
3083sub recip_remote_mta_smtp_response  # smtp response as issued by remote MTA
3084  { my($self)=shift; !@_ ? $$self[5] : ($$self[5]=shift) }
3085sub recip_remote_mta # remote MTA that issued the smtp response
3086  { my($self)=shift; !@_ ? $$self[6] : ($$self[6]=shift) }
3087sub recip_mbxname    # mailbox name or file when known ('local:' or 'bsmtp:')
3088  { my($self)=shift; !@_ ? $$self[7] : ($$self[7]=shift) }
3089sub recip_whitelisted_sender  # recip considers this sender whitelisted (> 0)
3090  { my($self)=shift; !@_ ? $$self[8] : ($$self[8]=shift) }
3091sub recip_blacklisted_sender  # recip considers this sender blacklisted
3092  { my($self)=shift; !@_ ? $$self[9] : ($$self[9]=shift) }
3093sub recip_score_boost  # recip adds penalty spam points to the final score
3094  { my($self)=shift; !@_ ? $$self[10] : ($$self[10]=shift) }
3095
3096sub recip_final_addr {  # return recip_addr_modified if set, else recip_addr
3097  my($self)=shift;
3098  my($newaddr) = $self->recip_addr_modified;
3099  defined $newaddr ? $newaddr : $self->recip_addr;
3100}
3101
31021;
3103
3104#
3105package Amavis::In::Message;
3106# the main purpose of this class is to contain information
3107# about the message being processed
3108
3109use strict;
3110use re 'taint';
3111
3112BEGIN {
3113  use Exporter ();
3114  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
3115  $VERSION = '2.034';
3116  @ISA = qw(Exporter);
3117}
3118
3119BEGIN {
3120  import Amavis::Conf qw( :platform );
3121  import Amavis::rfc2821_2822_Tools qw(rfc2822_timestamp);
3122  import Amavis::In::Message::PerRecip;
3123}
3124
3125sub new
3126  { my($class) = @_; bless {}, $class }
3127sub rx_time         # Unix time (s since epoch) of message reception by amavisd
3128  { my($self)=shift; !@_ ? $self->{rx_time}    : ($self->{rx_time}=shift) }
3129sub client_addr     # original client IP addr, obtained from XFORWARD or milter
3130  { my($self)=shift; !@_ ? $self->{cli_ip} : ($self->{cli_ip}=shift) }
3131sub client_name     # orig. client DNS name, obtained from XFORWARD or milter
3132  { my($self)=shift; !@_ ? $self->{cli_name} : ($self->{cli_name}=shift) }
3133sub client_proto     # orig. client protocol, obtained from XFORWARD or milter
3134  { my($self)=shift; !@_ ? $self->{cli_proto} : ($self->{cli_proto}=shift) }
3135sub client_helo     # orig. client EHLO name, obtained from XFORWARD or milter
3136  { my($self)=shift; !@_ ? $self->{cli_helo} : ($self->{cli_helo}=shift) }
3137sub queue_id        # MTA queue ID of message if known (Courier, milter/AM.PDP)
3138  { my($self)=shift; !@_ ? $self->{queue_id}   : ($self->{queue_id}=shift) }
3139sub msg_size        # ESMTP SIZE value, later corrected by actual message size
3140  { my($self)=shift; !@_ ? $self->{msg_size}   : ($self->{msg_size}=shift) }
3141sub auth_user       # ESMTP AUTH username
3142  { my($self)=shift; !@_ ? $self->{auth_user}  : ($self->{auth_user}=shift) }
3143sub auth_pass       # ESMTP AUTH password
3144  { my($self)=shift; !@_ ? $self->{auth_pass}  : ($self->{auth_pass}=shift) }
3145sub auth_submitter  # ESMTP MAIL command AUTH option value
3146  { my($self)=shift; !@_ ? $self->{auth_subm}  : ($self->{auth_subm}=shift) }
3147sub body_type       # ESMTP BODY parameter value
3148  { my($self)=shift; !@_ ? $self->{body_type}  : ($self->{body_type}=shift) }
3149sub sender          # envelope sender
3150  { my($self)=shift; !@_ ? $self->{sender}     : ($self->{sender}=shift) }
3151sub sender_contact  # unmangled sender address or undef (e.g. believed faked)
3152  { my($self)=shift; !@_ ? $self->{sender_c}   : ($self->{sender_c}=shift) }
3153sub sender_source   # unmangled sender address or info from the trace
3154  { my($self)=shift; !@_ ? $self->{sender_src} : ($self->{sender_src}=shift) }
3155sub mime_entity     # MIME::Parser entity holding the message
3156  { my($self)=shift; !@_ ? $self->{mime_entity}: ($self->{mime_entity}=shift)}
3157sub parts_root      # Amavis::Unpackers::Part root object
3158  { my($self)=shift; !@_ ? $self->{parts_root}:  ($self->{parts_root}=shift)}
3159sub mail_text       # rfc2822 msg: (open) file handle, or MIME::Entity object
3160  { my($self)=shift; !@_ ? $self->{mail_text}  : ($self->{mail_text}=shift) }
3161sub mail_text_fn    # orig. mail filename or undef, e.g. mail_tempdir/email.txt
3162  { my($self)=shift; !@_ ? $self->{mail_text_fn} : ($self->{mail_text_fn}=shift) }
3163sub mail_tempdir    # work directory, either $TEMPBASE or supplied by client
3164  { my($self)=shift; !@_ ? $self->{mail_tempdir} : ($self->{mail_tempdir}=shift) }
3165sub header_edits    # Amavis::Out::EditHeader object or undef
3166  { my($self)=shift; !@_ ? $self->{hdr_edits}  : ($self->{hdr_edits}=shift) }
3167sub orig_header     # original header - an arrayref of lines, with trailing LF
3168  { my($self)=shift; !@_ ? $self->{orig_header}: ($self->{orig_header}=shift) }
3169sub orig_header_size # size of original header
3170  { my($self)=shift; !@_ ? $self->{orig_hdr_s} : ($self->{orig_hdr_s}=shift) }
3171sub orig_body_size  # size of original body
3172  { my($self)=shift; !@_ ? $self->{orig_bdy_s} : ($self->{orig_bdy_s}=shift) }
3173sub body_digest     # message digest of message body
3174  { my($self)=shift; !@_ ? $self->{body_digest}: ($self->{body_digest}=shift) }
3175sub quarantined_to  # list of quarantine mailbox names or addresses if quarantined
3176  { my($self)=shift; !@_ ? $self->{quarantine} : ($self->{quarantine}=shift) }
3177sub dsn_sent        # delivery status notification was sent(1) or faked(2)
3178  { my($self)=shift; !@_ ? $self->{dsn_sent}   : ($self->{dsn_sent}=shift) }
3179sub delivery_method # delivery method, or empty for implicit delivery (milter)
3180  { my($self)=shift; !@_ ? $self->{delivery_method} : ($self->{delivery_method}=shift) }
3181sub client_delete   # don't delete the tempdir, it is a client's reponsibility
3182  { my($self)=shift; !@_ ? $self->{client_delete} : ($self->{client_delete}=shift) }
3183
3184# The order of entries in the list is the original order in which
3185# recipient addresses (e.g. obtained via 'MAIL TO:') were received.
3186# Only the entries that were accepted (via SMTP response code 2xx)
3187# are placed in the list. The ORDER MUST BE PRESERVED and no recipients
3188# may be added or removed from the list! This is vital in order to be able
3189# to produce correct per-recipient responses to a LMTP client!
3190# 'destiny' values match the meaning of 'final_*_destiny'
3191
3192sub per_recip_data {  # get or set a listref of envelope recipient n-tuples
3193  my($self) = shift;
3194  # store a given listref of n-tuples (originals, not copies!)
3195  if (@_) { @{$self->{recips}} = @{$_[0]} }
3196  # return a listref to the original n-tuples,
3197  # caller may modify the data if he knows what he is doing
3198  $self->{recips};
3199}
3200
3201sub recips {           # get or set a listref of envelope recipients
3202  my($self)=shift;
3203  if (@_) {  # store a copy of a given listref of recipient addresses
3204    # wrap scalars (strings) into n-tuples
3205    $self->per_recip_data([ map {
3206      my($per_recip_obj) = Amavis::In::Message::PerRecip->new;
3207      $per_recip_obj->recip_addr($_);
3208      $per_recip_obj->recip_destiny(D_PASS);  # default is Pass
3209      $per_recip_obj } @{$_[0]} ]);
3210  }
3211  return  if !defined wantarray;  # don't bother
3212  # return listref of recipient addresses
3213  [ map { $_->recip_addr } @{$self->per_recip_data} ];
3214}
3215
32161;
3217
3218#
3219package Amavis::Out::EditHeader;
3220
3221# Accumulates instructions on what lines need to be added to the message
3222# header, deleted, or how to change existing lines, then via a call
3223# to write_header() performs these edits on the fly.
3224
3225use strict;
3226use re 'taint';
3227
3228BEGIN {
3229  use Exporter ();
3230  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
3231  $VERSION = '2.034';
3232  @ISA = qw(Exporter);
3233  @EXPORT_OK = qw(&hdr);
3234}
3235
3236BEGIN {
3237  import Amavis::Conf qw(:platform c cr ca);
3238  import Amavis::Timing qw(section_time);
3239  import Amavis::Util qw(ll do_log safe_encode q_encode);
3240}
3241use MIME::Words;
3242
3243sub new { my($class) = @_; bless {}, $class }
3244
3245sub prepend_header($$$;$) {
3246  my($self, $field_name, $field_body, $structured) = @_;
3247  unshift(@{$self->{prepend}}, hdr($field_name, $field_body, $structured));
3248}
3249
3250sub append_header($$$;$) {
3251  my($self, $field_name, $field_body, $structured) = @_;
3252  push(@{$self->{append}}, hdr($field_name, $field_body, $structured));
3253}
3254
3255sub delete_header($$) {
3256  my($self, $field_name) = @_;
3257  $self->{edit}{lc($field_name)} = undef;
3258}
3259
3260sub edit_header($$$;$) {
3261  my($self, $field_name, $field_edit_sub, $structured) = @_;
3262  # $field_edit_sub will be called with 2 args: field name and field body;
3263  # it should return the replacement field body (no field name and colon),
3264  # with or without the trailing NL
3265  !defined($field_edit_sub) || ref($field_edit_sub) eq 'CODE'
3266    or die "edit_header: arg#3 must be undef or a subroutine ref";
3267  $self->{edit}{lc($field_name)} = $field_edit_sub;
3268}
3269
3270# copy all header edits from another header-edits object into this one
3271sub inherit_header_edits($$) {
3272  my($self, $other_edits) = @_;
3273  if (defined $other_edits) {
3274    unshift(@{$self->{prepend}},
3275            @{$other_edits->{prepend}})  if $other_edits->{prepend};
3276    unshift(@{$self->{append}},
3277            @{$other_edits->{append}})   if $other_edits->{append};
3278    if ($other_edits->{edit}) {
3279      for (keys %{$other_edits->{edit}})
3280        { $self->{edit}{$_} = $other_edits->{edit}{$_} }
3281    }
3282  }
3283}
3284
3285# Insert space after colon if not present, RFC2047-encode if field body
3286# contains non-ASCII characters, fold long lines if needed,
3287# prepend space before each NL if missing, append NL if missing;
3288# Header fields with only spaces are not allowed.
3289# (rfc2822: Each line of characters MUST be no more than 998 characters,
3290# and SHOULD be no more than 78 characters, excluding the CRLF.
3291# '$structured' indicates that folding is only allowed at positions
3292# indicated by \n in the provided header body.
3293#
3294sub hdr($$;$) {
3295  my($field_name, $field_body, $structured) = @_;
3296  if ($field_name =~ /^(X-.*|Subject|Comments)\z/si &&
3297      $field_body =~ /[^\011\012\040-\176]/ #any nonprintable except TAB and LF
3298  ) { # encode according to RFC 2047
3299    $field_body =~ s/\n([ \t])/$1/g;  # unfold
3300    chomp($field_body);
3301    my($field_body_octets) = safe_encode(c('hdr_encoding'), $field_body);
3302    my($qb) = c('hdr_encoding_qb');
3303    if (uc($qb) eq 'Q') {
3304      $field_body = q_encode($field_body_octets, $qb, c('hdr_encoding'));
3305    } else {
3306      $field_body = MIME::Words::encode_mimeword($field_body_octets,
3307                                                 $qb, c('hdr_encoding'));
3308    }
3309  } else {  # supposed to be in plain ASCII, let's make sure it is
3310    $field_body = safe_encode('ascii', $field_body);
3311  }
3312  $field_name = safe_encode('ascii', $field_name);
3313  my($str) = $field_name . ':';
3314  $str .= ' '  if $field_body !~ /^[ \t]/;
3315  $str .= $field_body;
3316  $str =~ s/\n([^ \t\n])/\n $1/g;  # insert a space at line folds if missing
3317  $str =~ s/\n([ \t]*\n)+/\n/g;    # remove empty lines
3318  chomp($str);                     # chop off trailing NL if present
3319  if ($structured) {
3320    my(@sublines) = split(/\n/, $str, -1);
3321    $str = ''; my($s) = ''; my($s_l) = 0;
3322    for (@sublines) {              # join shorter field sections
3323      if ($s !~ /^\s*\z/ && $s_l + length($_) > 78) {
3324        $str .= "\n"  if $str ne '';
3325        $str .= $s; $s = ''; $s_l = 0;
3326      }
3327      $s .= $_; $s_l += length($_);
3328    }
3329    if ($s !~ /^\s*\z/) {
3330      $str .= "\n"  if $str ne '';
3331      $str .= $s;
3332    }
3333  } elsif (length($str) > 998) {
3334    # truncate the damn thing (to be done better)
3335    $str = substr($str,0,998);
3336  }
3337  $str .= "\n";  # append final NL
3338  do_log(5, "header: $str");
3339  $str;
3340}
3341
3342# Copy mail header to the supplied method (line by line) while adding,
3343# removing, or changing certain header lines as required, and append
3344# an empty line (end-of-header). Returns number of original 'Received:'
3345# header fields to make simple loop detection possible (as required
3346# by rfc2821 section 6.2).
3347#
3348# Assumes input file is properly positioned, leaves it positioned
3349# at the beginning of the body.
3350#
3351sub write_header($$$) {
3352  my($self, $msg, $out_fh) = @_;
3353  $out_fh = IO::Wrap::wraphandle($out_fh);  # assure an IO::Handle-like obj
3354  my($is_mime) = ref($msg) && $msg->isa('MIME::Entity');
3355  my(@header);
3356  if ($is_mime) {
3357    @header = map { /^[ \t]*\n?\z/ ? ()   # remove empty lines, ensure NL
3358                                 : (/\n\z/ ? $_ : $_ . "\n") } @{$msg->header};
3359  }
3360  my($received_cnt) = 0; my($str) = '';
3361  for (@{$self->{prepend}}) { $str .= $_ }
3362  if ($str ne '') { $out_fh->print($str) or die "sending mail header1: $!" }
3363  if (!defined($msg)) {
3364    # existing header empty
3365  } elsif (!exists($self->{edit}) || !scalar(%{$self->{edit}})) {
3366    # no edits needed, do it the fast way
3367    if ($is_mime) {
3368      # NOTE: can't use method print_header, as it assumes file glob
3369      for my $h (@header)
3370        { $out_fh->print($h) or die "sending mail header2: $!" }
3371    } else {  # assume file handle
3372      while (<$msg>) {  # copy header only, read line by line
3373        last  if $_ eq $eol;  # end of header
3374        $out_fh->print($_) or die "sending mail header3: $!";
3375      }
3376    }
3377  } else {  # header edits are required
3378    my($curr_head, $next_head);
3379    push(@header, $eol)  if $is_mime;  # append empty line as end-of-header
3380    while (defined($next_head = $is_mime ? shift @header : <$msg>)) {
3381      if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head }  # folded
3382      else {                                                    # new header
3383        if (!defined($curr_head)) {  # no previous complete header field
3384        } elsif ($curr_head !~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s) {
3385          # invalid header, but we don't care
3386          $out_fh->print($curr_head) or die "sending mail header4: $!";
3387        } else {                     # count, edit, or delete
3388            # obsolete rfc822 syntax allowed whitespace before colon
3389          my($field_name, $field_body) = ($1, $2);
3390          my($field_name_lc) = lc($field_name);
3391          $received_cnt++  if $field_name_lc eq 'received';
3392          if (!exists($self->{edit}{$field_name_lc})) { # unchanged
3393            $out_fh->print($curr_head) or die "sending mail header5: $!";
3394          } else {
3395            my($edit) = $self->{edit}{$field_name_lc};
3396            if (defined($edit)) {                       # edit, not delete
3397              chomp($field_body);
3398              ### $field_body =~ s/\n([ \t])/$1/g;  # unfold
3399              $out_fh->print(hdr($field_name, &$edit($field_name,$field_body)))
3400                or die "sending mail header6: $!";
3401            }
3402          }
3403        }
3404        last  if $next_head eq $eol;                    # end-of-header reached
3405        $curr_head = $next_head;
3406      }
3407    }
3408  }
3409  $str = '';
3410  for (@{$self->{append}}) { $str .= $_ }
3411  $str .= $eol;  # end of header - separator line
3412  $out_fh->print($str) or die "sending mail header7: $!";
3413  section_time('write-header');
3414  $received_cnt;
3415}
34161;
3417
3418#
3419package Amavis::Out::Local;
3420use strict;
3421use re 'taint';
3422
3423BEGIN {
3424  use Exporter ();
3425  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
3426  $VERSION = '2.034';
3427  @ISA = qw(Exporter);
3428  @EXPORT_OK = qw(&mail_to_local_mailbox);
3429}
3430
3431use Errno qw(ENOENT EACCES);
3432use POSIX qw(strftime);
3433use IO::File ();
3434use IO::Wrap;
3435
3436BEGIN {
3437  import Amavis::Conf qw(:platform $gzip $bzip2 c cr ca);
3438  import Amavis::Lock;
3439  import Amavis::Util qw(ll do_log am_id exit_status_str run_command_consumer);
3440  import Amavis::Timing qw(section_time);
3441  import Amavis::rfc2821_2822_Tools;
3442  import Amavis::Out::EditHeader;
3443}
3444
3445use subs @EXPORT_OK;
3446
3447# Deliver to local mailboxes only, ignore the rest: either to directory
3448# (maildir style), or file (Unix mbox).  (normally used as a quarantine method)
3449#
3450sub mail_to_local_mailbox(@) {
3451  my($via, $msginfo, $initial_submission, $filter) = @_;
3452  $via =~ /^local:(.*)\z/si or die "Bad local method: $via";
3453  my($via_arg) = $1;
3454  my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
3455                             @{$msginfo->per_recip_data};
3456  return 1  if !@per_recip_data;
3457  my($msg) = $msginfo->mail_text;      # a file handle or a MIME::Entity object
3458  if (defined($msg) && !$msg->isa('MIME::Entity')) {
3459    # at this point, we have no idea what the user gave us...
3460    # a globref? a FileHandle?
3461    $msg = IO::Wrap::wraphandle($msg); # now we have an IO::Handle-like obj
3462  }
3463  my($sender) = $msginfo->sender;
3464  for my $r (@per_recip_data) {
3465    # each one receives its own copy; these are not the original recipients
3466    my($recip) = $r->recip_final_addr;
3467    next  if $recip eq '';
3468    my($localpart,$domain) = split_address($recip);
3469    my($smtp_response);
3470
3471    # %local_delivery_aliases emulates aliases map - this would otherwise
3472    # be done by MTA's local delivery agent if we gave the message to MTA.
3473    # This way we keep interface compatible with other mail delivery
3474    # methods. The hash value may be a ref to a pair of fixed strings,
3475    # or a subroutine ref (which must return such pair) to allow delayed
3476    # (lazy) evaluation when some part of the pair is not yet known
3477    # at initialization time.
3478    # If no matching entry is found, the key ($localpart) is treated as
3479    # a mailbox filename if nonempty, or else quarantining is skipped.
3480
3481    my($mbxname, $suggested_filename);
3482    { # a block is used as a 'switch' statement - 'last' will exit from it
3483      my($ldar) = cr('local_delivery_aliases');  # a ref to a hash
3484      my($alias) = $ldar->{$localpart};
3485      if (ref($alias) eq 'ARRAY') {
3486        ($mbxname, $suggested_filename) = @$alias;
3487      } elsif (ref($alias) eq 'CODE') {  # lazy evaluation
3488        ($mbxname, $suggested_filename) = &$alias;
3489      } elsif ($alias ne '') {
3490        ($mbxname, $suggested_filename) = ($alias, undef);
3491      } elsif (!exists $ldar->{$localpart}) {
3492        do_log(0, "no key '$localpart' in \%local_delivery_aliases, skip local delivery");
3493      }
3494      if ($mbxname eq '') {
3495        my($why) = !exists $ldar->{$localpart} ? 1 : $alias eq '' ? 2 : 3;
3496        do_log(2, "skip local delivery($why): <$sender> -> <$recip>");
3497        $smtp_response = "250 2.6.0 Ok, skip local delivery($why)";
3498        last;   # exit block, not the loop
3499      }
3500      my($ux);  # is it a UNIX-style mailbox?
3501      if (!-d $mbxname) {  # assume a filename (need not exist yet)
3502        $ux = 1;           # $mbxname is a UNIX-style mailbox (one file)
3503      } else {             # a directory
3504        $ux = 0;  # $mbxname is a amavis/maildir style mailbox (a directory)
3505        if ($suggested_filename eq '')
3506          { $suggested_filename = $via_arg ne '' ? $via_arg : 'msg-%i-%n' }
3507        $suggested_filename =~ s{%(.)}
3508          {  $1 eq 'b' ? $msginfo->body_digest
3509           : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time))
3510           : $1 eq 'n' ? am_id()
3511           : $1 eq '%' ? '%' : '%'.$1 }egs;
3512        # one mail per file, will create specified file
3513        $mbxname = "$mbxname/$suggested_filename";
3514      }
3515      do_log(1, "local delivery: <$sender> -> <$recip>, mbx=$mbxname");
3516      my($mp,$pos,$pipe,$pid);
3517      my($errn) = stat($mbxname) ? 0 : 0+$!;
3518      local $SIG{CHLD} = 'DEFAULT';
3519      local $SIG{PIPE} = 'IGNORE';  # write to broken pipe would throw a signal
3520      eval {                        # try to open the mailbox file for writing
3521        if (!$ux) {                 # new file, traditional amavis, or maildir
3522          if ($errn == ENOENT) {    # good, no file, as expected
3523          } elsif (!$errn && -e _)
3524            { die "File $mbxname already exists, refuse to overwrite" }
3525          if ($mbxname =~ /\.gz\z/) {
3526            ($mp,$pid) = run_command_consumer($mbxname,undef,$gzip);
3527            $pipe = 1;
3528          } else {
3529            $mp = IO::File->new;
3530            $mp->open($mbxname,'>',0640)
3531              or die "Can't create file $mbxname: $!";
3532          }
3533        } else {  # append to UNIX-style mailbox
3534                  # deliver only to non-executable regular files
3535          if ($errn == ENOENT) {
3536            $mp = IO::File->new;
3537            $mp->open($mbxname,'>',0640)
3538              or die "Can't create file $mbxname: $!";
3539          } elsif (!$errn && !-f _) {
3540            die "Mailbox $mbxname is not a regular file, refuse to deliver";
3541          } elsif (-x _ || -X _) {
3542            die "Mailbox file $mbxname is executable, refuse to deliver";
3543          } else {
3544            $mp = IO::File->new;
3545            $mp->open($mbxname,'>>',0640) or die "Can't append to $mbxname: $!";
3546          }
3547          binmode($mp, ":bytes") or die "Can't cancel :utf8 mode: $!"
3548            if $unicode_aware;
3549          lock($mp);
3550          $mp->seek(0,2) or die "Can't position mailbox file to its tail: $!";
3551          $pos = $mp->tell;
3552        }
3553        if (defined($msg) && !$msg->isa('MIME::Entity'))
3554          { $msg->seek(0,0) or die "Can't rewind mail file: $!" }
3555      };
3556      if ($@ ne '') {
3557        chomp($@);
3558        $smtp_response = $@ eq "timed out" ? "450 4.4.2" : "451 4.5.0";
3559        $smtp_response .= " Local delivery(1) to $mbxname failed: $@";
3560        last;          # exit block, not the loop
3561      }
3562      eval {  # if things fail from here on, try to restore mailbox state
3563        if ($ux) {
3564          $mp->printf("From %s  %s$eol", quote_rfc2821_local($sender),
3565                      scalar(localtime($msginfo->rx_time)) )
3566            or die "Can't write to $mbxname: $!";
3567        }
3568        my($hdr_edits) = $msginfo->header_edits;
3569        if (!$hdr_edits) {
3570          $hdr_edits = Amavis::Out::EditHeader->new;
3571          $msginfo->header_edits($hdr_edits);
3572        }
3573        $hdr_edits->delete_header('Return-Path');
3574        $hdr_edits->prepend_header('Delivered-To',
3575          quote_rfc2821_local($recip));
3576        $hdr_edits->prepend_header('Return-Path',
3577          qquote_rfc2821_local($sender));
3578        my($received_cnt) = $hdr_edits->write_header($msg,$mp);
3579        if ($received_cnt > 110) {
3580          # loop detection required by rfc2821 section 6.2
3581          # Do not modify the signal text, it gets matched elsewhere!
3582          die "Too many hops: $received_cnt 'Received:' header lines\n";
3583        }
3584        if (!$ux) {  # do it in blocks for speed if we can
3585          while ($msg->read($_,16384) > 0)
3586            { $mp->print($_) or die "Can't write to $mbxname: $!" }
3587        } else {     # for UNIX-style mailbox delivery: escape 'From '
3588          my($blank_line) = 1;
3589          while (<$msg>) {
3590            $mp->print('>') or die "Can't write to $mbxname: $!"
3591              if $blank_line && /^From /;
3592            $mp->print($_) or die "Can't write to $mbxname: $!";
3593            $blank_line = $_ eq $eol;
3594          }
3595        }
3596        # must append an empty line for a Unix mailbox format
3597        $mp->print($eol) or die "Can't write to $mbxname: $!"  if $ux;
3598      };
3599      my($failed) = 0;
3600      if ($@ ne '') {  # trouble
3601        chomp($@);
3602        if ($ux && defined($pos) && $can_truncate) {
3603          # try to restore UNIX-style mailbox to previous size;
3604          # Produces a fatal error if truncate isn't implemented
3605          # on your system.
3606          $mp->truncate($pos) or die "Can't truncate file $mbxname: $!";
3607        }
3608        $failed = 1;
3609      }
3610      unlock($mp)  if $ux;
3611      if (!$pipe) {
3612        $mp->close or die "Can't close $mbxname: $!";
3613      } else {
3614        my($err); $mp->close or $err = $!;
3615        $?==0 or die ("Closing pipe to $gzip: ".exit_status_str($?,$err));
3616      }
3617      if (!$failed) {
3618        $smtp_response = "250 2.6.0 Ok, delivered to $mbxname";
3619      } elsif ($@ eq "timed out") {
3620        $smtp_response = "450 4.4.2 Local delivery to $mbxname timed out";
3621      } elsif ($@ =~ /too many hops/i) {
3622        $smtp_response = "550 5.4.6 Rejected delivery to mailbox $mbxname: $@";
3623      } else {
3624        $smtp_response = "451 4.5.0 Local delivery to mailbox $mbxname failed: $@";
3625      }
3626    }  # end of block, 'last' within block brings us here
3627    do_log(-1, $smtp_response)  if $smtp_response !~ /^2/;
3628    $smtp_response .= ", id=" . am_id();
3629    $r->recip_smtp_response($smtp_response); $r->recip_done(2);
3630    $r->recip_mbxname($mbxname)  if $mbxname ne '' && $smtp_response =~ /^2/;
3631  }
3632  section_time('save-to-local-mailbox');
3633}
3634
36351;
3636
3637#
3638package Amavis::Out;
3639use strict;
3640use re 'taint';
3641
3642BEGIN {
3643  use Exporter ();
3644  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
3645  $VERSION = '2.034';
3646  @ISA = qw(Exporter);
3647  %EXPORT_TAGS = ();
3648  @EXPORT = qw(&mail_dispatch);
3649}
3650
3651use IO::File ();
3652use IO::Wrap;
3653use Net::Cmd;
3654use Net::SMTP 2.24;
3655# use Authen::SASL;
3656use POSIX qw(strftime
3657             WIFEXITED WIFSIGNALED WIFSTOPPED
3658             WEXITSTATUS WTERMSIG WSTOPSIG);
3659BEGIN {
3660  import Amavis::Conf qw(:platform $DEBUG $QUARANTINEDIR
3661                         $relayhost_is_client c cr ca);
3662  import Amavis::Util qw(untaint min max ll do_log debug_oneshot
3663                         am_id snmp_count retcode exit_status_str
3664                         prolong_timer run_command_consumer);
3665  import Amavis::Timing qw(section_time);
3666  import Amavis::rfc2821_2822_Tools;
3667  import Amavis::Out::Local qw(mail_to_local_mailbox);
3668  import Amavis::Out::EditHeader;
3669}
3670
3671# modify delivery method string if $relayhost_is_client and mail came in by TCP
3672sub dynamic_destination($$) {
3673  my($method,$conn) = @_;
3674  my($client_ip) = !defined($conn) ? undef : $conn->client_ip;
3675  if ($client_ip ne '' && $method =~ /^smtp\b/i) {
3676    my($new_method); my($relayhost,$relayhost_port,$rest);
3677    (undef,$relayhost,$relayhost_port,$rest) = split(/:/,$method,4);
3678    if ($relayhost_is_client)  # old style
3679      { ($relayhost,$relayhost_port,$rest) = ('*','*','') }
3680    $relayhost      = "[$client_ip]"        if $relayhost eq '*';
3681    $relayhost_port = $conn->socket_port+1  if $relayhost_port eq '*';
3682    $rest = ':'.$rest  if $rest ne '';
3683    $new_method = sprintf("smtp:%s:%s%s", $relayhost,$relayhost_port,$rest);
3684    if ($new_method ne $method) {
3685      do_log(3, "dynamic destination override: $method -> $new_method");
3686      $method = $new_method;
3687    }
3688  }
3689  $method;
3690}
3691
3692sub mail_dispatch($$$;$) {
3693  my($conn) = shift;  my($msginfo,$initial_submission,$filter) = @_;
3694  my($via) = $msginfo->delivery_method;
3695  if ($via =~ /^smtp:/i) {
3696    mail_via_smtp(dynamic_destination($via,$conn), @_);
3697  } elsif ($via =~ /^pipe:/i) {
3698    mail_via_pipe($via, @_);
3699  } elsif ($via =~ /^bsmtp:/i) {
3700    mail_via_bsmtp($via, @_);
3701  } elsif ($via =~ /^local:/i) {
3702    # 'local:' is used by the quarantine code to relieve it
3703    # of the need to know which delivery method needs to be used.
3704    # Deliver first what is local (whatever does not contain '@')
3705    mail_to_local_mailbox($via, $msginfo, $initial_submission,
3706                          sub { shift->recip_final_addr !~ /\@/ ? 1 : 0 });
3707    if (grep { !$_->recip_done } @{$msginfo->per_recip_data}) {
3708      my($nm) = c('notify_method');  # deliver the rest
3709      if ($nm =~ /^smtp:/i) { mail_via_smtp(dynamic_destination($nm,$conn),@_)}
3710      elsif ($nm =~ /^pipe:/i)  { mail_via_pipe($nm, @_) }
3711      elsif ($nm =~ /^bsmtp:/i) { mail_via_bsmtp($nm, @_) }
3712    }
3713  }
3714}
3715
3716#sub Net::Cmd::debug_print {
3717#  my($cmd,$out,$text) = @_;
3718#  do_log(0, "*** ".$cmd->debug_text($out,$text))  if $out;
3719#}
3720
3721# trivial OO wrapper around Net::SMTP::datasend
3722sub new_smtp_data { my($class, $sh) = @_; bless \$sh, $class }
3723
3724sub print {
3725  my($self) = shift;
3726  $$self->datasend(\@_)  # datasend may be given an array ref
3727    or die "datasend timed out while sending header\n";
3728}
3729
3730# Send mail using SMTP - do multiple transactions if necessary
3731# (e.g. due to '452 Too many recipients')
3732#
3733sub mail_via_smtp(@) {
3734  my($via, $msginfo, $initial_submission, $filter) = @_;
3735  my($num_recips_undone) =
3736    scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) }
3737                @{$msginfo->per_recip_data});
3738  while ($num_recips_undone > 0) {
3739    mail_via_smtp_single(@_);  # send what we can in one transaction
3740    my($num_recips_undone_after) =
3741      scalar(grep { !$_->recip_done && (!$filter || &$filter($_)) }
3742                  @{$msginfo->per_recip_data});
3743    if ($num_recips_undone_after >= $num_recips_undone) {
3744      do_log(-2, "TROUBLE: Number of recipients ($num_recips_undone_after) "
3745                 . "not reduced in SMTP transaction, abandon the effort");
3746      last;
3747    }
3748    if ($num_recips_undone_after > 0) {
3749      do_log(1, sprintf("Sent to %s recipients via SMTP, %s still to go",
3750                        $num_recips_undone - $num_recips_undone_after,
3751                        $num_recips_undone_after));
3752    }
3753    $num_recips_undone = $num_recips_undone_after;
3754  }
3755  1;
3756}
3757
3758# Send mail using SMTP - single transaction
3759# (e.g. forwarding original mail or sending notification)
3760# May throw exception (die) if temporary failure (4xx) or other problem
3761#
3762sub mail_via_smtp_single(@) {
3763  my($via, $msginfo, $initial_submission, $filter) = @_;
3764  my($which_section) = 'fwd_init';
3765  snmp_count('OutMsgs');
3766  local($1,$2,$3);  # avoid Perl taint bug, still in 5.8.3
3767  $via =~ /^smtp: (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) /six
3768    or die "Bad fwd method syntax: $via";
3769  my($relayhost, $relayhost_port) = ($1.$2, $3);
3770  my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
3771                             @{$msginfo->per_recip_data};
3772  my($logmsg) = sprintf("%s via SMTP: [%s]:%s <%s>",
3773                        ($initial_submission ? 'SEND' : 'FWD'),
3774                        $relayhost, $relayhost_port, $msginfo->sender);
3775  if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 }
3776  do_log(1, $logmsg . " -> " .
3777            qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data));
3778  my($msg) = $msginfo->mail_text;  # a file handle or a MIME::Entity object
3779  my($smtp_handle, $smtp_response); my($smtp_code, $smtp_msg, $received_cnt);
3780  my($any_valid_recips) = 0; my($any_tempfail_recips) = 0;
3781  my($any_valid_recips_and_data_sent) = 0; my($in_datasend_mode) = 0;
3782  if (defined($msg) && !$msg->isa('MIME::Entity')) {
3783    # at this point, we have no idea what the user gave us...
3784    # a globref? a FileHandle?
3785    $msg = IO::Wrap::wraphandle($msg);  # now we have an IO::Handle-like obj
3786    $msg->seek(0,0) or die "Can't rewind mail file: $!";
3787  }
3788  # NOTE: Net::SMTP uses alarm to do its own timing.
3789  #       We need to restart our timer when Net::SMTP is done using it !!!
3790  my($remaining_time) = alarm(0);  # check how much time is left, stop timer
3791  eval {
3792    $which_section = 'fwd-connect';
3793    # Timeout should be more than MTA normally takes to check DNS and RBL,
3794    # which may take a minute or more in case of unreachable DNS.
3795    # Specifying shorter timeout will cause alarm to terminate the wait
3796    # for SMTP status line prematurely, resulting in status code 000.
3797    # rfc2821 (section 4.5.3.2) requires timeout to be at least 5 minutes
3798    my($localaddr) = c('local_client_bind_address');  # IP assigned to socket
3799    my($heloname)  = c('localhost_name');      # host name used in HELO/EHLO
3800    $smtp_handle = Net::SMTP->new($relayhost, Port => $relayhost_port,
3801      ($localaddr eq '' ? () : (LocalAddr => $localaddr)),
3802      ($heloname  eq '' ? () : (Hello     => $heloname)),
3803      ExactAddresses => 1,
3804      Timeout => max(60, min(5 * 60, $remaining_time)),  # for each operation
3805#     Timeout => 0,  # no timeouts, disable nonblocking mode on socket
3806    # Debug => debug_oneshot(),
3807    );
3808    defined($smtp_handle)
3809      or die "Can't connect to $relayhost port $relayhost_port, $!";
3810    ll(5) && do_log(5,"Remote host presents itself as: ".$smtp_handle->domain);
3811
3812    section_time($which_section);
3813    prolong_timer($which_section, $remaining_time);  # restart timer
3814    $remaining_time = undef;
3815
3816    $which_section = 'fwd-xforward';
3817    if ($msginfo->client_addr ne '' && $smtp_handle->supports('XFORWARD')) {
3818      my($cmd) = join(' ', 'XFORWARD', map
3819        { my($n,$v) = @$_;
3820          # may encode value as xtext/rfc3461 in future attributes:
3821          # char between "!" (33) and "~" (126) inclusive, except "+" and "="
3822          # $v =~ s/[^\041-\052\054-\074\076-\176]/sprintf("+%02X",ord($&))/eg;
3823          # Wietse says not to xtext-encode these four attrs, just neuter them
3824          $v =~ s/[^\041-\176]/?/g;
3825          $v =~ s/[<>()\\";@]/?/g;  # other chars that are special in headers
3826                   # postfix/smtpd/smtpd.c NEUTER_CHARACTERS (but ':' for IPv6)
3827          $v = substr($v,0,255)  if length($v) > 255;  # see XFORWARD_README
3828          $v eq '' ? () : ("$n=$v") }
3829        ( ['ADDR', $msginfo->client_addr], ['NAME',$msginfo->client_name],
3830          ['PROTO',$msginfo->client_proto],['HELO',$msginfo->client_helo] ));
3831      do_log(5, "sending $cmd");
3832      $smtp_handle->command($cmd);
3833      $smtp_handle->response()==2 or die "sending $cmd\n";
3834      section_time($which_section); prolong_timer($which_section);
3835    }
3836
3837    $which_section = 'fwd-auth';
3838    my($auth_user) = $msginfo->auth_user;
3839    my($mechanisms) = $smtp_handle->supports('AUTH');
3840    if (!c('auth_required_out')) {
3841      do_log(3,"AUTH not needed, user='$auth_user', MTA offers '$mechanisms'");
3842    } elsif ($mechanisms eq '') {
3843      do_log(3,"INFO: MTA does not offer AUTH capability, user='$auth_user'");
3844    } elsif (!defined $auth_user) {
3845      do_log(0,"INFO: AUTH needed for submission but AUTH data not available");
3846    } else {
3847      do_log(3,"INFO: authenticating $auth_user, server supports AUTH $mechanisms");
3848      my($sasl) = Authen::SASL->new(
3849        'callback' => { 'user' => $auth_user, 'authname' => $auth_user,
3850                        'pass' => $msginfo->auth_pass });
3851      $smtp_handle->auth($sasl) or die "sending AUTH, user=$auth_user\n";
3852      section_time($which_section); prolong_timer($which_section);
3853    }
3854
3855    $which_section = 'fwd-mail-from';
3856    # how to pass the $msginfo->auth_submitter ???!!!
3857    $smtp_handle->mail(qquote_rfc2821_local($msginfo->sender))
3858      or die "sending MAIL FROM\n";
3859    section_time($which_section); prolong_timer($which_section);
3860
3861    $which_section = 'fwd-rcpt-to';
3862    my($skipping_resp);
3863    for my $r (@per_recip_data) {                    # send recipient addresses
3864      if (defined $skipping_resp) {
3865        $r->recip_smtp_response($skipping_resp); $r->recip_done(2);
3866        next;
3867      }
3868      # send a RCPT TO command and get the response
3869      $smtp_handle->recipient(qquote_rfc2821_local($r->recip_final_addr));
3870      $smtp_code = $smtp_handle->code;
3871      $smtp_msg  = $smtp_handle->message;
3872      chomp($smtp_msg);
3873      my($rcpt_smtp_resp) = "$smtp_code $smtp_msg";
3874      if ($smtp_code =~ /^2/) {
3875        $any_valid_recips++;
3876      } else {  # not ok
3877        do_log(3, "response to RCPT TO: \"$rcpt_smtp_resp\"");
3878        if ($rcpt_smtp_resp =~ /^0/) {
3879          # timeout, what to do, could cause duplicates
3880          do_log(-1, "response to RCPT TO not yet available");
3881          $rcpt_smtp_resp = "450 4.4.2 ($rcpt_smtp_resp - probably timed out)";
3882        }
3883        $r->recip_remote_mta($relayhost);
3884        $r->recip_remote_mta_smtp_response($rcpt_smtp_resp);
3885        if ($rcpt_smtp_resp =~ /^ (\d{3}) \s+ ([245] \. \d{1,3} \. \d{1,3})?
3886                                \s* (.*) \z/xs)
3887        {
3888          my($resp_code, $resp_enhcode, $resp_msg) = ($1, $2, $3);
3889          if ($resp_enhcode eq '' && $resp_code =~ /^([245])/) {
3890            my($c1) = $1;
3891            $resp_enhcode = $resp_code eq '452' ?
3892                              "$c1.5.3" : "$c1.1.0";   # insert enhanced code
3893            $rcpt_smtp_resp = "$resp_code  $resp_enhcode $smtp_msg";
3894          }
3895        }
3896        if ($rcpt_smtp_resp =~ /^452/) {  # too many recipients - see rfc2821
3897          do_log(-1, sprintf('Only %d recips sent in one go: "%s"',
3898                             $any_valid_recips, $rcpt_smtp_resp));
3899          $skipping_resp = $rcpt_smtp_resp;
3900        } elsif ($rcpt_smtp_resp =~ /^4/) {
3901          $any_tempfail_recips++;
3902          $smtp_response = $rcpt_smtp_resp  if !defined($smtp_response);
3903        }
3904        $r->recip_smtp_response($rcpt_smtp_resp); $r->recip_done(2);
3905        $smtp_response = $rcpt_smtp_resp
3906          if $rcpt_smtp_resp =~ /^5/ && $smtp_response !~ /^5/; # keep first 5x
3907      }
3908    }
3909    section_time($which_section); prolong_timer($which_section);
3910    $smtp_code = $smtp_msg = undef;
3911
3912    # TODO: get the value of $dsn_per_recip_capable argument of check_mail(),
3913    # or the actual protocol from In::Connection
3914    my($dsn_per_recip_capable) = 0;
3915
3916    if (!$any_valid_recips) {
3917      do_log(-1,"mail_via_smtp: DATA skipped, no valid recips, $any_tempfail_recips");
3918    } elsif ($any_tempfail_recips && !$dsn_per_recip_capable) {
3919      # we must not proceede if mail did not came in as LMTP,
3920      # or we would generate mail duplicates on each delivery attempt
3921      do_log(-1,"mail_via_smtp: DATA skipped, tempfailed recips: $any_tempfail_recips");
3922    } else {  # send the message contents (enter DATA phase)
3923      $which_section = 'fwd-data';
3924      $smtp_handle->data or die "sending DATA command\n";
3925      $in_datasend_mode = 1;
3926
3927      my($smtp_resp) = $smtp_handle->code . " " . $smtp_handle->message;
3928      chomp($smtp_resp);
3929      do_log(5, "response to DATA: \"$smtp_resp\"");
3930
3931      my($smtp_data_fh) = Amavis::Out->new_smtp_data($smtp_handle);
3932      my($hdr_edits) = $msginfo->header_edits;
3933      $hdr_edits = Amavis::Out::EditHeader->new  if !$hdr_edits;
3934      $received_cnt = $hdr_edits->write_header($msg, $smtp_data_fh);
3935      if ($received_cnt > 100) {
3936        # loop detection required by rfc2821 6.2
3937        # Do not modify the signal text, it gets matched elsewhere!
3938        die "Too many hops: $received_cnt 'Received:' header lines\n";
3939      }
3940      if (!defined($msg)) {
3941        # empty mail body
3942      } elsif ($msg->isa('MIME::Entity')) {
3943        $msg->print_body($smtp_data_fh);
3944      } else {
3945        # Using fixed-size reads instead of line-by-line approach
3946        # makes feeding mail back to MTA (e.g. Postfix) more than
3947        # twice as fast for larger mail.
3948
3949        # to reduce the likelyhood of a qmail bare-LF bug (bare LF reported
3950        # when CR and LF are separated by a TCP packet boundary) one may use
3951        # this 'while' line, reading line by line, instead of the normal one:
3952###     while (defined($_=$msg->getline)) {
3953
3954        while ($msg->read($_, 16384) > 0) {
3955          $smtp_handle->datasend($_)
3956            or die "datasend timed out while sending body\n";
3957        }
3958
3959      }
3960      section_time($which_section); prolong_timer($which_section);
3961
3962      $which_section = 'fwd-data-end';
3963      # don't check status of dataend here, it may not yet be available
3964      $smtp_handle->dataend;
3965      $in_datasend_mode = 0; $any_valid_recips_and_data_sent = 1;
3966      section_time($which_section); prolong_timer($which_section);
3967
3968      $which_section = 'fwd-rundown-1';
3969      # figure out the final SMTP response
3970      $smtp_code = $smtp_handle->code;
3971      my(@msgs) = $smtp_handle->message;
3972      # only the 'command()' resets messages list, so now we have both:
3973      # 'End data with <CR><LF>.<CR><LF>' and 'Ok: queued as...' in @msgs
3974      # and only the last SMTP response code in $smtp_handle->code
3975      my($smtp_msg) = $msgs[$#msgs];  chomp($smtp_msg);  # take the last one
3976      $smtp_response = "$smtp_code $smtp_msg";
3977      do_log(5, "response to data end: \"$smtp_response\"");
3978      for my $r (@per_recip_data) {
3979        next  if $r->recip_done;  # skip those that failed at RCPT TO
3980        $r->recip_remote_mta($relayhost);
3981        $r->recip_remote_mta_smtp_response($smtp_response);
3982      }
3983      if ($smtp_code =~ /^[245]/) {
3984        my($smtp_status) = substr($smtp_code, 0, 1);
3985        $smtp_response = sprintf("%s %d.6.0 %s, id=%s, from MTA: %s",
3986               $smtp_code, $smtp_status, ($smtp_status == 2 ? 'Ok' : 'Failed'),
3987               am_id(), $smtp_response);
3988      }
3989    }
3990  };
3991  my($err) = $@;
3992  my($saved_section_name) = $which_section;
3993  if ($err ne '') { chomp($err); $err = ' ' if $err eq '' } # careful chomp
3994  prolong_timer($which_section, $remaining_time);           # restart the timer
3995  $which_section = 'fwd-rundown';
3996  if ($err ne '') {  # fetch info about failure
3997    do_log(3, "mail_via_smtp: session failed: $err");
3998    if (!defined($smtp_handle)) { $smtp_code = ''; $smtp_msg = '' }
3999    else {
4000      $smtp_code = $smtp_handle->code; $smtp_msg = $smtp_handle->message;
4001      chomp($smtp_msg);
4002    }
4003  }
4004  # terminate the SMTP session if still alive
4005  if (!defined $smtp_handle) {
4006    # nothing
4007  } elsif ($in_datasend_mode) {
4008    # We are aborting SMTP session.  DATA send mode must NOT be normally
4009    # terminated with a dataend (dot), otherwise recipient will receive
4010    # a chopped-off mail (and possibly be receiving it over and over again
4011    # during each MTA retry.
4012    do_log(-1, "mail_via_smtp: NOTICE: aborting SMTP session, $err");
4013    $smtp_handle->close; # abruptly terminate the SMTP session, ignoring status
4014  } else {
4015    $smtp_handle->timeout(15);  # don't wait too long for response to a QUIT
4016    $smtp_handle->quit;         # send a QUIT regardless of success so far
4017    if ($err eq '' && $smtp_handle->status != CMD_OK) {
4018      do_log(-1,"WARN: sending SMTP QUIT command failed: "
4019                . $smtp_handle->code . " " . $smtp_handle->message);
4020    }
4021  }
4022  # prepare final smtp response and log abnormal events
4023  if ($err eq '') {             # no errors
4024    if ($any_valid_recips_and_data_sent && $smtp_response !~ /^[245]/) {
4025      $smtp_response =
4026        sprintf("451 4.6.0 Bad SMTP code, id=%s, from MTA: \"%s\"",
4027                am_id(), $smtp_response);
4028    }
4029  } elsif ($err eq "timed out" || $err =~ /: Timeout\z/) {
4030    my($msg) = ($in_datasend_mode && $smtp_code =~ /^354/) ?
4031               '' : ", $smtp_code $smtp_msg";
4032    $smtp_response = sprintf("450 4.4.2 Timed out during %s%s, id=%s",
4033                             $saved_section_name, $msg, am_id());
4034  } elsif ($err =~ /^Can't connect/) {
4035    $smtp_response = sprintf("450 4.4.1 %s, id=%s", $err, am_id());
4036  } elsif ($err =~ /^Too many hops/) {
4037    $smtp_response = sprintf("550 5.4.6 Rejected: %s, id=%s", $err, am_id());
4038  } elsif ($smtp_code =~ /^5/) {  # 5xx
4039    $smtp_response = sprintf("%s 5.5.0 Rejected by MTA: %s %s, id=%s",
4040                             ($smtp_code !~ /^5\d\d\z/ ? "550" : $smtp_code),
4041                             $smtp_code, $smtp_msg, am_id());
4042  } elsif ($smtp_code =~ /^0/) {  # 000
4043    $smtp_response = sprintf("450 4.4.2 No response during %s (%s): id=%s",
4044                             $saved_section_name, $err, am_id());
4045  } else {
4046    $smtp_response = sprintf("%s 4.5.0 from MTA during %s (%s): %s %s, id=%s",
4047                             ($smtp_code !~ /^4\d\d\z/ ? "451" : $smtp_code),
4048                             $saved_section_name, $err, $smtp_code, $smtp_msg,
4049                             am_id());
4050  }
4051
4052  do_log( ($smtp_response =~ /^2/ ? 3 : -1),
4053          "mail_via_smtp: $smtp_response" )  if $smtp_response ne '';
4054  if (defined $smtp_response) {
4055    for my $r (@per_recip_data) {
4056      if (!$r->recip_done) {  # mark it as done
4057        $r->recip_smtp_response($smtp_response); $r->recip_done(2);
4058        $r->recip_mbxname($r->recip_final_addr)  if $smtp_response =~ /^2/;
4059      } elsif ($any_valid_recips_and_data_sent
4060               && $r->recip_smtp_response =~ /^452/) {
4061        # 'undo' the RCPT TO '452 Too many recipients' situation,
4062        # needs to be handled in more than one transaction
4063        $r->recip_smtp_response(undef); $r->recip_done(undef);
4064      }
4065    }
4066  }
4067  if (   $smtp_response =~ /^2/) { snmp_count('OutMsgsDelivers') }
4068  elsif ($smtp_response =~ /^4/) { snmp_count('OutAttemptFails') }
4069  elsif ($smtp_response =~ /^5/) { snmp_count('OutMsgsRejects')  }
4070  section_time($which_section);
4071  1;
4072}
4073
4074# Send mail using external mail submission program 'sendmail' (also available
4075# with Postfix and Exim) - used for forwarding original mail or sending notif.
4076# May throw exception (die) if temporary failure (4xx) or other problem
4077#
4078sub mail_via_pipe(@) {
4079  my($via, $msginfo, $initial_submission, $filter) = @_;
4080  snmp_count('OutMsgs');
4081  $via =~ /^pipe:(.*)\z/si or die "Bad fwd method syntax: $via";
4082  my($pipe_args) = $1;
4083  $pipe_args =~ s/^flags=\S*\s*//i;  # flags are currently ignored, q implied
4084  $pipe_args =~ s/^argv=//i;
4085  my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
4086                             @{$msginfo->per_recip_data};
4087  my($logmsg) = sprintf("%s via PIPE: <%s>",
4088                     ($initial_submission ? 'SEND' : 'FWD'), $msginfo->sender);
4089  if (!@per_recip_data) {
4090    do_log(5, "$logmsg, nothing to do");
4091    return 1;
4092  }
4093  do_log(1, $logmsg . " -> " .
4094            qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data));
4095  my($msg) = $msginfo->mail_text;  # a file handle or a MIME::Entity object
4096  if (defined($msg) && !$msg->isa('MIME::Entity')) {
4097    # at this point, we have no idea what the user gave us...
4098    # a globref? a FileHandle?
4099    $msg = IO::Wrap::wraphandle($msg);  # now we have an IO::Handle-like obj
4100    $msg->seek(0,0) or die "Can't rewind mail file: $!";
4101  }
4102  my(@pipe_args) = split(' ', $pipe_args);  my(@command) = shift @pipe_args;
4103  for (@pipe_args) {
4104    # The sendmail command line expects addresses quoted as per RFC 822.
4105    #   "funny user"@some.domain
4106    # For compatibility with Sendmail, the Postfix sendmail command line
4107    # also accepts address formats that are legal in RFC 822 mail headers:
4108    #   Funny Dude <"funny user"@some.domain>
4109    # Although addresses passed as args to sendmail initial submission
4110    # should not be <...> bracketed, for some reason original sendmail
4111    # issues a warning on null reverse-path, but gladly accepty <>.
4112    # As this is not strictly wrong, we comply to make it happy.
4113    if (/^\$\{sender\}\z/i) {
4114      push(@command,
4115           map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) }
4116               $msginfo->sender);
4117    } elsif (/^\$\{recipient\}\z/i) {
4118      push(@command,
4119           map { $_ eq '' ? '<>' : untaint(quote_rfc2821_local($_)) }
4120           map { $_->recip_final_addr } @per_recip_data);
4121    } else {
4122      push(@command, $_);
4123    }
4124  }
4125  do_log(5, "mail_via_pipe running command: " . join(' ', @command));
4126  local $SIG{CHLD} = 'DEFAULT';
4127  local $SIG{PIPE} = 'IGNORE';     # write to broken pipe would throw a signal
4128  my($mp,$pid) = run_command_consumer(undef,undef,@command);
4129  binmode($mp) or die "Can't set pipe to binmode: $!";  # dflt since Perl 5.8.1
4130  my($hdr_edits) = $msginfo->header_edits;
4131  $hdr_edits = Amavis::Out::EditHeader->new  if !$hdr_edits;
4132  my($received_cnt) = $hdr_edits->write_header($msg, $mp);
4133  if ($received_cnt > 100) {  # loop detection required by rfc2821 6.2
4134                              # deal with it later, for now just skip the body
4135  } elsif (!defined($msg)) {
4136    # empty mail body
4137  } elsif ($msg->isa('MIME::Entity')) {
4138    $msg->print_body($mp);
4139  } else {
4140    while ($msg->read($_, 16384) > 0)
4141      { $mp->print($_) or die "Submitting mail text failed: $!" }
4142  }
4143  my($smtp_response);
4144  if ($received_cnt > 100) { # loop detection required by rfc2821 6.2
4145    do_log(-2, "Too many hops: $received_cnt 'Received:' header lines");
4146    kill('TERM',$pid);       # kill the process running mail submission program
4147    $mp->close;              # and ignore status
4148    $smtp_response = "550 5.4.6 Rejected: " .
4149                     "Too many hops: $received_cnt 'Received:' header lines";
4150  } else {
4151    my($err); $mp->close or $err=$!; my($child_stat) = $?;
4152    my($error_str) = exit_status_str($child_stat,$err);
4153    my($status) = WEXITSTATUS($child_stat);
4154    # sendmail program (Postfix variant) can return the following exit codes:
4155    # EX_OK(0), EX_DATAERR, EX_SOFTWARE, EX_TEMPFAIL, EX_NOUSER, EX_UNAVAILABLE
4156    if ($status == EX_OK) {
4157      $smtp_response = "250 2.6.0 Ok";  # submitted to MTA
4158      snmp_count('OutMsgsDelivers');
4159    } elsif ($status == EX_TEMPFAIL) {
4160      $smtp_response = "450 4.5.0 Temporary failure submitting message";
4161      snmp_count('OutAttemptFails');
4162    } elsif ($status == EX_NOUSER) {
4163      $smtp_response = "550 5.1.1 Recipient unknown";
4164      snmp_count('OutMsgsRejects');
4165    } elsif ($status == EX_UNAVAILABLE) {
4166      $smtp_response = "550 5.5.0 Mail submission service unavailable";
4167      snmp_count('OutMsgsRejects');
4168    } else {
4169      $smtp_response = "451 4.5.0 Failed to submit a message: $error_str";
4170      snmp_count('OutAttemptFails');
4171    }
4172  }
4173  $smtp_response .= ", id=" . am_id();
4174  for my $r (@per_recip_data) {
4175    next  if $r->recip_done;
4176    $r->recip_smtp_response($smtp_response); $r->recip_done(2);
4177    $r->recip_mbxname($r->recip_final_addr)  if $smtp_response =~ /^2/;
4178  }
4179  section_time('fwd-pipe');
4180  1;
4181}
4182
4183sub mail_via_bsmtp(@) {
4184  my($via, $msginfo, $initial_submission, $filter) = @_;
4185  snmp_count('OutMsgs'); local($1);
4186  $via =~ /^bsmtp:(.*)\z/si or die "Bad fwd method: $via";
4187  my($bsmtp_file_final) = $1; my($mbxname);
4188  $bsmtp_file_final =~ s{%(.)}
4189    {  $1 eq 'b' ? $msginfo->body_digest
4190     : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time))
4191     : $1 eq 'n' ? am_id()
4192     : $1 eq '%' ? '%' : '%'.$1 }egs;
4193  # prepend directory if not specified
4194  $bsmtp_file_final = $QUARANTINEDIR."/".$bsmtp_file_final
4195    if $QUARANTINEDIR ne '' && $bsmtp_file_final !~ m{^/};
4196  my($bsmtp_file_tmp) = $bsmtp_file_final . ".tmp";
4197  my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
4198                             @{$msginfo->per_recip_data};
4199  my($logmsg) = sprintf("%s via BSMTP: %s",
4200                        ($initial_submission ? 'SEND' : 'FWD'),
4201                        qquote_rfc2821_local($msginfo->sender));
4202  if (!@per_recip_data) { do_log(5, "$logmsg, nothing to do"); return 1 }
4203  do_log(1, $logmsg . " -> " .
4204            qquote_rfc2821_local(map {$_->recip_final_addr} @per_recip_data) .
4205            ", file " . $bsmtp_file_final);
4206  my($msg) = $msginfo->mail_text;  # a scalar reference, or a file handle
4207  if (defined($msg) && !$msg->isa('MIME::Entity')) {
4208    # at this point, we have no idea what the user gave us...
4209    # a globref? a FileHandle?
4210    $msg = IO::Wrap::wraphandle($msg);  # now we have an IO::Handle-like obj
4211    $msg->seek(0,0) or die "Can't rewind mail file: $!";
4212  }
4213  my($mp) = IO::File->new;
4214  eval {
4215    $mp->open($bsmtp_file_tmp,'>',0640)
4216      or die "Can't create BSMTP file $bsmtp_file_tmp: $!";
4217    binmode($mp, ":bytes") or die "Can't set :bytes, $!"  if $unicode_aware;
4218    $mp->print("EHLO ", c('localhost_name'), $eol)
4219      or die "print failed (EHLO): $!";
4220    $mp->printf("MAIL FROM:%s BODY=8BITMIME%s",  # avoid conversion to 7bit
4221                qquote_rfc2821_local($msginfo->sender), $eol)
4222      or die "print failed (MAIL FROM): $!";
4223    for my $r (@per_recip_data) {
4224      $mp->print("RCPT TO:", qquote_rfc2821_local($r->recip_final_addr), $eol)
4225        or die "print failed (RCPT TO): $!";
4226    }
4227    $mp->print("DATA", $eol) or die "print failed (DATA): $!";
4228    my($hdr_edits) = $msginfo->header_edits;
4229    $hdr_edits = Amavis::Out::EditHeader->new  if !$hdr_edits;
4230    my($received_cnt) = $hdr_edits->write_header($msg,$mp);
4231    if ($received_cnt > 100) {  # loop detection required by rfc2821 6.2
4232      die "Too many hops: $received_cnt 'Received:' header lines";
4233    } elsif (!defined($msg))            {  # empty mail body
4234    } elsif ($msg->isa('MIME::Entity')) {
4235      $msg->print_body($mp);
4236    } else {
4237      while (<$msg>) {
4238        $mp->print(/^\./ ? (".",$_) : $_)  or die "print failed-data: $!";
4239      }
4240    }
4241    $mp->print(".", $eol) or die "print failed (final dot): $!";
4242  # $mp->print("QUIT",$eol) or die "print failed (QUIT): $!";
4243    $mp->close or die "Can't close BSMTP file $bsmtp_file_tmp: $!";
4244    $mp = undef;
4245    rename($bsmtp_file_tmp, $bsmtp_file_final)
4246      or die "Can't rename BSMTP file to $bsmtp_file_final: $!";
4247    $mbxname = $bsmtp_file_final;
4248  };
4249  my($err) = $@; my($smtp_response);
4250  if ($err eq '') {
4251    $smtp_response = "250 2.6.0 Ok, queued as BSMTP $bsmtp_file_final";
4252    snmp_count('OutMsgsDelivers');
4253  } else {
4254    chomp($err);
4255    unlink($bsmtp_file_tmp)
4256      or do_log(-2,"Can't delete half-finished BSMTP file $bsmtp_file_tmp: $!");
4257    $mp->close  if defined $mp;  # ignore status
4258    if ($err =~ /too many hops/i) {
4259      $smtp_response = "550 5.4.6 Rejected: $err";
4260      snmp_count('OutMsgsRejects');
4261    } else {
4262      $smtp_response = "451 4.5.0 Writing $bsmtp_file_tmp failed: $err";
4263      snmp_count('OutAttemptFails');
4264    }
4265  }
4266  $smtp_response .= ", id=" . am_id();
4267  for my $r (@per_recip_data) {
4268    next  if $r->recip_done;
4269    $r->recip_smtp_response($smtp_response); $r->recip_done(2);
4270    $r->recip_mbxname($mbxname)  if $mbxname ne '' && $smtp_response =~ /^2/;
4271  }
4272  section_time('fwd-bsmtp');
4273  1;
4274}
4275
42761;
4277
4278#
4279package Amavis::UnmangleSender;
4280use strict;
4281use re 'taint';
4282
4283BEGIN {
4284  use Exporter ();
4285  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
4286  $VERSION = '2.034';
4287  @ISA = qw(Exporter);
4288  %EXPORT_TAGS = ();
4289  @EXPORT = ();
4290  @EXPORT_OK = qw(&best_try_originator_ip &best_try_originator
4291                  &first_received_from);
4292}
4293use subs @EXPORT_OK;
4294
4295BEGIN {
4296  import Amavis::Conf qw(:platform @viruses_that_fake_sender_maps);
4297  import Amavis::Util qw(ll do_log);
4298  import Amavis::rfc2821_2822_Tools qw(
4299                   split_address parse_received fish_out_ip_from_received);
4300  import Amavis::Lookup qw(lookup lookup_ip_acl);
4301}
4302use Mail::Address;
4303
4304# Returns the envelope sender address, or reconstructs it if there is
4305# a good reason to believe the envelope address has been changed or forged,
4306# as is common for some varieties of viruses. Returns best guess of the
4307# sender address, or undef if it can not be determined.
4308#
4309sub unmangle_sender($$$) {
4310  my $sender         = shift;  # rfc2821 envelope sender address
4311  my $from           = shift;  # rfc2822 'From:' header, may include comment
4312  my $virusname_list = shift;  # list ref containing names of detected viruses
4313  # based on ideas from Furio Ercolessi, Mike Atkinson, Mark Martinec
4314
4315  my($best_try_originator) = $sender;
4316  my($localpart,$domain) = split_address($sender);
4317  # extract the RFC2822 'from' address, ignoring phrase and comment
4318  chomp($from);
4319  {
4320    local($1,$2,$3,$4);  # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted !
4321    $from = (Mail::Address->parse($from))[0];
4322  }
4323  $from = $from->address  if $from ne '';
4324  # NOTE: rfc2822 allows multiple addresses in the From field!
4325
4326  if (grep { /magistr/i } @$virusname_list) {
4327    for my $j (0..2) {     #  assemble possible `shifted' candidates
4328      next  if $j >= length($localpart);
4329      my($try) = $sender;
4330      substr($try, $j, 1) = chr(ord(substr($try, $j, 1)) - 1);
4331      if (lc($from) eq lc($try)) { $best_try_originator = $try; last }
4332    }
4333  }
4334  #
4335  #   Virus names are AV-checker vendor specific, but many use same
4336  #   or similar virus names. This requires attention and adjustments
4337  #   from Amavis administrators.
4338  #
4339  if (grep { /badtrans/i } @$virusname_list) {
4340    if ($from =~ /^     # these are fake built-in addresses
4341                   (joanna\@mail\.utexas\.edu | powerpuff\@videotron\.ca |
4342                     (mary\@c-com | support\@cyberramp | admin\@gte |
4343                      administrator\@border) \.net |
4344                     (monika\@telia | jessica\@aol | spiderroll\@hotmail |
4345                      lgonzal\@hotmail | andy\@hweb-media | Gravity49\@aol |
4346                      tina0828\@yahoo | JUJUB271\@AOL | aizzo\@home) \.com
4347                   ) \z/xi )
4348    { # discard recipient's address used as a fake 'MAIL FROM:'
4349      $best_try_originator = undef;
4350    } else {
4351      $best_try_originator = $1 if $from=~/^_(.+)\z/s && lc($sender) ne lc($1);
4352    }
4353  }
4354  for my $vn (@$virusname_list) {
4355    my($result,$matching_key) = lookup(0,$vn,@viruses_that_fake_sender_maps);
4356    if ($result) {
4357      do_log(2, "Virus $vn matches $matching_key, sender addr ignored");
4358      $best_try_originator = undef;
4359      last;
4360    }
4361  }
4362  $best_try_originator;
4363}
4364
4365# Given a dotted-quad IPv4 address try reverse DNS resolve, and then
4366# forward DNS resolve. If they match, return domain name,
4367# otherwise return the IP address in brackets. (resolves IPv4 only)
4368#
4369sub ip_addr_to_name($) {
4370  my($addr) = @_;     # dotted-quad address string
4371  local($1,$2,$3,$4); my($result);
4372  if ($addr !~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
4373    $result = $addr;  # not an IPv4 address
4374  } else {
4375    my($binaddr) = pack('C4', $1,$2,$3,$4);   # to binary string
4376    do_log(5, "ip_addr_to_name: DNS reverse-resolving: $addr");
4377    my(@addr) = gethostbyaddr($binaddr,2);           # IP -> name
4378    $result = '['.$addr.']';  # IP address in brackets if nothing matches
4379    if (@addr) {
4380      my($name,$aliases,$addrtype,$length,@addrs) = @addr;
4381      if ($name =~ /[^.]\.[a-zA-Z]+\z/s) {
4382        do_log(5, "ip_addr_to_name: DNS forward-resolving: $name");
4383        my(@raddr) = gethostbyname($name);           # name -> IP
4384        my($rname,$raliases,$raddrtype,$rlength,@raddrs) = @raddr;
4385        for my $ra (@raddrs) {
4386          if (lc($ra) eq lc($binaddr)) { $result = $name; last }
4387        }
4388      }
4389    }
4390  }
4391  do_log(3, "ip_addr_to_name: returning: $result");
4392  $result;
4393}
4394
4395# Obtain and parse the first entry (chronologically) in the 'Received:' header
4396# path trace - to be used as the value of the macro %t in customized messages
4397#
4398sub first_received_from($) {
4399  my($entity) = shift;
4400  my($first_received);
4401  if (defined($entity)) {
4402    my($fields) = parse_received($entity->head->get('received', -1));
4403    if (exists $fields->{'from'}) {
4404      my($item, $v1, $v2, $v3, $comment) = @{$fields->{'from'}};
4405      $first_received = join(' ', $item, $comment);
4406      $first_received =~ s/^[ \t\n\r]+//s;   # discard leading whitespace
4407      $first_received =~ s/[ \t\n\r]+\z//s;  # discard trailing whitespace
4408    }
4409    do_log(5, "first_received_from: $first_received");
4410  }
4411  $first_received;
4412}
4413
4414# Try to extract sender's public IP address from the Received trace
4415#
4416sub best_try_originator_ip($) {
4417  my($entity) = @_;
4418  my($first_received_from_ip);
4419  if (defined($entity)) {
4420    my(@publicnetworks) = qw(
4421      !0.0.0.0/8 !127.0.0.0/8 !172.16.0.0/12 !192.168.0.0/16 !10.0.0.0/8
4422      !169.254.0.0/16 !192.0.2.0/24 !192.88.99.0/24 !224.0.0.0/4
4423      ::FFFF:0:0/96
4424      !:: !::1 !FF00::/8 !FE80::/10 !FEC0::/10
4425      ::/0 );  # rfc3330, rfc3513
4426    my(@received) = reverse $entity->head->get('received');
4427    $#received = 5  if $#received > 5;  # first six, chronologically
4428    for my $r (@received) {
4429      $first_received_from_ip = fish_out_ip_from_received($r);
4430      last if $first_received_from_ip ne '' &&
4431              eval { lookup_ip_acl($first_received_from_ip,\@publicnetworks) };
4432    }
4433    do_log(5, "best_try_originator_ip: $first_received_from_ip");
4434  }
4435  $first_received_from_ip;
4436}
4437
4438# For the purpose of informing administrators try to obtain true sender
4439# address or at least its site, as most viruses and spam have a nasty habit
4440# of faking envelope sender address. Return a pair of addresses:
4441# - the first (if defined) appears valid and may be used for sender
4442#   notifications;
4443# - the second should only be used in generating customizable notification
4444#   messages (macro %o), NOT to be used as address for sending notifications,
4445#   as it can contain invalid address (but can be more informative).
4446#
4447sub best_try_originator($$$) {
4448  my($sender, $entity, $virusname_list) = @_;
4449  return ($sender,$sender)  if !defined($entity);  # don't bother if no header
4450  my($originator) =
4451    unmangle_sender($sender, $entity->head->get('from',0), $virusname_list);
4452  return ($originator, $originator)  if defined $originator;
4453  my($first_received_from_ip) = best_try_originator_ip($entity);
4454  $originator = '?@' . ip_addr_to_name($first_received_from_ip)
4455    if $first_received_from_ip ne '';
4456  (undef, $originator);
4457}
4458
44591;
4460
4461#
4462package Amavis::Unpackers::NewFilename;
4463use strict;
4464use re 'taint';
4465
4466BEGIN {
4467  use Exporter ();
4468  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
4469  $VERSION = '2.034';
4470  @ISA = qw(Exporter);
4471  @EXPORT_OK = qw(&consumed_bytes);
4472}
4473
4474BEGIN {
4475  import Amavis::Conf qw(c cr ca
4476                         $MIN_EXPANSION_QUOTA $MIN_EXPANSION_FACTOR
4477                         $MAX_EXPANSION_QUOTA $MAX_EXPANSION_FACTOR);
4478  import Amavis::Util qw(ll do_log min max);
4479}
4480
4481use vars qw($avail_quota);  # available bytes quota for unpacked mail
4482use vars qw($rem_quota);    # remaining bytes quota for unpacked mail
4483
4484sub new($;$$) {  # create a file name generator object
4485  my($class, $maxfiles,$mail_size) = @_;
4486  # calculate and initialize quota
4487  $avail_quota = $rem_quota =  # quota in bytes
4488    max($MIN_EXPANSION_QUOTA, $mail_size * $MIN_EXPANSION_FACTOR,
4489        min($MAX_EXPANSION_QUOTA, $mail_size * $MAX_EXPANSION_FACTOR));
4490  do_log(4,"Original mail size: $mail_size; quota set to: $avail_quota bytes");
4491  # create object
4492  bless {
4493    num_of_issued_names => 0,  first_issued_ind => 1,  last_issued_ind => 0,
4494    maxfiles => $maxfiles,  # undef disables limit
4495    objlist => [],
4496  }, $class;
4497}
4498
4499sub parts_list_reset($) {              # clear a list of recently issued names
4500  my($self) = shift;
4501  $self->{num_of_issued_names} = 0;
4502  $self->{first_issued_ind} = $self->{last_issued_ind} + 1;
4503  $self->{objlist} = [];
4504}
4505
4506sub parts_list($) {  # returns a ref to a list of recently issued names
4507  my($self) = shift;
4508  $self->{objlist};
4509}
4510
4511sub parts_list_add($$) {  # add a parts object to the list of parts
4512  my($self, $part) = @_;
4513  push(@{$self->{objlist}}, $part);
4514}
4515
4516sub generate_new_num($) {  # make-up a new number for a file and return it
4517  my($self) = @_;
4518  if (defined($self->{maxfiles}) &&
4519      $self->{num_of_issued_names} >= $self->{maxfiles}) {
4520    # do not change the text in die without adjusting decompose_part()
4521    die "Maximum number of files ($self->{maxfiles}) exceeded";
4522  }
4523  $self->{num_of_issued_names}++; $self->{last_issued_ind}++;
4524  $self->{last_issued_ind};
4525}
4526
4527sub consumed_bytes($$;$$) {
4528  my($bytes, $bywhom, $tentatively, $exquota) = @_;
4529  my($perc) = !$avail_quota ? '' : sprintf(", (%.0f%%)",
4530                  100 * ($avail_quota - ($rem_quota - $bytes)) / $avail_quota);
4531  do_log(4,"Charging $bytes bytes to remaining quota $rem_quota"
4532           . " (out of $avail_quota$perc) - by $bywhom");
4533  if ($bytes > $rem_quota && $rem_quota >= 0) {
4534    # Do not modify the following signal text, it gets matched elsewhere!
4535    my($msg) = "Exceeded storage quota $avail_quota bytes by $bywhom; ".
4536               "last chunk $bytes bytes";
4537    do_log(-1, $msg);
4538    die "$msg\n"  if !$exquota;
4539  }
4540  $rem_quota -= $bytes  unless $tentatively;
4541  $rem_quota;  # return remaining quota
4542}
4543
45441;
4545
4546#
4547package Amavis::Unpackers::Part;
4548use strict;
4549use re 'taint';
4550
4551BEGIN {
4552  use Exporter ();
4553  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
4554  $VERSION = '2.034';
4555  @ISA = qw(Exporter);
4556}
4557
4558BEGIN {
4559  import Amavis::Util qw(ll do_log);
4560}
4561
4562use vars qw($file_generator_object);
4563sub init($) { $file_generator_object = shift }
4564
4565sub new($;$$) {  # create a part descriptor object
4566  my($class, $dir_name,$parent) = @_;
4567  my($self) = bless {}, $class;
4568  if (!defined($dir_name) && !defined($parent)) {
4569    # just make an empty object, presumably used as a new root
4570  } else {
4571    $self->number($file_generator_object->generate_new_num);
4572    $self->dir_name($dir_name)  if defined $dir_name;
4573    if (defined $parent) {
4574      $self->parent($parent);
4575      my($ch_ref) = $parent->children;
4576      push(@$ch_ref,$self); $parent->children($ch_ref);
4577    }
4578    $file_generator_object->parts_list_add($self);  # save it
4579    ll(4) && do_log(4, "Issued a new " .
4580               (defined $dir_name ? "file name" : "pseudo part") . ": " .
4581               $self->base_name);
4582  }
4583  $self;
4584}
4585
4586sub number
4587  { my($self)=shift; !@_ ? $self->{number}   : ($self->{number}=shift) };
4588sub dir_name
4589  { my($self)=shift; !@_ ? $self->{dir_name} : ($self->{dir_name}=shift) };
4590sub parent
4591  { my($self)=shift; !@_ ? $self->{parent}   : ($self->{parent}=shift) };
4592sub children
4593  { my($self)=shift; !@_ ? $self->{children}||[] : ($self->{children}=shift) };
4594sub mime_placement    # part location within a MIME tree, e.g. "1/1/3"
4595  { my($self)=shift; !@_ ? $self->{place}    : ($self->{place}=shift) };
4596sub type_short     # string or a ref to a list of strings
4597  { my($self)=shift; !@_ ? $self->{ty_short} : ($self->{ty_short}=shift) };
4598sub type_long
4599  { my($self)=shift; !@_ ? $self->{ty_long}  : ($self->{ty_long}=shift) };
4600sub type_declared
4601  { my($self)=shift; !@_ ? $self->{ty_decl}  : ($self->{ty_decl}=shift) };
4602sub name_declared  # string or a ref to a list of strings
4603  { my($self)=shift; !@_ ? $self->{nm_decl}  : ($self->{nm_decl}=shift) };
4604sub size
4605  { my($self)=shift; !@_ ? $self->{size}     : ($self->{size}=shift) };
4606sub exists
4607  { my($self)=shift; !@_ ? $self->{exists}   : ($self->{exists}=shift) };
4608sub attributes        # listref of characters representing attributes
4609  { my($self)=shift; !@_ ? $self->{attr}     : ($self->{attr}=shift) };
4610sub attributes_add {  # U=undecodable, C=crypted, D=directory,S=special,L=link
4611  my($self)=shift; my($a) = $self->{attr} || [];
4612  for my $arg (@_) { push(@$a,$arg)  if $arg ne '' && !grep {$_ eq $arg} @$a }
4613  $self->{attr} = $a;
4614};
4615
4616sub base_name { my($self)=shift; sprintf("p%03d",$self->number) }
4617
4618sub full_name {
4619  my($self)=shift; my($d) = $self->dir_name;
4620  !defined($d) ? undef : $d.'/'.$self->base_name;
4621}
4622
4623# returns a ref to a list of part ancestors, starting with the root object,
4624# and including the part object itself
4625sub path {
4626  my($self)=shift;
4627  my(@path);
4628  for (my($p)=$self; defined($p); $p=$p->parent) { unshift(@path,$p) }
4629  \@path;
4630};
4631
46321;
4633
4634#
4635package Amavis::Unpackers::OurFiler;
4636use strict;
4637use re 'taint';
4638
4639BEGIN {
4640  use Exporter ();
4641  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
4642  $VERSION = '2.034';
4643  @ISA = qw(Exporter MIME::Parser::Filer);  # subclass of MIME::Parser::Filer
4644  %EXPORT_TAGS = ();
4645  @EXPORT = ();
4646  @EXPORT_OK = ();
4647}
4648# This package will be used by mime_decode().
4649#
4650# We don't want no heavy MIME::Parser machinery for file name extension
4651# guessing, decoding charsets in filenames (and listening to complaints
4652# about it), checking for evil filenames, checking for filename contention, ...
4653# (which can not be turned off completely by ignore_filename(1) !!!)
4654# Just enforce our file name! And while at it, collect generated filenames.
4655#
4656sub new($$$) {
4657  my($class, $dir, $parent_obj) = @_;
4658  $dir =~ s{/+\z}{};  # chop off trailing slashes from directory name
4659  bless {parent => $parent_obj, directory => $dir}, $class;
4660}
4661
4662# provide a generated file name
4663sub output_path($@) {
4664  my($self, $head) = @_;
4665  my($newpart_obj) =
4666    Amavis::Unpackers::Part->new($self->{directory}, $self->{parent});
4667  get_amavisd_part($head, $newpart_obj);  # store object into head
4668  $newpart_obj->full_name;
4669}
4670
4671sub get_amavisd_part($;$) {
4672  my($head) = shift;
4673  !@_ ? $head->{amavisd_parts_obj} : ($head->{amavisd_parts_obj} = shift);
4674}
4675
46761;
4677
4678#
4679package Amavis::Unpackers::Validity;
4680use strict;
4681use re 'taint';
4682
4683BEGIN {
4684  use Exporter ();
4685  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
4686  $VERSION = '2.034';
4687  @ISA = qw(Exporter);
4688  %EXPORT_TAGS = ();
4689  @EXPORT = ();
4690  @EXPORT_OK = qw(&check_header_validity &check_for_banned_names);
4691}
4692
4693BEGIN {
4694  import Amavis::Util qw(ll do_log sanitize_str);
4695  import Amavis::Conf qw(:platform c cr ca);
4696  import Amavis::Lookup qw(lookup);
4697}
4698
4699use subs @EXPORT_OK;
4700
4701sub check_header_validity($$) {
4702  my($conn, $msginfo) = @_;
4703  my(@bad);
4704  my($curr_head);
4705  for my $next_head (@{$msginfo->orig_header}, "\n") {
4706    if ($next_head =~ /^[ \t]/) { $curr_head .= $next_head }  # folded
4707    else {                                                    # new header
4708      if (!defined($curr_head)) {  # no previous complete header
4709      } else {
4710        # obsolete rfc822 syntax allowed whitespace before colon
4711        my($field_name, $field_body) =
4712          $curr_head =~ /^([!-9;-\176]+)[ \t]*:(.*)\z/s
4713            ? ($1, $2) : (undef, $curr_head);
4714        my($msg1,$msg2);
4715        if (!defined($field_name) && $curr_head=~/^()()(.*)\z/s) {
4716          $msg1 = "Invalid header field head";
4717        } elsif ($curr_head =~ /^(.*?)([\000\015])(.*)\z/s) {
4718          $msg1 = "Improper use of control character";
4719        } elsif ($curr_head =~ /^(.*?)([\200-\377])(.*)\z/s) {
4720          $msg1 = "Non-encoded 8-bit data";
4721        } elsif ($curr_head =~ /^(.*?)([^\000-\377])(.*)\z/s) {
4722          $msg1 = "Non-encoded Unicode character";
4723        } elsif ($curr_head =~ /^()()([ \t]+)$/m) {
4724          $msg1 ="Improper folded header field made up entirely of whitespace";
4725        }
4726        if (defined $msg1) {
4727          my($pre, $ch, $post) = ($1, $2, $3);
4728          if (length($post) > 20) { $post = substr($post,0,15) . "..." }
4729          if (length($pre)-length($field_name)-2 > 50-length($post)) {
4730            $pre = "$field_name: ..."
4731                   . substr($pre, length($pre) - (45-length($post)));
4732          }
4733          $msg1 .= sprintf(" (char %02X hex)", ord($ch))  if length($ch)==1;
4734          $msg1 .= " in message header '$field_name'"     if $field_name ne '';
4735          $msg2 = sanitize_str($pre); my($msg2_pre_l) = length($msg2);
4736          $msg2 .= sanitize_str($ch . $post);
4737        # push(@bad, "$msg1\n  $msg2\n  " . (' ' x $msg2_pre_l) . '^');
4738          push(@bad, "$msg1: $msg2");
4739        }
4740      }
4741      last  if $next_head eq $eol;  # end-of-header reached
4742      $curr_head = $next_head;
4743    }
4744  }
4745  @bad;
4746}
4747
4748sub check_for_banned_names($) {
4749  my($parts_root) = @_;
4750  do_log(3, "Checking for banned types and filenames");
4751  my(@banned_part_descr,@banned_matching_keys,@banned_rhs); my($part);
4752  my($bfnmr) = ca('banned_filename_maps');  # a ref to a list
4753  for (my(@unvisited)=($parts_root);
4754       @unvisited and $part=shift(@unvisited);
4755       push(@unvisited,@{$part->children}))
4756  { # traverse decomposed parts tree breadth-first
4757    my(@path) = @{$part->path};
4758    next  if @path <= 1;
4759#   ll(5) && do_log(5, "part path: ".join(", ", map {$_->base_name} @path));
4760    shift(@path);  # ignore place-holder root node
4761    next  if @{$part->children};  # ignore non-leaf nodes
4762    my(@descr); my($found,$key_val,$key_what,$result,$matchingkey);
4763    for my $p (@path) {
4764      my(@k,$n);
4765      $n = $p->base_name;
4766      if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"P=$n") }
4767      $n = $p->mime_placement;
4768      if ($n ne '') { $n=~s/[\t\n]/ /g; push(@k,"L=$n") }
4769      $n = $p->type_declared;
4770      $n = [$n]  if !ref($n);
4771      for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"M=$m")} }
4772      $n = $p->type_short;
4773      $n = [$n]  if !ref($n);
4774      for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"T=$m")} }
4775      $n = $p->name_declared;
4776      $n = [$n]  if !ref($n);
4777      for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"N=$m")} }
4778      $n = $p->attributes;
4779      $n = [$n]  if !ref($n);
4780      for (@$n) {if ($_ ne '') {my($m)=$_; $m=~s/[\t\n]/ /g; push(@k,"A=$m")} }
4781      push(@descr, join("\t",@k));
4782      if (!$found && @$bfnmr) {  # still searching?  (old style)
4783        for my $k (@k) {
4784          $k =~ /^([a-zA-Z0-9])=(.*)\z/s;
4785          ($key_what,$key_val) = ($1,$2);
4786          next  unless $key_what =~ /^[TMNA]\z/;
4787          if ($key_what eq 'T') {
4788            $key_val = '.' . $key_val;  # prepend a dot for compatibility
4789          } elsif ($key_what eq 'A') {
4790            if ($key_val eq 'U') { $key_val = 'UNDECIPHERABLE' } else { next }
4791          }
4792          do_log(4, sprintf("check_for_banned (%s) %s=%s",
4793                            $p->base_name, $key_what, $key_val));
4794          ($result,$matchingkey) = lookup(0,$key_val,@$bfnmr);
4795          $found++  if defined $result;
4796          last  if $found;
4797        }
4798      }
4799    }
4800    my($key_val_str) = join(' | ',@descr);  $key_val_str =~ s/\t/,/g;
4801    if (!$found) {  # try new style
4802      ($result,$matchingkey) =
4803        lookup(0,join("\n",@descr),
4804               Amavis::Lookup::Label->new('banned_namepath_re'),
4805               cr('banned_namepath_re'));
4806      $found++  if defined $result;
4807    }
4808    my($ll) = $result ? 1 : 3;
4809    if (ll($ll)) {  # only bother with logging when needed
4810      my(%esc) = (r => "\r", n => "\n", f => "\f", b => "\b",
4811                  e => "\e", a => "\a", t => "\t");
4812      my($mk) = $matchingkey;  # pretty-print
4813      $mk =~ s{ \\(.) }{ exists($esc{$1}) ? $esc{$1} : '\\'.$1 }egsx;
4814      do_log($ll, sprintf('p.path%s: "%s"%s',
4815                          !$result?'':" BANNED:$result", $key_val_str,
4816                          !defined $result ? '' : ", matching_key=\"$mk\""));
4817    }
4818    if ($result) {
4819      push(@banned_part_descr, $key_val_str);
4820      push(@banned_matching_keys, $matchingkey);
4821      push(@banned_rhs, $result);
4822    }
4823  }
4824  # return listrefs of parts descriptors, matching keys and lookup results
4825  (\@banned_part_descr, \@banned_matching_keys, \@banned_rhs);
4826}
4827
48281;
4829
4830#
4831package Amavis::Unpackers::MIME;
4832use strict;
4833use re 'taint';
4834
4835BEGIN {
4836  use Exporter ();
4837  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
4838  $VERSION = '2.034';
4839  @ISA = qw(Exporter);
4840  %EXPORT_TAGS = ();
4841  @EXPORT = ();
4842  @EXPORT_OK = qw(&mime_decode);
4843}
4844use Errno qw(ENOENT EACCES);
4845use MIME::Parser;
4846use MIME::Words;
4847
4848BEGIN {
4849  import Amavis::Conf qw(:platform c cr ca);
4850  import Amavis::Timing qw(section_time);
4851  import Amavis::Util qw(snmp_count ll do_log);
4852  import Amavis::Unpackers::NewFilename qw(consumed_bytes);
4853}
4854
4855use subs @EXPORT_OK;
4856
4857# save MIME preamble and epilogue (if nontrivial) as extra (pseudo)parts
4858sub mime_decode_pre_epi($$$$$) {
4859  my($pe_name, $pe_lines, $tempdir, $parent_obj, $placement) = @_;
4860  if (defined $pe_lines && @$pe_lines) {
4861    do_log(5, "mime_decode_$pe_name: " . scalar(@$pe_lines) . " lines");
4862    if (@$pe_lines > 5 || "@$pe_lines" !~ m{^[a-zA-Z0-9/\@:;,. \t\n_-]*\z}s) {
4863      my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",
4864                                                      $parent_obj);
4865      $newpart_obj->mime_placement($placement);
4866      $newpart_obj->name_declared($pe_name);
4867      my($newpart) = $newpart_obj->full_name;
4868      my($outpart) = IO::File->new;
4869      $outpart->open($newpart,'>')
4870        or die "Can't create $pe_name file $newpart: $!";
4871      binmode($outpart, ":bytes") or die "Can't cancel :utf8 mode: $!"
4872        if $unicode_aware;
4873      my($len);
4874      for (@$pe_lines) {
4875        $outpart->print($_) or die "Can't write $pe_name to $newpart: $!";
4876        $len += length($_);
4877      }
4878      $outpart->close or die "Can't close $pe_name $newpart: $!";
4879      $newpart_obj->size($len);
4880      consumed_bytes($len, "mime_decode_$pe_name", 0, 1);
4881    }
4882  }
4883}
4884
4885# traverse MIME::Entity object depth-first,
4886# extracting preambles and epilogues as extra (pseudo)parts, and
4887# filling-in additional information into Amavis::Unpackers::Part objects
4888sub mime_traverse($$$$$);  # prototype
4889sub mime_traverse($$$$$) {
4890  my($entity, $tempdir, $parent_obj, $depth, $placement) = @_;
4891  mime_decode_pre_epi('preamble', $entity->preamble,
4892                      $tempdir, $parent_obj, $placement);
4893  my($mt, $et) = ($entity->mime_type, $entity->effective_type);
4894  my($part); my($head) = $entity->head; my($body) = $entity->bodyhandle;
4895  if (!defined($body)) {  # a MIME container only contains parts, no bodypart
4896    # create pseudo-part objects for MIME containers (e.g. multipart/* )
4897    $part = Amavis::Unpackers::Part->new(undef,$parent_obj);
4898#   $part->type_short('no-file');
4899    do_log(2, $part->base_name." $placement Content-Type: $mt");
4900  } else {  # does have a body part (i.e. not a MIME container)
4901    my($fn) = $body->path; my($size);
4902    if (!defined($fn)) { $size = length($body->as_string) }
4903    else {
4904      my($msg); my($errn) = lstat($fn) ? 0 : 0+$!;
4905      if ($errn == ENOENT) { $msg = "does not exist" }
4906      elsif ($errn) { $msg = "is inaccessible: $!" }
4907      elsif (!-r _) { $msg = "is not readable" }
4908      elsif (!-f _) { $msg = "is not a regular file" }
4909      else {
4910        $size = -s _;
4911        do_log(4,"mime_traverse: file $fn is empty")  if !$size;
4912      }
4913      do_log(-1,"WARN: mime_traverse: file $fn $msg")  if defined $msg;
4914    }
4915    consumed_bytes($size, 'mime_decode', 0, 1);
4916    # retrieve Amavis::Unpackers::Part object (if any), stashed into head obj
4917    $part = Amavis::Unpackers::OurFiler::get_amavisd_part($head);
4918    if (defined $part) {
4919      $part->size($size);
4920      if ($size==0) { $part->type_short('empty'); $part->type_long('empty') }
4921      do_log(2, $part->base_name." $placement Content-Type: $mt" .
4922                ", size: $size B, name: ".$entity->head->recommended_filename);
4923      my($old_parent_obj) = $part->parent;
4924      if ($parent_obj ne $old_parent_obj) {  # reparent if necessary
4925        ll(5) && do_log(5,sprintf("reparenting %s from %s to %s",
4926                          $part->base_name,
4927                          $old_parent_obj->base_name, $parent_obj->base_name));
4928        my($ch_ref) = $old_parent_obj->children;
4929        $old_parent_obj->children([grep {$_ ne $part} @$ch_ref]);
4930        $ch_ref = $parent_obj->children;
4931        push(@$ch_ref,$part); $parent_obj->children($ch_ref);
4932        $part->parent($parent_obj);
4933      }
4934    }
4935  }
4936  if (defined $part) {
4937    $part->mime_placement($placement);
4938    $part->type_declared($mt eq $et ? $mt : [$mt, $et]);
4939    my(@rn);  # recommended file names, both raw and RFC 2047 decoded
4940    my($val, $val_decoded);
4941    $val = $head->mime_attr('content-disposition.filename');
4942    if ($val ne '') {
4943      push(@rn, $val);
4944      $val_decoded = MIME::Words::decode_mimewords($val);
4945      push(@rn, $val_decoded)  if $val_decoded ne $val;
4946    }
4947    $val = $head->mime_attr('content-type.name');
4948    if ($val ne '') {
4949      $val_decoded = MIME::Words::decode_mimewords($val);
4950      push(@rn, $val_decoded)  if !grep { $_ eq $val_decoded } @rn;
4951      push(@rn, $val)          if !grep { $_ eq $val         } @rn;
4952    }
4953    $part->name_declared(@rn==1 ? $rn[0] : \@rn)  if @rn;
4954  }
4955  mime_decode_pre_epi('epilogue', $entity->epilogue,
4956                      $tempdir, $parent_obj, $placement);
4957  my($item_num) = 0;
4958  for my $e ($entity->parts) {  # recursive descent
4959    $item_num++;
4960    mime_traverse($e,$tempdir,$part,$depth+1,"$placement/$item_num");
4961  }
4962}
4963
4964# Break up mime parts, return MIME::Entity object
4965sub mime_decode($$$) {
4966  my($fileh, $tempdir, $parent_obj) = @_;
4967  # $fileh may be an open file handle, or a file name
4968
4969  my($parser) = MIME::Parser->new;
4970  $parser->filer(Amavis::Unpackers::OurFiler->new("$tempdir/parts",
4971                                                  $parent_obj));
4972  $parser->ignore_errors(1);  # also is the default
4973# $parser->extract_nested_messages(0);
4974  $parser->extract_nested_messages("NEST");  # parse embedded message/rfc822
4975  $parser->extract_uuencode(1);
4976  my($entity);
4977  snmp_count('OpsDecByMimeParser');
4978  if (ref($fileh)) {                         # assume open file handle
4979    do_log(4, "Extracting mime components");
4980    $fileh->seek(0,0) or die "Can't rewind mail file: $!";
4981    local($1,$2,$3,$4);       # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted !
4982    $entity = $parser->parse($fileh);
4983  } else {                    # assume $fileh is a file name
4984    do_log(4, "Extracting mime components from $fileh");
4985    local($1,$2,$3,$4);       # avoid Perl 5.8.0 & 5.8.2 bug, $1 gets tainted !
4986    $entity = $parser->parse_open("$tempdir/parts/$fileh");
4987  }
4988# my($mime_err) = $parser->last_error;  # deprecated
4989  my($mime_err) = $parser->results->errors;
4990  $mime_err=~s/\s+\z//; $mime_err=~s/[ \t\r]*\n+/; /g; $mime_err=~s/\s+/ /g;
4991  $mime_err = substr($mime_err,0,250) . '...'  if length($mime_err) > 250;
4992  do_log(1, "WARN: MIME::Parser $mime_err")  if $mime_err ne '';
4993  mime_traverse($entity, $tempdir, $parent_obj, 0, '1');
4994  section_time('mime_decode');
4995  ($entity, $mime_err);
4996}
4997
49981;
4999
5000#
5001package Amavis::Notify;
5002use strict;
5003use re 'taint';
5004
5005BEGIN {
5006  use Exporter ();
5007  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
5008  $VERSION = '2.034';
5009  @ISA = qw(Exporter);
5010  %EXPORT_TAGS = ();
5011  @EXPORT = ();
5012  @EXPORT_OK = qw(&delivery_status_notification &delivery_short_report
5013                  &string_to_mime_entity &defanged_mime_entity);
5014}
5015
5016BEGIN {
5017  import Amavis::Util qw(ll do_log am_id safe_encode q_encode);
5018  import Amavis::Timing qw(section_time);
5019  import Amavis::Conf qw(:platform $myhostname c cr ca);
5020  import Amavis::Lookup qw(lookup);
5021  import Amavis::Expand qw(expand);
5022  import Amavis::rfc2821_2822_Tools;
5023}
5024# use Encode;  # Perl 5.8  UTF-8 support
5025use MIME::Entity;
5026
5027use subs @EXPORT_OK;
5028
5029# Convert mail (that was obtained by macro-expanding notification templates)
5030# into proper MIME::Entity object. Some ad-hoc solutions are used
5031# for compatibility with previous version.
5032#
5033sub string_to_mime_entity($) {
5034  my($mail_as_string_ref) = @_;
5035  local($1,$2,$3); my($entity); my($m_hdr,$m_body);
5036  ($m_hdr, $m_body) = ($1, $3)
5037    if $$mail_as_string_ref =~ /^(.*?\r?\n)(\r?\n|\z)(.*)\z/s;
5038  $m_body = safe_encode(c('bdy_encoding'), $m_body);
5039  # make sure _our_ source line number is reported in case of failure
5040  my($nxmh) = c('notify_xmailer_header');
5041  eval {$entity = MIME::Entity->build(
5042    Type => 'text/plain', Encoding => '-SUGGEST', Charset=> c('bdy_encoding'),
5043    (defined $nxmh && $nxmh eq '' ? ()  # leave the MIME::Entity default
5044     : ('X-Mailer' => $nxmh) ),         # X-Mailer hdr or undef
5045    Data => $m_body); 1}  or do {chomp($@); die $@};
5046  my($head) = $entity->head;
5047  # insert header fields from template into MIME::Head entity
5048  $m_hdr =~ s/\r?\n([ \t])/$1/g;  # unfold template header
5049  for my $hdr_line (split(/\r?\n/, $m_hdr)) {
5050    if ($hdr_line =~ /^([^:]*):\s*(.*)\z/s) {
5051      my($fhead, $fbody) = ($1, $2);
5052      # encode according to RFC 2047 if necessary
5053      $fhead = safe_encode('ascii', $fhead);
5054      if ($fhead =~ /^(X-.*|Subject|Comments)\z/si &&
5055          $fbody =~ /[^\011\012\040-\176]/)  # nonprint. except TAB and LF?
5056      {                                      # encode according to RFC 2047
5057        # TODO: shouldn't we unfold first?!
5058        my($fbody_octets);
5059        if (!$unicode_aware) { $fbody_octets = $fbody }
5060        else {
5061          $fbody_octets = safe_encode(c('hdr_encoding'), $fbody);
5062          do_log(5, "string_to_mime_entity UTF-8 body:  $fbody");
5063          do_log(5, "string_to_mime_entity body octets: $fbody_octets");
5064        }
5065        my($qb) = c('hdr_encoding_qb');
5066        if (uc($qb) eq 'Q') {
5067          $fbody = q_encode($fbody_octets, $qb, c('hdr_encoding'));
5068        } else {
5069          $fbody = MIME::Words::encode_mimeword($fbody_octets,
5070                                           $qb, c('hdr_encoding'));
5071        }
5072      } else {  # supposed to be in plain ASCII, let's make sure it is
5073        $fbody = safe_encode('ascii', $fbody);
5074      }
5075      do_log(5, sprintf("string_to_mime_entity %s: %s", $fhead, $fbody));
5076      # make sure _our_ source line number is reported in case of failure
5077      if (!eval { $head->replace($fhead, $fbody); 1 }) {
5078        chomp($@);
5079        die sprintf("%s header field '%s: %s'",
5080                    ($@ eq '' ? "invalid" : "$@, "), $fhead, $fbody);
5081      }
5082    }
5083  }
5084  $entity;  # return the built MIME::Entity
5085}
5086
5087# Generate delivery status notification according to
5088# rfc1892 (now rfc3462) and rfc1894 (now rfc3464).
5089# Return dsn message object if dsn is needed, or undef otherwise.
5090#
5091sub delivery_status_notification($$$$$) {
5092  my($conn,$msginfo,$report_success_dsn_also,$builtins_ref,$template_ref) = @_;
5093  my($dsn_time) = time;  # time of dsn creation - now
5094  my($notification);
5095  if ($msginfo->sender eq '') {  # must not respond to null reverse path
5096    do_log(4, "Not sending DSN to empty return path");
5097  } else {
5098    my($from_mta, $client_ip) = ($conn->smtp_helo, $conn->client_ip);
5099    my($msg) = '';              # constructed dsn text according to rfc3464
5100    $msg .= "Reporting-MTA: dns; $myhostname\n";
5101    $msg .= "Received-From-MTA: smtp; $from_mta ([$client_ip])\n"
5102      if $from_mta ne '';
5103    $msg .= "Arrival-Date: " . rfc2822_timestamp($msginfo->rx_time) . "\n";
5104
5105    my($any);                   # any recipients with failed delivery?
5106    for my $r (@{$msginfo->per_recip_data}) {
5107      my($remote_mta) = $r->recip_remote_mta;
5108      my($smtp_resp)  = $r->recip_smtp_response;
5109      if (!$r->recip_done) {
5110        if ($msginfo->delivery_method eq '') {  # e.g. milter
5111          # as far as we are concerned all is ok, delivery will be performed
5112          # by a helper program or MTA
5113          $smtp_resp = "250 2.5.0 Ok, continue delivery";
5114        } else {
5115          do_log(-2,"TROUBLE: recipient not done: <"
5116                    . $r->recip_addr . "> " . $smtp_resp);
5117        }
5118      }
5119      my($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg);
5120      if ($smtp_resp =~ /^ (\d{3}) \s+ ([245] \. \d{1,3} \. \d{1,3})?
5121                           \s* (.*) \z/xs) {
5122        ($smtp_resp_code, $smtp_resp_enhcode, $smtp_resp_msg) = ($1,$2,$3);
5123      } else {
5124        $smtp_resp_msg = $smtp_resp;
5125      }
5126      my($smtp_resp_class) = $smtp_resp_code =~ /^(\d)/ ? $1 : '0';
5127      if ($smtp_resp_enhcode eq '' && $smtp_resp_class =~ /^([245])\z/) {
5128        $smtp_resp_enhcode = "$1.0.0";
5129      }
5130      # skip success notifications
5131      next unless $smtp_resp_class ne '2' || $report_success_dsn_also;
5132      $any++;
5133      $msg .= "\n";  # empty line between groups of per-recipient fields
5134      if ($remote_mta ne '' && $r->recip_final_addr ne $r->recip_addr) {
5135        $msg .= "X-NextToLast-Final-Recipient: rfc822; "
5136                . quote_rfc2821_local($r->recip_addr) . "\n";
5137        $msg .= "Final-Recipient: rfc822; "
5138                . quote_rfc2821_local($r->recip_final_addr) . "\n";
5139      } else {
5140        $msg .= "Final-Recipient: rfc822; "
5141                . quote_rfc2821_local($r->recip_addr) . "\n";
5142      }
5143      $msg .= "Action: ".($smtp_resp_class eq '2' ? 'delivered':'failed')."\n";
5144      $msg .= "Status: $smtp_resp_enhcode\n";
5145      my($rem_smtp_resp) = $r->recip_remote_mta_smtp_response;
5146      if ($remote_mta eq '' || $rem_smtp_resp eq '') {
5147        $msg .= "Diagnostic-Code: smtp; $smtp_resp\n";
5148      } else {
5149        $msg .= "Remote-MTA: dns; $remote_mta\n";
5150        $msg .= "Diagnostic-Code: smtp; $rem_smtp_resp\n";
5151      }
5152      $msg .= "Last-Attempt-Date: " . rfc2822_timestamp($dsn_time) . "\n";
5153    }
5154    return $notification  if !$any;  # don't bother, we won't be sending DSN
5155
5156    my($to_hdr) = qquote_rfc2821_local($msginfo->sender_contact);
5157
5158    # use the provided template text
5159    my(%mybuiltins) = %$builtins_ref;  # make a local copy
5160    $mybuiltins{'f'} = c('hdrfrom_notify_sender');
5161    $mybuiltins{'T'} = $to_hdr;
5162    $mybuiltins{'d'} = rfc2822_timestamp($dsn_time);
5163    my($dsn) = expand($template_ref, \%mybuiltins);
5164
5165    my($dsn_entity) = string_to_mime_entity($dsn);
5166    $dsn_entity->make_multipart;
5167    my($head) = $dsn_entity->head;
5168
5169    # rfc3464: The From field of the message header of the DSN SHOULD contain
5170    # the address of a human who is responsible for maintaining the mail system
5171    # at the Reporting MTA site (e.g. Postmaster), so that a reply to the
5172    # DSN will reach that person.
5173    eval { $head->replace('From', c('hdrfrom_notify_sender')); 1 }
5174      or do { chomp($@); die $@ };
5175    eval { $head->replace('To', $to_hdr); 1 } or do { chomp($@); die $@ };
5176    eval { $head->replace('Date', rfc2822_timestamp($dsn_time)); 1 }
5177      or do { chomp($@); die $@ };
5178
5179    my($field) = Mail::Field->new('Content_type');  # underline, not hyphen!
5180    $field->type("multipart/report; report-type=delivery-status");
5181    $field->boundary(MIME::Entity::make_boundary());
5182    $head->replace('Content-type', $field->stringify);
5183    $head = undef;
5184
5185    # make sure _our_ source line number is reported in case of failure
5186    eval {$dsn_entity->attach(
5187            Type => 'message/delivery-status', Encoding => '7bit',
5188            Description => 'Delivery error report',
5189            Data => $msg); 1} or do {chomp($@); die $@};
5190    eval {$dsn_entity->attach(
5191            Type => 'text/rfc822-headers', Encoding => '-SUGGEST',
5192            Description => 'Undelivered-message headers',
5193            Data => $msginfo->orig_header); 1} or do {chomp($@); die $@};
5194    $notification = Amavis::In::Message->new;
5195    $notification->rx_time($dsn_time);
5196    $notification->delivery_method(c('notify_method'));
5197    $notification->sender(c('mailfrom_notify_sender'));  # should be empty!
5198    $notification->auth_submitter('<>');
5199    $notification->auth_user(c('amavis_auth_user'));
5200    $notification->auth_pass(c('amavis_auth_pass'));
5201    $notification->recips([$msginfo->sender_contact]);
5202    $notification->mail_text($dsn_entity);
5203  }
5204  $notification;
5205}
5206
5207# Return a pair of arrayrefs of quoted recipient addresses (the first lists
5208# recipients with successful delivery status, the second all the rest),
5209# plus a list of short per-recipient delivery reports for failed deliveries,
5210# that can be used in the first MIME part (the free text format) of delivery
5211# status notifications.
5212#
5213sub delivery_short_report($) {
5214  my($msginfo) = @_;
5215  my(@succ_recips, @failed_recips, @failed_recips_full);
5216  for my $r (@{$msginfo->per_recip_data}) {
5217    my($remote_mta)  = $r->recip_remote_mta;
5218    my($smtp_resp)   = $r->recip_smtp_response;
5219    my($qrecip_addr) = scalar(qquote_rfc2821_local($r->recip_addr));
5220    if ($r->recip_destiny == D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)) {
5221      push(@succ_recips,   $qrecip_addr);
5222    } else {
5223      push(@failed_recips, $qrecip_addr);
5224      push(@failed_recips_full,
5225           sprintf("%s:%s\n   %s", $qrecip_addr,
5226                   ($remote_mta eq ''?'':" $remote_mta said:"), $smtp_resp));
5227    }
5228  }
5229  (\@succ_recips, \@failed_recips, \@failed_recips_full);
5230}
5231
5232# Build a new MIME::Entity object based on the original mail, but hopefully
5233# safer to mail readers: conventional mail header fields are retained,
5234# original mail becomes an attachment of type 'message/rfc822'.
5235# Text in $first_part becomes the first MIME part of type 'text/plain'.
5236#
5237sub defanged_mime_entity($$$) {
5238  my($conn,$msginfo,$first_part) = @_;
5239  my($new_entity);
5240  my($resent_time) = $msginfo->rx_time;
5241  $first_part = safe_encode(c('bdy_encoding'), $first_part);
5242  # make sure _our_ source line number is reported in case of failure
5243  my($nxmh) = c('notify_xmailer_header');
5244  eval {$new_entity = MIME::Entity->build(
5245    Type => 'multipart/mixed',
5246    (defined $nxmh && $nxmh eq '' ? ()  # leave the MIME::Entity default
5247     : ('X-Mailer' => $nxmh) ),         # X-Mailer hdr or undef
5248    ); 1}  or do {chomp($@); die $@};
5249  my($head) = $new_entity->head;
5250  my($orig_head) = $msginfo->mime_entity->head;
5251  # TODO: we should retain the ordering of Resent-* with their Received fields
5252  for my $field_head (   # copy some of the original header fields
5253      qw(Received From Sender To Cc Reply-To Date Message-ID
5254         Resent-From Resent-Sender Resent-To Resent-Cc
5255         Resent-Date Resent-Message-ID
5256         In-Reply-To References Subject
5257         Comments Keywords Organization X-Mailer) ) {
5258    for my $value ($orig_head->get_all($field_head)) {
5259      do_log(4, "copying-over the header field: $field_head");
5260      eval { $head->add($field_head, $value); 1 } or do {chomp($@); die $@};
5261    }
5262  }
5263  $head = undef;  # object not needed any longer
5264  eval {$new_entity->attach(
5265    Type => 'text/plain', Encoding => '-SUGGEST', Charset => c('bdy_encoding'),
5266    Data => $first_part); 1}  or do {chomp($@); die $@};
5267  eval {$new_entity->attach(  # rfc2046
5268    Type => 'message/rfc822; x-spam-type=original',
5269    Encoding => '8bit', Path => $msginfo->mail_text_fn,
5270    Description => 'Original message',
5271    Filename => 'message.txt', Disposition => 'attachment'); 1}
5272    or do {chomp($@); die $@};
5273  $new_entity;
5274}
5275
52761;
5277
5278#
5279package Amavis::Cache;
5280# offer an 'IPC::Cache'-compatible simple interface
5281# to a local (per-process) memory-based cache;
5282use strict;
5283use re 'taint';
5284
5285BEGIN {
5286  import Amavis::Util qw(ll do_log);
5287}
5288BEGIN {
5289  use Exporter ();
5290  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
5291  $VERSION = '2.0341';
5292  @ISA = qw(Exporter);
5293}
5294
5295# simple local memory-based cache
5296sub new {  # called by each child process
5297  my($class) = @_;
5298  do_log(5,"BerkeleyDB not available, using memory-based local cache");
5299  bless {}, $class;
5300}
5301sub get { my($self,$key) = @_; thaw($self->{$key}) }
5302sub set { my($self,$key,$obj) = @_; $self->{$key} = freeze($obj) }
5303
5304# protect % and ~, as well as NUL and \200 for good measure
5305sub encode($) {
5306  my($str) = @_; $str =~ s/[%~\000\200]/sprintf("%%%02X",ord($&))/egs; $str;
5307}
5308
5309# simple Storable::freeze lookalike
5310sub freeze($);  # prototype
5311sub freeze($) {
5312  my($obj) = @_; my($ty) = ref($obj);
5313  if (!defined($obj))     { 'U' }
5314  elsif (!$ty)            { join('~', '',  encode($obj))  }  # string
5315  elsif ($ty eq 'SCALAR') { join('~', 'S', encode(freeze($$obj))) }
5316  elsif ($ty eq 'REF')    { join('~', 'R', encode(freeze($$obj))) }
5317  elsif ($ty eq 'ARRAY')  { join('~', 'A', map {encode(freeze($_))} @$obj) }
5318  elsif ($ty eq 'HASH') {
5319    join('~','H',map {(encode($_),encode(freeze($obj->{$_})))} sort keys %$obj)
5320  } else { die "Can't freeze object type $ty" }
5321}
5322
5323# simple Storable::thaw lookalike
5324sub thaw($);  # prototype
5325sub thaw($) {
5326  my($str) = @_;
5327  return undef  if !defined $str;
5328  my($ty,@val) = split(/~/,$str,-1);
5329  for (@val) { s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg }
5330  if    ($ty eq 'U') { undef }
5331  elsif ($ty eq '')  { $val[0] }
5332  elsif ($ty eq 'S') { my($obj)=thaw($val[0]); \$obj }
5333  elsif ($ty eq 'R') { my($obj)=thaw($val[0]); \$obj }
5334  elsif ($ty eq 'A') { [map {thaw($_)} @val] }
5335  elsif ($ty eq 'H') {
5336    my($hr) = {};
5337    while (@val) { my($k) = shift @val; $hr->{$k} = thaw(shift @val) }
5338    $hr;
5339  } else { die "Can't thaw object type $ty" }
5340}
5341
53421;
5343
5344#
5345package Amavis;
5346require 5.005;  # need qr operator and \z in regexps
5347use strict;
5348use re 'taint';
5349
5350use POSIX qw(strftime);
5351use Errno qw(ENOENT EACCES);
5352use IO::File ();
5353# body digest for caching, either SHA1 or MD5
5354#use Digest::SHA1;
5355use Digest::MD5;
5356use Net::Server 0.83;
5357use Net::Server::PreForkSimple;
5358
5359BEGIN {
5360  import Amavis::Conf qw(:platform :sa :confvars c cr ca);
5361  import Amavis::Util qw(untaint min max ll do_log sanitize_str debug_oneshot
5362                         am_id snmp_counters_init snmp_count prolong_timer);
5363  import Amavis::Log;
5364  import Amavis::Timing qw(section_time get_time_so_far);
5365  import Amavis::rfc2821_2822_Tools;
5366  import Amavis::Lookup qw(lookup lookup_ip_acl);
5367  import Amavis::Out;
5368  import Amavis::Out::EditHeader;
5369  import Amavis::UnmangleSender qw(best_try_originator_ip best_try_originator
5370                                   first_received_from);
5371  import Amavis::Unpackers::Validity qw(
5372                           check_header_validity check_for_banned_names);
5373  import Amavis::Unpackers::MIME qw(mime_decode);
5374  import Amavis::Expand qw(expand);
5375  import Amavis::Notify qw(delivery_status_notification delivery_short_report
5376                           string_to_mime_entity defanged_mime_entity);
5377  import Amavis::In::Connection;
5378  import Amavis::In::Message;
5379}
5380
5381# Make it a subclass of Net::Server::PreForkSimple
5382# to override method &process_request (and others if desired)
5383use vars qw(@ISA);
5384# @ISA = qw(Net::Server);
5385@ISA = qw(Net::Server::PreForkSimple);
5386
5387delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
5388
5389use vars qw(
5390  $extra_code_db $extra_code_cache
5391  $extra_code_sql $extra_code_ldap
5392  $extra_code_in_amcl $extra_code_in_smtp $extra_code_in_qmqpqq
5393  $extra_code_antivirus $extra_code_antispam $extra_code_unpackers);
5394
5395use vars qw(%modules_basic);
5396use vars qw($spam_level $spam_status $spam_report);
5397use vars qw($user_id_sql $wb_listed_sql $implicit_maps_inserted);
5398use vars qw($db_env $snmp_db);
5399use vars qw($body_digest $body_digest_cache);
5400use vars qw(%builtins);    # customizable notification messages
5401use vars qw($child_invocation_count $child_task_count);
5402# $child_invocation_count  # counts child re-use from 1 to max_requests
5403# $child_task_count        # counts check_mail() calls - this normally runs
5404                           # in sync with $child_invocation_count, but with
5405                           # SMTP or LMTP input there may be more than one
5406                           # message passed during a single SMTP session
5407use vars qw(@config_files);
5408use vars qw($VIRUSFILE $CONN $MSGINFO);
5409use vars qw($av_output @virusname @detecting_scanners
5410            @banned_filename @bad_headers);
5411
5412use vars qw($amcl_in_obj $smtp_in_obj); # Amavis::In::AMCL and In::SMTP objects
5413use vars qw($qmqpqq_in_obj);            # Amavis::In::QMQPqq object
5414use vars qw($sql_policy $sql_wblist);   # Amavis::Lookup::SQL objects
5415use vars qw($ldap_policy);              # Amavis::Lookup::LDAP objects
5416
5417# initialize some remaining global variables;
5418# invoked after chroot and after privileges have been dropped
5419sub after_chroot_init() {
5420  $child_invocation_count = $child_task_count = 0;
5421  %modules_basic = %INC;  # helps to track missing modules in chroot
5422  my(@msg);
5423  my($euid) = $>;   # effective UID
5424  $> = 0;           # try to become root
5425  POSIX::setuid(0)  if $> != 0;  # and try some more
5426  if ($> == 0) {    # succeded? panic!
5427    @msg = ("It is possible to change EUID from $euid to root, ABORTING!",
5428            "Perhaps you forgot to patch the Net::Server - see:",
5429            "  http://www.ijs.si/software/amavisd/#net-server-sec",
5430            "or start as non-root, e.g. by su(1) or using option -u user");
5431  } elsif ($daemon_chroot_dir eq '') {
5432    # A quick check on vulnerability/protection of a config file
5433    # (non-exhaustive: doesn't test for symlink tricks and higher directories).
5434    # The config file has already been executed by now, so it may be
5435    # too late to feel sorry now, but better late then never.
5436    for my $config_file (@config_files) {
5437      my($fh) = IO::File->new;
5438      my($errn) = lstat($config_file) ? 0 : 0+$!;
5439      if ($errn) {  # not accessible, don't bother to test further
5440      } elsif ($fh->open($config_file,'+<')) {
5441        push(@msg, "Config file \"$config_file\" is writable, ".
5442                   "UID $<, EUID $>, EGID $)" );
5443        $fh->close;  # close, ignoring status
5444      } elsif (rename($config_file, $config_file.'.moved')) {
5445        my($m) = 'appears writable (unconfirmed)';
5446        if (!-e $config_file && -e $config_file.'.moved') {
5447          rename($config_file.'.moved', $config_file);  # try to rename back
5448          $m = 'is writable (confirmed)';
5449        }
5450        push(@msg, "Directory of a config file \"$config_file\" $m, ".
5451                   "UID $<, EUID $>, EGID $)" );
5452      }
5453      last  if @msg;
5454    }
5455  }
5456  if (@msg) {
5457    do_log(-3,"FATAL: $_")  for @msg;
5458    print STDERR (map {"$_\n"} @msg);
5459    die "SECURITY PROBLEM, ABORTING";
5460    exit 1;  # just in case
5461  }
5462  # report versions of some modules
5463  for my $m ('Amavis::Conf',
5464             sort map { s/\.pm\z//; s[/][::]g; $_ } grep { /\.pm\z/ } keys %INC){
5465    next  if !grep { $_ eq $m } qw(Amavis::Conf
5466      Archive::Tar Archive::Zip Compress::Zlib Convert::TNEF Convert::UUlib
5467      MIME::Entity MIME::Parser MIME::Tools Mail::Header Mail::Internet
5468      Mail::ClamAV Mail::SpamAssassin Mail::SpamAssassin::SpamCopURI URI
5469      Razor2::Client::Version Mail::SPF::Query Authen::SASL
5470      Net::DNS Net::SMTP Net::Cmd Net::Server Net::LDAP
5471      DBI BerkeleyDB DB_File SAVI Unix::Syslog Time::HiRes);
5472    do_log(0, sprintf("Module %-19s %s", $m, $m->VERSION || '?'));
5473  }
5474  if (c('forward_method') eq '' && $extra_code_in_smtp) {
5475    do_log(1,"forward_method in default policy bank is null (milter setup?), ".
5476             "DISABLING SMTP-in AS A PRECAUTION");
5477    $extra_code_in_smtp = undef;
5478  }
5479  do_log(0,"Amavis::DB code       ".($extra_code_db     ?'':" NOT")." loaded");
5480  do_log(0,"Amavis::Cache code    ".($extra_code_cache  ?'':" NOT")." loaded");
5481  do_log(0,"Lookup::SQL code      ".($extra_code_sql    ?'':" NOT")." loaded");
5482  do_log(0,"Lookup::LDAP code     ".($extra_code_ldap   ?'':" NOT")." loaded");
5483  do_log(0,"AMCL-in protocol code ".($extra_code_in_amcl?'':" NOT")." loaded");
5484  do_log(0,"SMTP-in protocol code ".($extra_code_in_smtp?'':" NOT")." loaded");
5485  do_log(0,"QMQPqq-in protocol code ".($extra_code_in_qmqpqq?'':" NOT")." loaded");
5486  do_log(0,"ANTI-VIRUS code       ".($extra_code_antivirus?'':" NOT")." loaded");
5487  do_log(0,"ANTI-SPAM  code       ".($extra_code_antispam ?'':" NOT")." loaded");
5488  do_log(0,"Unpackers  code       ".($extra_code_unpackers?'':" NOT")." loaded");
5489
5490  # Prepare a hash of macros to be used in notification message expansion.
5491  # A key (macro name) must be a single character. Most characters are
5492  # allowed, but to be on the safe side and for clarity it is suggested
5493  # that only letters are used. Upper case letters may (as a mnemonic)
5494  # suggest the value is an array, lower case may suggest the value is
5495  # a scalar string - but this is only a convention and not enforced.
5496  #
5497  # A value may be a reference to a subroutine which will be called later at
5498  # the time of macro expansion. This way we can provide a method for obtaining
5499  # information which is not yet available, such as AV scanner results,
5500  # or provide a lazy evaluation for more expensive calculations.
5501  # Subroutine will be called in scalar context with no arguments.
5502  # It may return a scalar string (or undef), or an array reference.
5503  %builtins = (
5504    '.' => undef,
5505    p => sub {c('policy_bank_path')},
5506      # mail reception timestamp (e.g. start of SMTP transaction):
5507    d => sub {rfc2822_timestamp($MSGINFO->rx_time)}, # rfc2822 local date-time
5508#   U => sub {iso8601_timestamp($MSGINFO->rx_time)},     # iso8601, local time
5509    U => sub {iso8601_utc_timestamp($MSGINFO->rx_time)}, # iso8601 UTC
5510    y => sub {sprintf("%.0f", 1000*get_time_so_far())},  # elapsed time in ms
5511    u => sub {sprintf("%010d",$MSGINFO->rx_time)},   # s since Unix epoch (UTC)
5512
5513    h => $myhostname, # dns name of this host, or configurable name
5514    l => sub {my($ip) = $MSGINFO->client_addr; my($val);
5515              $val = $ip ne '' ? lookup_ip_acl($ip,@{ca('mynetworks_maps')})
5516                               : lookup(0,$MSGINFO->sender_source,
5517                                        @{ca('local_domains_maps')});
5518              $val ? 1 : undef}, # sender's client IP (if known) from @mynetworks
5519                                 # (if IP is known), or sender domain is local
5520    s => sub {qquote_rfc2821_local($MSGINFO->sender)}, # original envelope sender in <>
5521    S => sub { # unmangled sender or sender address to be notified, or empty...
5522               sanitize_str($MSGINFO->sender_contact) },  # ..if sender unknown
5523    o => sub { # best attempt at determining true sender (origin) of the virus,
5524               sanitize_str($MSGINFO->sender_source) },   # normally same as %s
5525    R => sub {$MSGINFO->recips},    # original message recipients list
5526    D => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $y}, # succ.delivered
5527    O => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $n}, # failed recips
5528    N => sub {my($y,$n,$f)=delivery_short_report($MSGINFO); $f}, # short dsn
5529    Q => sub {$MSGINFO->queue_id},  # MTA queue ID of the message if known
5530    m => sub { local($_) = $MSGINFO->mime_entity;   # Message-ID of the message
5531               if (defined) { $_ = $_->head->get('Message-ID',0); chomp;
5532                              s/^[ \t]+//; s/[ \t\n]+\z//;  # trim whitespace
5533                              # protect space and \n, other special characters
5534                              # will be sanitized before logging
5535                              s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }},
5536    r => sub { local($_) = $MSGINFO->mime_entity;   # first Resent-Message-ID
5537               if (defined) { $_ = $_->head->get('Resent-Message-ID',0); chomp;
5538                              s/^[ \t]+//; s/[ \t\n]+\z//;  # trim whitespace
5539                              s{([ =\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }},
5540    j => sub { local($_) = $MSGINFO->mime_entity;   # Subject of the message
5541               if (defined) { $_ = $_->head->get('Subject',0); chomp;
5542                              s/\n([ \t])/$1/g;  # unfold
5543                              s{([=\r\n])}{sprintf("=%02X",ord($1))}eg; $_ }},
5544    b => sub {$MSGINFO->body_digest},     # original message body digest
5545    n => \&am_id,                # amavis internal message id (for log entries)
5546    i => sub {$VIRUSFILE},       # some quarantine id, e.g. quarantine filename
5547    q => sub {my($q) = $MSGINFO->quarantined_to;
5548              !defined($q) ? undef :
5549                [map { my($m)=$_; $m=~s{^\Q$QUARANTINEDIR\E/}{}; $m } @$q];
5550             },  # list of quarantine mailboxes
5551    v => sub {[split(/[ \t]*\r?\n/,$av_output)]},   # anti-virus scanner output
5552    V => sub {my(%seen); [grep {!$seen{$_}++} @virusname]}, #unique virus names
5553    F => sub {@banned_filename<=1 ? \@banned_filename
5554                : [$banned_filename[0], '...'] },   # list of banned file names
5555    X => sub {\@bad_headers},        # list of header syntax violations
5556    W => sub {\@detecting_scanners}, # list of av scanners detecting a virus
5557    H => sub {[map {my $h=$_; chomp($h); $h} @{$MSGINFO->orig_header}]},# orig hdr
5558    A => sub {[split(/\r?\n/, $spam_report)]},      # SpamAssassin report lines
5559    c => sub {!defined $spam_level ? '-'            # SA hits/score
5560              : 0+sprintf("%.3f",$spam_level+min(map {$_->recip_score_boost}
5561                                                @{$MSGINFO->per_recip_data}))},
5562    z => sub {$MSGINFO->orig_body_size+1+$MSGINFO->orig_header_size}, # mail size
5563    t => sub { # first entry in the Received trace
5564               sanitize_str(first_received_from($MSGINFO->mime_entity)) },
5565    e => sub { # first valid public IP in the Received trace
5566               sanitize_str(best_try_originator_ip($MSGINFO->mime_entity)) },
5567    a => sub {$MSGINFO->client_addr}, # original SMTP session client IP address
5568    g => sub { # original SMTP session client DNS name
5569               sanitize_str($MSGINFO->client_name) },
5570    k => sub { my($kill_level);
5571               scalar(grep   # any recipient declared the message be killed ?
5572                 { !$_->recip_whitelisted_sender &&
5573                   ($_->recip_blacklisted_sender ||
5574                     ($kill_level=lookup(0,$_->recip_addr,
5575                                         @{ca('spam_kill_level_maps')}),
5576                      defined $spam_level && defined $kill_level &&
5577                      $spam_level + $_->recip_score_boost >= $kill_level) )
5578                 } @{$MSGINFO->per_recip_data}) },
5579    '1'=> sub { my($tag_level);
5580                scalar(grep  # above tag level for any recipient?
5581                 { !$_->recip_whitelisted_sender &&
5582                   ($_->recip_blacklisted_sender ||
5583                     ($tag_level=lookup(0,$_->recip_addr,
5584                                        @{ca('spam_tag_level_maps')}),
5585                      defined $spam_level && defined $tag_level &&
5586                      $spam_level + $_->recip_score_boost >= $tag_level) )
5587                 } @{$MSGINFO->per_recip_data}) },
5588    '2'=> sub { my($tag2_level);
5589                scalar(grep  # above tag2 level for any recipient?
5590                 { !$_->recip_whitelisted_sender &&
5591                   ($_->recip_blacklisted_sender ||
5592                     ($tag2_level=lookup(0,$_->recip_addr,
5593                                         @{ca('spam_tag2_level_maps')}),
5594                     defined $spam_level && defined $tag2_level &&
5595                     $spam_level + $_->recip_score_boost >= $tag2_level) )
5596                 } @{$MSGINFO->per_recip_data}) },
5597    # macros f, T, C, B will be defined for each notification as appropriate
5598    # (representing From:, To:, Cc:, and Bcc: respectively)
5599    # remaining free letters: wxyEGIJKLMPYZ
5600  );
5601
5602  # Map local virtual 'localpart' to a mailbox (e.g. to a quarantine filename
5603  # or a directory). Used by method 'local:', i.e. in mail_to_local_mailbox(),
5604  # for direct local quarantining. The hash value may be a ref to a pair of
5605  # fixed strings, or a subroutine ref (which must return a pair of strings
5606  # (a list, not a list ref)) which makes possible lazy evaluation
5607  # when some part of the pair is not known before the final delivery time.
5608  #
5609  # The first string in a pair must be either:
5610  #   - empty or undef, which will disable saving the message,
5611  #   - a filename, indicating a Unix-style mailbox,
5612  #   - a directory name, indicating a maildir-style mailbox,
5613  #     in which case the second string may provide a suggested file name.
5614  #
5615  %Amavis::Conf::local_delivery_aliases = (
5616    'virus-quarantine'    => sub { ($QUARANTINEDIR,  $VIRUSFILE) },
5617    'banned-quarantine'   => sub { ($QUARANTINEDIR,  $VIRUSFILE) },
5618    'bad-header-quarantine'=>sub { ($QUARANTINEDIR,  $VIRUSFILE) },
5619  # 'spam-quarantine'     => sub { ($QUARANTINEDIR,  $VIRUSFILE) },     # normal
5620    'spam-quarantine'     => sub { ($QUARANTINEDIR, "$VIRUSFILE.gz") }, # gzipped
5621
5622    # some more examples:
5623    'sender-quarantine' =>
5624      sub { my($s) = $MSGINFO->sender; local($1);
5625            $s =~ s/[^a-zA-Z0-9._@]/=/g; $s =~ s/\@/%/g;
5626            $s = untaint($s)  if $s =~ /^([a-zA-Z0-9._=%]+)\z/;  # untaint
5627            $s =~ s/%/%%/g;  # protect %
5628            ( $QUARANTINEDIR, "sender-$s-%i-%n.gz" );   # suggested file name
5629          },
5630    'recip-quarantine' =>
5631      sub { ("$QUARANTINEDIR/recip-archive.mbox", undef) },
5632    'ham-quarantine' =>
5633      sub { ("$QUARANTINEDIR/ham.mbox", undef) },
5634    'outgoing-quarantine' =>
5635      sub { ("$QUARANTINEDIR/outgoing.mbox", undef) },
5636    'incoming-quarantine' =>
5637      sub { ("$QUARANTINEDIR/incoming.mbox", undef) },
5638  );
5639  # store policy names into 'policy_bank_name' fields, if not explicitly set
5640  for my $name (keys %policy_bank) {
5641    if (ref($policy_bank{$name}) eq 'HASH' &&
5642        !exists($policy_bank{$name}{'policy_bank_name'})) {
5643      $policy_bank{$name}{'policy_bank_name'} = $name;
5644      $policy_bank{$name}{'policy_bank_path'} = $name;
5645    }
5646  }
5647};
5648
5649# overlay the current policy bank by settings from the
5650# $policy_bank{$policy_bank_name}, or load the default policy bank (empty name)
5651sub load_policy_bank($) {
5652  my($policy_bank_name) = @_;
5653  if (!exists $policy_bank{$policy_bank_name}) {
5654    do_log(-1,"policy bank \"$policy_bank_name\" does not exist, ignored");
5655  } elsif ($policy_bank_name eq '') {
5656    %current_policy_bank = %{$policy_bank{$policy_bank_name}};
5657    do_log(4,'loaded base policy bank');
5658  } else {
5659    my($cpbp) = c('policy_bank_path');  # currently loaded bank
5660    for my $k (keys %{$policy_bank{$policy_bank_name}}) {
5661      do_log(-1,"loading policy bank \"$policy_bank_name\": ".
5662                "unknown field \"$k\"")  if !exists $current_policy_bank{$k};
5663      $current_policy_bank{$k} = $policy_bank{$policy_bank_name}{$k};
5664    }
5665    $current_policy_bank{'policy_bank_path'} =
5666      ($cpbp eq '' ? '' : $cpbp.'/') . $policy_bank_name;
5667    do_log(2,sprintf('loaded policy bank "%s"%s', $policy_bank_name,
5668                     $cpbp eq '' ? '' : " over \"$cpbp\""));
5669  }
5670}
5671
5672### Net::Server hook
5673### This hook occurs after chroot, change of user, and change of group has
5674### occured.  It allows for preparation before looping begins.
5675sub pre_loop_hook {
5676  my($self) = @_;
5677  local $SIG{CHLD} = 'DEFAULT';
5678  eval {
5679    after_chroot_init();  # the rest of the top-level initialization
5680
5681    # this needs to be done only after chroot, otherwise paths will be wrong
5682    find_external_programs([split(/:/, $path, -1)]);
5683    # do some sanity checking
5684    my($name) = $TEMPBASE;
5685    $name = "$daemon_chroot_dir $name"  if $daemon_chroot_dir ne '';
5686    my($errn) = stat($TEMPBASE) ? 0 : 0+$!;
5687    if    ($errn==ENOENT) { die "No TEMPBASE directory: $name" }
5688    elsif ($errn)         { die "TEMPBASE directory inaccessible, $!: $name" }
5689    elsif (!-d _)         { die "TEMPBASE is not a directory: $name" }
5690    elsif (!-w _)         { die "TEMPBASE directory is not writable: $name" }
5691    if ($enable_global_cache && $extra_code_db) {
5692      my($name) = $db_home;
5693      $name = "$daemon_chroot_dir $name"  if $daemon_chroot_dir ne '';
5694      $errn = stat($db_home) ? 0 : 0+$!;
5695      if ($errn == ENOENT) {
5696        die "Please create an empty directory $name to hold a database".
5697            " (config variable \$db_home)\n" }
5698      elsif ($errn) { die "db_home inaccessible, $!: $name" }
5699      elsif (!-d _) { die "db_home is not a directory : $name" }
5700      elsif (!-w _) { die "db_home directory is not writable: $name" }
5701      Amavis::DB::init(1);
5702    }
5703    if ($QUARANTINEDIR ne '') {
5704      my($name) = $QUARANTINEDIR;
5705      $name = "$daemon_chroot_dir $name"  if $daemon_chroot_dir ne '';
5706      $errn = stat($QUARANTINEDIR) ? 0 : 0+$!;
5707      if    ($errn == ENOENT) { }  # ok
5708      elsif ($errn)         { die "QUARANTINEDIR inaccessible, $!: $name" }
5709      elsif (-d _ && !-w _) { die "QUARANTINEDIR directory not writable: $name" }
5710    }
5711    Amavis::SpamControl::init()  if $extra_code_antispam;
5712  };
5713  if ($@ ne '') {
5714    chomp($@); my($msg) = "TROUBLE in pre_loop_hook: $@"; do_log(-2,$msg);
5715    die ("Suicide (" . am_id() . ") " . $msg . "\n"); # kills child, not parent
5716  }
5717  1;
5718}
5719
5720### log routine Net::Server hook
5721### (Sys::Syslog MUST NOT be specified as a value of 'log_file'!)
5722#
5723# Redirect Net::Server logging to use Amavis' do_log().
5724# The main reason is that Net::Server uses Sys::Syslog
5725# (and has two bugs in doing it, at least the Net-Server-0.82),
5726# and Amavis users are acustomed to Unix::Syslog.
5727sub write_to_log_hook {
5728  my($self,$level,$msg) = @_;
5729  my($prop) = $self->{server};
5730  local $SIG{CHLD} = 'DEFAULT';
5731  chomp($msg);
5732  do_log(1, "Net::Server: " . $msg);  # just call Amavis' traditional logging
5733  1;
5734}
5735
5736### user customizable Net::Server hook
5737sub child_init_hook {
5738  my($self) = @_;
5739  local $SIG{CHLD} = 'DEFAULT';
5740  $0 = 'amavisd (virgin child)';
5741  eval {
5742    $db_env = $snmp_db = $body_digest_cache = undef;  # just in case
5743    Amavis::Timing::init(); snmp_counters_init();
5744    if ($extra_code_db) {
5745      $db_env = Amavis::DB->new;  # get access to a bdb environment
5746      $snmp_db = Amavis::DB::SNMP->new($db_env);
5747      $snmp_db->register_proc('')  if defined $snmp_db;  # process alive & idle
5748    }
5749    # if $db_env is undef the Amavis::Cache::new creates a memory-based cache
5750    $body_digest_cache = Amavis::Cache->new($db_env);
5751    if ($extra_code_db) {  # is it worth reporting the timing?
5752      section_time('bdb-open');
5753      do_log(2, Amavis::Timing::report());  # report elapsed times
5754    }
5755  };
5756  if ($@ ne '') {
5757    chomp($@); do_log(-2, "TROUBLE in child_init_hook: $@");
5758    die "Suicide in child_init_hook: $@\n";
5759  }
5760  Amavis::Timing::go_idle('vir');
5761}
5762
5763### user customizable Net::Server hook
5764sub post_accept_hook {
5765  my($self) = @_;
5766  local $SIG{CHLD} = 'DEFAULT';
5767  $child_invocation_count++;
5768  $0 = sprintf("amavisd (ch%d-accept)", $child_invocation_count);
5769  Amavis::Timing::go_busy('hi ');
5770  # establish initial time right after 'accept'
5771  Amavis::Timing::init(); snmp_counters_init();
5772  $snmp_db->register_proc('A')  if defined $snmp_db;  # in 'accept' state
5773  load_policy_bank('');    # start with a builting policy bank
5774}
5775
5776### user customizable Net::Server hook
5777### if this hook returns 1 the request is processed
5778### if this hook returns 0 the request is denied
5779sub allow_deny_hook {
5780  my($self) = @_;
5781  local($1,$2,$3,$4);  # Perl bug: $1 and $2 come tainted from Net::Server !
5782  local $SIG{CHLD} = 'DEFAULT';
5783  my($prop) = $self->{server}; my($sock) = $prop->{client}; my($bank_name);
5784  my($is_ux) = UNIVERSAL::can($sock,'NS_proto') && $sock->NS_proto eq 'UNIX';
5785  if ($is_ux) {
5786    $bank_name = $interface_policy{"SOCK"};  # possibly undef
5787  } else {
5788    my($myif,$myport) = ($prop->{sockaddr}, $prop->{sockport});
5789    if (defined $interface_policy{"$myif:$myport"}) {
5790      $bank_name = $interface_policy{"$myif:$myport"};
5791    } elsif (defined $interface_policy{$myport}) {
5792      $bank_name = $interface_policy{$myport};
5793    }
5794  }
5795  load_policy_bank($bank_name)  if defined $bank_name &&
5796                                   $bank_name ne c('policy_bank_name');
5797  # note that the new policy bank may have replaced the inet_acl access table
5798  if ($is_ux) {
5799    # always permit access - unix sockets are immune to this check
5800  } else {
5801    my($permit,$fullkey) = lookup_ip_acl($prop->{peeraddr},ca('inet_acl'));
5802    if (!$permit) {
5803      my($msg) = sprintf("DENIED ACCESS from IP %s, policy bank '%s'",
5804                         $prop->{peeraddr}, c('policy_bank_name') );
5805      $msg .= ", blocked by rule $fullkey"  if defined $fullkey;
5806      do_log(-1,$msg);
5807      return 0;
5808    }
5809  }
5810  1;
5811}
5812
5813# use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
5814# sub cloexec_on($;$) {
5815#   my($fd,$name) = @_; my($flags);
5816#   $flags = fcntl($fd, F_GETFD, 0)
5817#     or die "Can't get flags from the file descriptor: $!";
5818#   if ($flags & FD_CLOEXEC == 0) {
5819#     do_log(4,"Turning on FD_CLOEXEC flag on $name");
5820#     fcntl($fd, F_SETFD, $flags | FD_CLOEXEC)
5821#       or die "Can't set FD_CLOEXEC on file descriptor $name: $!";
5822#   }
5823# }
5824
5825### The heart of the program
5826### user customizable Net::Server hook
5827sub process_request {
5828  my($self) = shift;
5829  my($prop) = $self->{server}; my($sock) = $prop->{client};
5830  local $SIG{CHLD} = 'DEFAULT';
5831  local($1,$2,$3,$4);  # Perl bug: $1 and $2 come tainted from Net::Server !
5832  # Net::Server assigns STDIN and STDOUT to the socket
5833  binmode(STDIN)  or die "Can't set STDIN to binmode: $!";
5834  binmode(STDOUT) or die "Can't set STDOUT to binmode: $!";
5835  binmode($sock)  or die "Can't set socket to binmode: $!";
5836  $| = 1;
5837  local $SIG{ALRM} = sub { die "timed out\n" };  # do not modify the sig text!
5838  eval {
5839#   if ($] < 5.006) { # Perl older than 5.6.0 did not set FD_CLOEXEC on sockets
5840#     for my $mysock (@{$prop->{sock}}) { cloexec_on($mysock, $mysock) }
5841#   }
5842    prolong_timer('new request - timer reset', $child_timeout);  # timer init
5843    if ($extra_code_ldap && !defined $ldap_policy) {
5844      # make LDAP lookup object
5845      $ldap_policy = Amavis::Lookup::LDAP->new($default_ldap);
5846    }
5847    if (defined $ldap_policy && !$implicit_maps_inserted) {
5848      # make SQL field lookup objects with incorporated field names
5849      # fieldtype: B=boolean, N=numeric, S=string, L=list
5850      #            B-, N-, S-, L-  returns undef if field does not exist
5851      #            B0: boolean, nonexistent field treated as false,
5852      #            B1: boolean, nonexistent field treated as true
5853      my $lf = sub{Amavis::Lookup::LDAPattr->new($ldap_policy,@_)};
5854      unshift(@Amavis::Conf::virus_lovers_maps,        $lf->('amavisVirusLover',         'B-'));
5855      unshift(@Amavis::Conf::spam_lovers_maps,         $lf->('amavisSpamLover',          'B-'));
5856      unshift(@Amavis::Conf::banned_files_lovers_maps, $lf->('amavisBannedFilesLover',   'B-'));
5857      unshift(@Amavis::Conf::bad_header_lovers_maps,   $lf->('amavisBadHeaderLover',     'B-'));
5858      unshift(@Amavis::Conf::bypass_virus_checks_maps, $lf->('amavisBypassVirusChecks',  'B-'));
5859      unshift(@Amavis::Conf::bypass_spam_checks_maps,  $lf->('amavisBypassSpamChecks',   'B-'));
5860      unshift(@Amavis::Conf::bypass_banned_checks_maps,$lf->('amavisBypassBannedChecks', 'B-'));
5861      unshift(@Amavis::Conf::bypass_header_checks_maps,$lf->('amavisBypassHeaderChecks', 'B-'));
5862      unshift(@Amavis::Conf::spam_tag_level_maps,      $lf->('amavisSpamTagLevel',       'N'));
5863      unshift(@Amavis::Conf::spam_tag2_level_maps,     $lf->('amavisSpamTag2Level',      'N'));
5864      unshift(@Amavis::Conf::spam_kill_level_maps,     $lf->('amavisSpamKillLevel',      'N'));
5865      unshift(@Amavis::Conf::spam_modifies_subj_maps,  $lf->('amavisSpamModifiesSubj',   'B-'));
5866      unshift(@Amavis::Conf::message_size_limit_maps,  $lf->('amavisMessageSizeLimit',   'N-'));
5867      unshift(@Amavis::Conf::virus_quarantine_to_maps, $lf->('amavisVirusQuarantineTo',  'S-'));
5868      unshift(@Amavis::Conf::spam_quarantine_to_maps,  $lf->('amavisSpamQuarantineTo',   'S-'));
5869      unshift(@Amavis::Conf::banned_quarantine_to_maps, $lf->('amavisBannedQuarantineTo','S-'));
5870      unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $lf->('amavisBadHeaderQuarantineTo', 'S-'));
5871      unshift(@Amavis::Conf::local_domains_maps,       $lf->('amavisLocal',
5872         'B1'));
5873      unshift(@Amavis::Conf::warnvirusrecip_maps,      $lf->('amavisWarnVirusRecip',     'B-'));
5874      unshift(@Amavis::Conf::warnbannedrecip_maps,     $lf->('amavisWarnBannedRecip',    'B-'));
5875      unshift(@Amavis::Conf::warnbadhrecip_maps,       $lf->('amavisWarnBadHeaderRecip', 'B-'));
5876      section_time('ldap-prepare');
5877    }
5878    if ($extra_code_sql && @lookup_sql_dsn) {
5879      # make SQL lookup objects which will hold SELECT and DBI handles
5880      $sql_wblist = Amavis::Lookup::SQL->new
5881        if !defined $sql_wblist && defined $sql_select_white_black_list;
5882      $sql_policy = Amavis::Lookup::SQL->new
5883        if !defined $sql_policy && defined $sql_select_policy;
5884    }
5885    if (defined $sql_policy && !$implicit_maps_inserted) {
5886      # make SQL field lookup objects with incorporated field names
5887      # fieldtype: B=boolean, N=numeric, S=string,
5888      #            B-, N-, S-   returns undef if field does not exist
5889      #            B0: boolean, nonexistent field treated as false,
5890      #            B1: boolean, nonexistent field treated as true
5891      my $nf = sub{Amavis::Lookup::SQLfield->new($sql_policy,@_)}; #shorthand
5892      $user_id_sql =                                    $nf->('id',                   'S');
5893      unshift(@Amavis::Conf::local_domains_maps,        $nf->('local',                'B1'));
5894
5895      unshift(@Amavis::Conf::virus_lovers_maps,         $nf->('virus_lover',          'B0'));
5896      unshift(@Amavis::Conf::spam_lovers_maps,          $nf->('spam_lover',           'B-'));
5897      unshift(@Amavis::Conf::banned_files_lovers_maps,  $nf->('banned_files_lover',   'B-'));
5898      unshift(@Amavis::Conf::bad_header_lovers_maps,    $nf->('bad_header_lover',     'B-'));
5899
5900      unshift(@Amavis::Conf::bypass_virus_checks_maps,  $nf->('bypass_virus_checks',  'B0'));
5901      unshift(@Amavis::Conf::bypass_spam_checks_maps,   $nf->('bypass_spam_checks',   'B0'));
5902      unshift(@Amavis::Conf::bypass_banned_checks_maps, $nf->('bypass_banned_checks', 'B-'));
5903      unshift(@Amavis::Conf::bypass_header_checks_maps, $nf->('bypass_header_checks', 'B-'));
5904
5905      unshift(@Amavis::Conf::spam_tag_level_maps,       $nf->('spam_tag_level',       'N'));
5906      unshift(@Amavis::Conf::spam_tag2_level_maps,      $nf->('spam_tag2_level',      'N'));
5907      unshift(@Amavis::Conf::spam_kill_level_maps,      $nf->('spam_kill_level',      'N'));
5908      unshift(@Amavis::Conf::spam_dsn_cutoff_level_maps,$nf->('spam_dsn_cutoff_level','N-'));
5909
5910      unshift(@Amavis::Conf::spam_modifies_subj_maps,   $nf->('spam_modifies_subj',   'B-'));
5911      unshift(@Amavis::Conf::spam_subject_tag_maps,     $nf->('spam_subject_tag',     'S-'));
5912      unshift(@Amavis::Conf::spam_subject_tag2_maps,    $nf->('spam_subject_tag2',    'S-'));
5913
5914      unshift(@Amavis::Conf::virus_quarantine_to_maps,  $nf->('virus_quarantine_to',  'S-'));
5915      unshift(@Amavis::Conf::banned_quarantine_to_maps, $nf->('banned_quarantine_to', 'S-'));
5916      unshift(@Amavis::Conf::bad_header_quarantine_to_maps, $nf->('bad_header_quarantine_to','S-'));
5917      unshift(@Amavis::Conf::spam_quarantine_to_maps,   $nf->('spam_quarantine_to',   'S-'));
5918      unshift(@Amavis::Conf::message_size_limit_maps,   $nf->('message_size_limit',   'N-'));
5919
5920      unshift(@Amavis::Conf::addr_extension_virus_maps, $nf->('addr_extension_virus', 'S-'));
5921      unshift(@Amavis::Conf::addr_extension_spam_maps,  $nf->('addr_extension_spam',  'S-'));
5922      unshift(@Amavis::Conf::addr_extension_banned_maps,$nf->('addr_extension_banned','S-'));
5923      unshift(@Amavis::Conf::addr_extension_bad_header_maps,$nf->('addr_extension_bad_header','S-'));
5924
5925      unshift(@Amavis::Conf::warnvirusrecip_maps,       $nf->('warnvirusrecip',       'B-'));
5926      unshift(@Amavis::Conf::warnbannedrecip_maps,      $nf->('warnbannedrecip',      'B-'));
5927      unshift(@Amavis::Conf::warnbadhrecip_maps,        $nf->('warnbadhrecip',        'B-'));
5928
5929      unshift(@Amavis::Conf::newvirus_admin_maps,       $nf->('newvirus_admin',       'S-'));
5930      unshift(@Amavis::Conf::virus_admin_maps,          $nf->('virus_admin',          'S-'));
5931      unshift(@Amavis::Conf::banned_admin_maps,         $nf->('banned_admin',         'S-'));
5932      unshift(@Amavis::Conf::bad_header_admin_maps,     $nf->('bad_header_admin',     'S-'));
5933      unshift(@Amavis::Conf::spam_admin_maps,           $nf->('spam_admin',           'S-'));
5934
5935      section_time('sql-prepare');
5936    }
5937    Amavis::Conf::label_default_maps()  if !$implicit_maps_inserted;
5938    $implicit_maps_inserted = 1;
5939
5940    my($conn) = Amavis::In::Connection->new;
5941    $CONN = $conn;  # ugly - save in a global
5942    $conn->proto($sock->NS_proto);
5943    my($suggested_protocol) = c('protocol');  # suggested by the policy bank
5944    do_log(5,"process_request: suggested_protocol=\"$suggested_protocol\" on ".
5945             $sock->NS_proto);
5946    if ($sock->NS_proto eq 'UNIX') {     # traditional amavis helper program
5947      if ($suggested_protocol eq 'COURIER') {
5948        die "unavailable support for protocol: $suggested_protocol";
5949      } elsif ($suggested_protocol eq 'AM.PDP') {
5950        $amcl_in_obj = Amavis::In::AMCL->new  if !$amcl_in_obj;
5951        $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0);
5952      } else {  # default to old amavis helper program protocol
5953        $amcl_in_obj = Amavis::In::AMCL->new  if !$amcl_in_obj;
5954        $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 1);
5955      }
5956    } elsif ($sock->NS_proto eq 'TCP') {
5957      $conn->socket_ip($prop->{sockaddr});
5958      $conn->socket_port($prop->{sockport});
5959      $conn->client_ip($prop->{peeraddr});
5960      if ($suggested_protocol eq 'TCP-LOOKUP') {  # postfix maps (experimental)
5961        process_tcp_lookup_request($sock, $conn);
5962        do_log(2, Amavis::Timing::report());  # report elapsed times
5963      } elsif ($suggested_protocol eq 'AM.PDP') {
5964        # amavis policy delegation protocol (e.g. new milter helper program)
5965        $amcl_in_obj = Amavis::In::AMCL->new  if !$amcl_in_obj;
5966        $amcl_in_obj->process_policy_request($sock, $conn, \&check_mail, 0);
5967      } elsif ($suggested_protocol eq 'QMQPqq') {
5968        if (!$extra_code_in_qmqpqq) {
5969          die "incoming TCP connection, but dynamic QMQPqq code not loaded";
5970        }
5971        $qmqpqq_in_obj = Amavis::In::QMQPqq->new if !$qmqpqq_in_obj;
5972        $qmqpqq_in_obj->process_qmqpqq_request($sock,$conn,\&check_mail);
5973      } else {  # defaults to SMTP or LMTP
5974        if (!$extra_code_in_smtp) {
5975          die "incoming TCP connection, but dynamic SMTP/LMTP code not loaded";
5976        }
5977        $smtp_in_obj = Amavis::In::SMTP->new  if !$smtp_in_obj;
5978        $smtp_in_obj->process_smtp_request(
5979              $sock, ($suggested_protocol eq 'LMTP'?1:0), $conn, \&check_mail);
5980      }
5981    } else {
5982      die ("unsupported protocol: $suggested_protocol, " . $sock->NS_proto);
5983    }
5984  };  # eval
5985  alarm(0);          # stop the timer
5986  if ($@ ne '') {
5987    chomp($@); my($timed_out) = $@ eq "timed out";
5988    my($msg) = $timed_out ? "Child task exceeded $child_timeout seconds, abort"
5989                          : "TROUBLE in process_request: $@";
5990    do_log(-2, $msg);
5991    $smtp_in_obj->preserve_evidence(1)  if $smtp_in_obj && !$timed_out;
5992    # kills a child, hopefully preserving tempdir; does not kill parent
5993    do_log(-1, "Requesting process rundown after fatal error");
5994    $self->done(1);
5995#   die ("Suicide (" . am_id() . ") " . $msg . "\n");
5996  }
5997
5998  my(@modules_extra) = grep {!exists $modules_basic{$_}} keys %INC;
5999# do_log(0, "modules loaded: ".join(", ", sort keys %modules_basic));
6000  do_log(1, "extra modules loaded: ".
6001            join(", ", sort @modules_extra))  if @modules_extra;
6002
6003# if ($child_task_count >= $max_requests &&
6004#     $child_invocation_count < $max_requests) {
6005  if ($child_task_count > $max_requests) {
6006    # in case of multiple-transaction protocols (e.g. SMTP, LMTP)
6007    # we do not like to keep running indefinitely at the MTA's mercy
6008    do_log(1, "Requesting process rundown after $child_task_count tasks ".
6009              "(and $child_invocation_count sessions)");
6010    $self->done(1);
6011  }
6012}
6013
6014### override Net::Server::PreForkSimple::done
6015### to be able to rundown the child process prematurely
6016sub done(@) {
6017  my($self) = shift;
6018  if (@_) { $self->{server}->{done} = shift }
6019  elsif (!$self->{server}->{done})
6020    { $self->{server}->{done} = $self->SUPER::done }
6021  $self->{server}->{done};
6022}
6023
6024### Net::Server hook
6025sub post_process_request_hook {
6026  my($self) = @_;
6027  local $SIG{CHLD} = 'DEFAULT';
6028  debug_oneshot(0);
6029  $0 = sprintf("amavisd (ch%d-avail)", $child_invocation_count);
6030  alarm(0); do_log(5,"post_process_request_hook: timer stopped");
6031  $snmp_db->register_proc('')  if defined $snmp_db; # process is alive and idle
6032  Amavis::Timing::go_idle('bye'); Amavis::Timing::report_load();
6033}
6034
6035### Child is about to be terminated
6036### user customizable Net::Server hook
6037sub child_finish_hook {
6038  my($self) = @_;
6039  local $SIG{CHLD} = 'DEFAULT';
6040# for my $m (sort map { s/\.pm\z//; s[/][::]g; $_ } grep { /\.pm\z/ } keys %INC){
6041#   do_log(0, sprintf("Module %-19s %s", $m, $m->VERSION || '?'))
6042#     if grep {$m=~/^$_/} qw(Mail::ClamAV Mail::SpamAssassin Razor2 Net::DNS);
6043# }
6044  $0 = sprintf("amavisd (ch%d-finish)", $child_invocation_count);
6045  do_log(5,"child_finish_hook: invoking DESTROY methods");
6046  $smtp_in_obj = undef;  # calls Amavis::In::SMTP::DESTROY
6047  $qmqpqq_in_obj = undef;  # calls Amavis::In::QMQPqq::DESTROY
6048  $amcl_in_obj = undef;  # (currently does nothing for Amavis::In::AMCL)
6049  $sql_wblist = undef;   # calls Amavis::Lookup::SQL::DESTROY
6050  $sql_policy = undef;   # calls Amavis::Lookup::SQL::DESTROY
6051  $ldap_policy = undef;  # calls Amavis::Lookup::LDAP::DESTROY
6052  $body_digest_cache = undef;  # calls Amavis::Cache::DESTROY
6053  eval { $snmp_db->register_proc(undef) }  if defined $snmp_db;  # going away
6054  $snmp_db = undef;      # calls Amavis::DB::SNMP::DESTROY
6055  $db_env = undef;
6056}
6057
6058sub END {                # runs before exiting the module
6059  do_log(5,"at the END handler: invoking DESTROY methods");
6060  $smtp_in_obj = undef;  # at end calls Amavis::In::SMTP::DESTROY
6061  $qmqpqq_in_obj = undef;  # at end calls Amavis::In::QMQPqq::DESTROY
6062  $amcl_in_obj = undef;  # (currently does nothing for Amavis::In::AMCL)
6063  $sql_wblist = undef;   # at end calls Amavis::Lookup::SQL::DESTROY
6064  $sql_policy = undef;   # at end calls Amavis::Lookup::SQL::DESTROY
6065  $ldap_policy = undef;  # at end calls Amavis::Lookup::LDAP::DESTROY
6066  $body_digest_cache = undef;  # at end calls Amavis::Cache::DESTROY
6067  eval { $snmp_db->register_proc(undef) }  if defined $snmp_db;  # going away
6068  $snmp_db = undef;      # at end calls Amavis::DB::SNMP::DESTROY
6069  $db_env = undef;
6070}
6071
6072# implements Postfix TCP lookup server, see tcp_table(5) man page; experimental
6073sub process_tcp_lookup_request($$) {
6074  my($sock, $conn) = @_;
6075  local($/) = "\012";  # set line terminator to LF (regardless of platform)
6076  my($req_cnt);
6077  while (<$sock>) {
6078    $req_cnt++; my($level) = 0;
6079    my($resp_code, $resp_msg) = (400, 'INTERNAL ERROR');
6080    if (/^get (.*?)\015?\012\z/si) {
6081      my($key) = tcp_lookup_decode($1);
6082      my($sl); $sl = lookup(0,$key, @{ca('spam_lovers_maps')});
6083      $resp_code = 200; $level = 2;
6084      $resp_msg = $sl ? "OK Recipient <$key> IS spam lover"
6085                      : "DUNNO Recipient <$key> is NOT spam lover";
6086    } elsif (/^put ([^ ]*) (.*?)\015?\012\z/si) {
6087      $resp_code = 500; $resp_msg = 'request not implemented: ' . $_;
6088    } else {
6089      $resp_code = 500; $resp_msg = 'illegal request: ' . $_;
6090    }
6091    do_log($level, "tcp_lookup($req_cnt): $resp_code $resp_msg");
6092    $sock->printf("%03d %s\012", $resp_code, tcp_lookup_encode($resp_msg))
6093      or die "Can't write to tcp_lookup socket: $!";
6094  }
6095  do_log(0, "tcp_lookup: RUNDOWN after $req_cnt requests");
6096}
6097
6098sub tcp_lookup_encode($) {
6099  my($str) = @_;
6100  $str =~ s/[^\041-\044\046-\176]/sprintf("%%%02x",ord($&))/eg;
6101  $str;
6102}
6103
6104sub tcp_lookup_decode($) {
6105  my($str) = @_;
6106  $str =~ s/%([0-9a-fA-F]{2})/pack("C",hex($1))/eg;
6107  $str;
6108}
6109
6110# Checks the message stored on a file. File must already
6111# be open on file handle $msginfo->mail_text; it need not be positioned
6112# properly, check_mail must not close the file handle.
6113#
6114sub check_mail($$$$) {
6115  my($conn, $msginfo, $dsn_per_recip_capable, $tempdir) = @_;
6116
6117  my($am_id) = am_id();
6118  $snmp_db->register_proc($am_id)  if defined $snmp_db;
6119  my($fh) = $msginfo->mail_text; my(@recips) = @{$msginfo->recips};
6120
6121  $MSGINFO = $msginfo;  # ugly - save in a global, to make it accessible
6122                        # to %builtins
6123  # check_mail() may be called several times per child lifetime and/or
6124  # per-SMTP session. The variable $child_task_count is mainly used
6125  # by AV-scanner interfaces, e.g. to initialize when invoked
6126  # for the first time during child process lifetime.
6127  $child_task_count++;
6128
6129  # reset certain global variables for each task
6130  $VIRUSFILE = undef; $av_output = undef; @detecting_scanners = ();
6131  @virusname = (); @banned_filename = (); @bad_headers = ();
6132  $spam_level = undef; $spam_status = undef; $spam_report = undef;
6133
6134  # comment out to retain SQL cache entries for the whole child lifetime:
6135  $sql_policy->clear_cache  if defined $sql_policy;
6136  $sql_wblist->clear_cache  if defined $sql_wblist;
6137
6138  # comment out to retain LDAP cache entries for the whole child lifetime:
6139  $ldap_policy->clear_cache  if defined $ldap_policy;
6140
6141  # also measures mail size and saves mail header
6142  $body_digest = get_body_digest($fh, $msginfo);
6143
6144  my($mail_size) = $msginfo->msg_size;  # use ESMTP size if available
6145  $mail_size = $msginfo->orig_header_size + 1 + $msginfo->orig_body_size
6146    if $mail_size <= 0;
6147# $mail_size = -s $msginfo->mail_text_fn;  # get it from a file system
6148
6149  my($file_generator_object) =   # maxfiles 0 disables the $MAXFILES limit
6150    Amavis::Unpackers::NewFilename->new($MAXFILES?$MAXFILES:undef, $mail_size);
6151  Amavis::Unpackers::Part::init($file_generator_object); # fudge: keep in variable
6152  my($parts_root) = Amavis::Unpackers::Part->new;
6153  $msginfo->parts_root($parts_root);
6154  my($smtp_resp, $exit_code, $preserve_evidence); my($virus_dejavu) = 0;
6155  my($banned_filename_checked,$virus_presence_checked,$spam_presence_checked);
6156
6157  # matching banned rules suggest DSN to be suppressed?
6158  my($banned_dsn_suppress) = 0;
6159
6160  # is any mail component password protected or otherwise non-decodable?
6161  my($any_undecipherable) = 0;
6162
6163  my($cl_ip) = $msginfo->client_addr; my($pbn) = c('policy_bank_path');
6164  do_log(1,sprintf("Checking: %s%s%s -> %s",
6165                   $pbn eq   '' ? '' : "$pbn ",
6166                   $cl_ip eq '' ? '' : "[$cl_ip] ",
6167                   qquote_rfc2821_local($msginfo->sender),
6168                   join(',', qquote_rfc2821_local(@recips)) ));
6169
6170  my($mime_err); # undef, or MIME parsing error string as given by MIME::Parser
6171  my($hold);           # set to some string to cause the message to be
6172                       # placed on hold (frozen) by MTA. This can be used
6173                       # in cases when we stumble across some permanent problem
6174                       # making us unable to decide if the message is to be
6175                       # really delivered.
6176  my($which_section);
6177  eval {
6178    snmp_count('InMsgs');
6179    snmp_count('InMsgsNullRPath')  if $msginfo->sender eq '';
6180    if    (@recips == 1) { snmp_count(  'InMsgsRecips' ) }
6181    elsif (@recips >  1) { snmp_count( ['InMsgsRecips',scalar(@recips)] ) }
6182
6183    $which_section = "creating_partsdir";
6184    if (-d "$tempdir/parts") {
6185      # mkdir is a costly operation (must be atomic, flushes buffers).
6186      # If we can re-use directory 'parts' from the previous invocation
6187      # it saves us precious time. Together with matching rmdir this can
6188      # amount to 10-15 % of total elapsed time !!!  (no spam checking)
6189    } else {
6190      mkdir("$tempdir/parts", 0750)
6191        or die "Can't create directory $tempdir/parts: $!";
6192      section_time('mkdir parts');
6193    }
6194    chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
6195
6196    # FIRST: what kind of e-mail did we get? call content scanners
6197
6198    # already in cache?
6199    $which_section = "cached";
6200    snmp_count('CacheAttempts');
6201    my($cache_entry); my($now) = time;
6202    my($cache_entry_ttl) =
6203      max($virus_check_negative_ttl, $virus_check_positive_ttl,
6204          $spam_check_negative_ttl,  $spam_check_positive_ttl);
6205    my($now_utc_iso8601)     = iso8601_utc_timestamp($now,1);
6206    my($expires_utc_iso8601) = iso8601_utc_timestamp($now+$cache_entry_ttl,1);
6207    $cache_entry = $body_digest_cache->get($body_digest)
6208      if $body_digest_cache && defined $body_digest;
6209    if (!defined $cache_entry) {
6210      snmp_count('CacheMisses');
6211      $cache_entry->{'ctime'} = $now_utc_iso8601;  # create a new cache record
6212    } else {
6213      snmp_count('CacheHits');
6214      $banned_filename_checked = defined $cache_entry->{'FB'} ? 1 : 0;
6215      $virus_presence_checked  = defined $cache_entry->{'VN'} ? 1 : 0;
6216
6217      # spam level and spam report may be influenced by mail header, not only
6218      # by mail body, so caching based on body is only a close approximation;
6219      # ignore spam cache if body is too small
6220      $spam_presence_checked = defined $cache_entry->{'SL'} ? 1 : 0;
6221      if ($msginfo->orig_body_size < 200) { $spam_presence_checked = 0 }
6222
6223      if ($virus_presence_checked && defined $cache_entry->{'Vt'}) {
6224        # check for expiration of cached virus test results
6225        my($ttl) = !@{$cache_entry->{'VN'}} ? $virus_check_negative_ttl
6226                                            : $virus_check_positive_ttl;
6227        if ($now > $cache_entry->{'Vt'} + $ttl) {
6228          do_log(2,"Cached virus check expired, TTL = $ttl s");
6229          $virus_presence_checked  = 0;
6230        }
6231      }
6232      if ($spam_presence_checked && defined $cache_entry->{'St'}) {
6233        # check for expiration of cached spam test results
6234        # (note: hard-wired spam level 6)
6235        my($ttl) = $cache_entry->{'SL'} < 6  ? $spam_check_negative_ttl
6236                                             : $spam_check_positive_ttl;
6237        if ($now > $cache_entry->{'St'} + $ttl) {
6238          do_log(2,"Cached spam check expired, TTL = $ttl s");
6239          $spam_presence_checked  = 0;
6240        }
6241      }
6242      if ($virus_presence_checked) {
6243        $av_output = $cache_entry->{'VO'};
6244        @virusname = @{$cache_entry->{'VN'}};
6245        @detecting_scanners = @{$cache_entry->{'VD'}};
6246        $virus_dejavu = 1;
6247      }
6248      if ($banned_filename_checked) {
6249        @banned_filename = @{$cache_entry->{'FB'}};
6250        $banned_dsn_suppress = $cache_entry->{'FS'};
6251      }
6252      ($spam_level, $spam_status, $spam_report) = @$cache_entry{'SL','SS','SR'}
6253        if $spam_presence_checked;
6254      do_log(1,sprintf("cached %s from <%s> (%s,%s,%s)",
6255                       $body_digest, $msginfo->sender,
6256                       $banned_filename_checked, $virus_presence_checked,
6257                       $spam_presence_checked));
6258      snmp_count('CacheHitsVirusCheck')   if $virus_presence_checked;
6259      snmp_count('CacheHitsVirusMsgs')    if @virusname;
6260      snmp_count('CacheHitsSpamCheck')    if $spam_presence_checked;
6261      snmp_count('CacheHitsSpamMsgs')     if $spam_level >= 6;  # a hack
6262#     snmp_count('CacheHitsBannedCheck')  if $banned_filename_checked;
6263#     snmp_count('CacheHitsBannedMsgs')   if @banned_filename;
6264      do_log(5,sprintf("cache entry age: %s c=%s a=%s",
6265                  (@virusname ? 'V' : $spam_level > 5 ? 'S' : '.'),
6266                  $cache_entry->{'ctime'}, $cache_entry->{'atime'} ));
6267    }  # if defined $cache_entry
6268
6269    my($will_do_virus_scanning) =   # virus scanning will be needed?
6270       !$virus_presence_checked && $extra_code_antivirus &&
6271       grep {!lookup(0,$_, @{ca('bypass_virus_checks_maps')})} @recips;
6272
6273    my($will_do_banned_checking) =  # banned name checking will be needed?
6274       !$banned_filename_checked &&
6275       (@{ca('banned_filename_maps')} || cr('banned_namepath_re')) &&
6276       grep {!lookup(0,$_, @{ca('bypass_banned_checks_maps')})} @recips;
6277
6278    # will do decoding parts as deeply as possible?  only if needed
6279    my($will_do_parts_decoding) =
6280       !c('bypass_decode_parts') &&
6281       ($will_do_virus_scanning || $will_do_banned_checking);
6282
6283    $which_section = "mime_decode-1";
6284    my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
6285    $msginfo->mime_entity($ent);
6286    prolong_timer($which_section);
6287
6288    if ($will_do_parts_decoding) {
6289      # decoding parts can take a lot of time!
6290      snmp_count('OpsDec');
6291      ($hold,$any_undecipherable) =
6292        Amavis::Unpackers::decompose_mail($tempdir,$file_generator_object);
6293    }
6294    if (grep {!lookup(0,$_,@{ca('bypass_header_checks_maps')})} @recips) {
6295      push(@bad_headers, "MIME error: ".$mime_err)  if $mime_err ne '';
6296      push(@bad_headers, check_header_validity($conn,$msginfo));
6297    }
6298    if ($will_do_banned_checking) {      # check for banned file contents
6299      $which_section = "check-banned";
6300      my($banned_part_descr_ref, $banned_matching_keys_ref, $banned_rhs_ref) =
6301        check_for_banned_names($parts_root);
6302      for my $j (0..$#{$banned_part_descr_ref}) {
6303        if ($banned_rhs_ref->[$j] =~ /^DISCARD/) {
6304          $banned_dsn_suppress = 1;
6305          do_log(4,sprintf('BANNED:%s: %s', $banned_rhs_ref->[$j],
6306                           $banned_part_descr_ref->[$j]));
6307        }
6308      }
6309      push(@banned_filename, @$banned_part_descr_ref);
6310    }
6311    $cache_entry->{'FB'} = \@banned_filename;
6312    $cache_entry->{'FS'} = $banned_dsn_suppress;
6313
6314    if ($virus_presence_checked) {
6315      do_log(5, "virus_presence cached, skipping virus_scan");
6316    } elsif (!$extra_code_antivirus) {
6317      do_log(5, "no anti-virus code loaded, skipping virus_scan");
6318    } elsif (!grep {!lookup(0,$_,@{ca('bypass_virus_checks_maps')})} @recips) {
6319      do_log(5, "bypassing of virus checks requested");
6320    } elsif ($hold ne '') {  # protect virus scanner from mail bombs
6321      do_log(0, "NOTICE: Virus scanning skipped: $hold");
6322      $will_do_virus_scanning = 0;
6323    } else {
6324      if (!$will_do_virus_scanning)
6325        { do_log(-1, "NOTICE: will_do_virus_scanning is false???") }
6326      if (!defined($msginfo->mime_entity)) {
6327        $which_section = "mime_decode-3";
6328        my($ent); ($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
6329        $msginfo->mime_entity($ent);
6330        prolong_timer($which_section);
6331      }
6332      # special case to make available a complete mail file for inspection
6333      if ($mime_err ne '' ||
6334          lookup(0,'MAIL',@keep_decoded_original_maps) ||
6335          $any_undecipherable && lookup(0,'MAIL-UNDECIPHERABLE',
6336                                        @keep_decoded_original_maps)) {
6337        # keep the original email.txt by making a hard link to it in ./parts/
6338        $which_section = "linking-to-MAIL";
6339        my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",
6340                                                        $parts_root);
6341        my($newpart) = $newpart_obj->full_name;
6342        do_log(2, "providing full original message to scanners as $newpart".
6343           (!$any_undecipherable ?'' :", $any_undecipherable undecipherable").
6344           ($mime_err eq '' ? '' : ", MIME error: $mime_err") );
6345        link($msginfo->mail_text_fn, $newpart)
6346          or die sprintf("Can't create hard link %s to %s: %s",
6347                         $newpart, $msginfo->mail_text_fn, $!);
6348        $newpart_obj->type_short('MAIL');
6349        $newpart_obj->type_declared('message/rfc822');
6350      }
6351      $which_section = "virus_scan";
6352      # some virus scanners behave badly if interrupted,
6353      # so for now just turn off the timer
6354      my($remaining_time) = alarm(0);  # check time left, stop timer
6355      my($av_ret);
6356      eval {
6357        my($vn, $ds);
6358        ($av_ret, $av_output, $vn, $ds) =
6359          Amavis::AV::virus_scan($tempdir, $child_task_count==1, $parts_root);
6360        @virusname = @$vn; @detecting_scanners = @$ds;  # copy
6361      };
6362      prolong_timer($which_section, $remaining_time);   # restart timer
6363      if ($@ ne '') {
6364        chomp($@);
6365        if ($@ eq "timed out") {
6366          @virusname = (); $av_ret = 0;  # assume not a virus!
6367          do_log(-1, "virus_scan TIMED OUT, ASSUME NOT A VIRUS !!!");
6368        } else {
6369          $hold = "virus_scan: $@";  # request HOLD
6370          $av_ret = 0;               # pretend it was ok (msg should be held)
6371          die "$hold\n";             # die, TEMPFAIL is preferred to HOLD
6372        }
6373      }
6374      snmp_count('OpsVirusCheck');
6375      defined($av_ret) or die "All virus scanners failed!";
6376      @$cache_entry{'Vt','VO','VN','VD'} =
6377        ($now, $av_output, \@virusname, \@detecting_scanners);
6378      $virus_presence_checked = 1;
6379      if (defined $snmp_db && @virusname) {
6380        $which_section = "read_counters";
6381        $virus_dejavu = 1  if !grep {$_==0}  # none with counter zero or undef
6382                @{$snmp_db->read_counters(map {"virus.byname.$_"} @virusname)};
6383        section_time($which_section);
6384      }
6385    }
6386
6387    my($sender_contact,$sender_source);
6388    if (!@virusname) { $sender_contact = $sender_source = $msginfo->sender }
6389    else {
6390      ($sender_contact,$sender_source) = best_try_originator(
6391                        $msginfo->sender, $msginfo->mime_entity, \@virusname);
6392      section_time('best_try_originator');
6393    }
6394    $msginfo->sender_contact($sender_contact);  # save it
6395    $msginfo->sender_source($sender_source);    # save it
6396
6397    # consider doing spam scanning
6398    if (!$extra_code_antispam) {
6399      do_log(5, "no anti-spam code loaded, skipping spam_scan");
6400    } elsif (@virusname || @banned_filename) {
6401      do_log(5, "infected or banned contents, skipping spam_scan");
6402    } elsif (!grep {!lookup(0,$_,@{ca('bypass_spam_checks_maps')})} @recips) {
6403      do_log(5, "bypassing of spam checks requested");
6404    } else {
6405      $which_section = "spam-wb-list";
6406      my($any_wbl, $all_wbl) = Amavis::SpamControl::white_black_list(
6407                     $conn, $msginfo, $sql_wblist, $user_id_sql, $ldap_policy);
6408      section_time($which_section);
6409      if ($all_wbl) {
6410        do_log(5, "sender white/blacklisted, skipping spam_scan");
6411      } elsif ($spam_presence_checked) {
6412        do_log(5, "spam_presence cached, skipping spam_scan");
6413      } else {
6414        $which_section = "spam_scan";
6415        ($spam_level, $spam_status, $spam_report) =
6416          Amavis::SpamControl::spam_scan($conn, $msginfo);
6417        prolong_timer($which_section);
6418        snmp_count('OpsSpamCheck');
6419        @$cache_entry{'St','SL','SS','SR'} =
6420          ($now, $spam_level, $spam_status, $spam_report);
6421        $spam_presence_checked = 1;
6422      }
6423    }
6424
6425    # store to cache
6426    $cache_entry->{'atime'} = $now_utc_iso8601;   # update accessed timestamp
6427    $body_digest_cache->set($body_digest,$cache_entry,
6428                            $now_utc_iso8601,$expires_utc_iso8601)
6429      if $body_digest_cache && defined $body_digest;
6430    $cache_entry = undef;  # discard the object, it is no longer needed
6431    section_time('update_cache');
6432
6433    snmp_count("virus.byname.$_")  for @virusname;
6434
6435    # SECOND: now that we know what we got, decide what to do with it
6436
6437    my($considered_spam_by_some_recips,$considered_oversize_by_some_recips);
6438
6439    if (@virusname || @banned_filename) {  # virus or banned filename found
6440      # bad_headers do not enter this section, although code is ready for them;
6441      # we'll handle bad headers later, if mail turns out not to be spam
6442      $which_section = "deal_with_virus_or_banned";
6443      my($final_destiny) = @virusname       ? c('final_virus_destiny')
6444                         : @banned_filename ? c('final_banned_destiny')
6445                         : @bad_headers     ? c('final_bad_header_destiny')
6446                         : D_PASS;
6447      for my $r (@{$msginfo->per_recip_data}) {
6448        next  if $r->recip_done;           # already dealt with
6449        if ($final_destiny == D_PASS) {
6450          # recipient wants this message, malicious or not
6451        } elsif ((!@virusname ||           # not a virus or we want it
6452                  lookup(0,$r->recip_addr, @{ca('virus_lovers_maps')})) &&
6453                 (!@banned_filename ||     # not banned or we want it
6454                  lookup(0,$r->recip_addr, @{ca('banned_files_lovers_maps')})) &&
6455                 (!@bad_headers ||         # not bad header or we want it
6456                  lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')})) )
6457        {
6458          # clean, or recipient wants it
6459        } else {  # change mail destiny for those not wanting malware
6460          $r->recip_destiny($final_destiny);
6461          my($reason);
6462          if (@virusname)
6463            { $reason = "VIRUS: "  . join(", ", @virusname) }
6464          elsif (@banned_filename)
6465            { $reason = "BANNED: " . join(", ", @banned_filename) }
6466          elsif (@bad_headers)
6467            { $reason = "BAD_HEADER: " . join(", ", @bad_headers) }
6468          $reason = substr($reason,0,100)."..."  if length($reason) > 100+3;
6469          $r->recip_smtp_response( ($final_destiny == D_DISCARD
6470                                    ? "250 2.7.1 Ok, discarded"
6471                                    : "550 5.7.1 Message content rejected") .
6472                                   ", id=$am_id - $reason");
6473          $r->recip_done(1);
6474        }
6475      }
6476      $which_section = "virus_or_banned quar+notif";
6477      ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
6478      # send notifications, quarantine it
6479      do_virus($conn, $msginfo, $virus_dejavu);
6480
6481    } else {                      # perhaps some recips consider it spam?
6482        # spaminess is an individual matter, we must compare spam level
6483        # with each recipient setting, there is no global criterium
6484        # that the mail is spam
6485      $which_section = "deal_with_spam";
6486      my($final_destiny) = c('final_spam_destiny');
6487      for my $r (@{$msginfo->per_recip_data}) {
6488        next  if $r->recip_done;  # already dealt with
6489        my($kill_level);
6490        $kill_level = lookup(0,$r->recip_addr, @{ca('spam_kill_level_maps')});
6491        my($boost) = $r->recip_score_boost;
6492        my($should_be_killed) =
6493          !$r->recip_whitelisted_sender &&
6494          ($r->recip_blacklisted_sender ||
6495           (defined $spam_level && defined $kill_level ?
6496                     $spam_level+$boost >= $kill_level : 0) );
6497        next  unless $should_be_killed;
6498        # message is at or above kill level, or sender is blacklisted
6499        $considered_spam_by_some_recips = 1;
6500        if ($final_destiny == D_PASS ||
6501            lookup(0,$r->recip_addr, @{ca('spam_lovers_maps')})) {
6502          # do nothing, recipient wants this message, even if spam
6503        } else {  # change mail destiny for those not wanting spam
6504          ll(3) && do_log(3,sprintf(
6505            "SPAM-KILL, %s -> %s, hits=%s, kill=%s%s",
6506            qquote_rfc2821_local($msginfo->sender, $r->recip_addr),
6507            (!defined $spam_level ? 'x'
6508             : !defined $boost ? $spam_level
6509             : $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost),
6510            !defined $kill_level ? 'x' : 0+sprintf("%.3f",$kill_level),
6511            $r->recip_blacklisted_sender ? ', BLACKLISTED' : ''));
6512          $r->recip_destiny($final_destiny);
6513          my($reason) =
6514            $r->recip_blacklisted_sender ? 'sender blacklisted' : 'UBE';
6515          $r->recip_smtp_response(($final_destiny == D_DISCARD
6516                              ? "250 2.7.1 Ok, discarded, $reason"
6517                              : "550 5.7.1 Message content rejected, $reason"
6518                            ) . ", id=$am_id");
6519          $r->recip_done(1);
6520        }
6521      }
6522      if ($considered_spam_by_some_recips) {
6523        $which_section = "spam quar+notif";
6524        ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
6525        do_spam($conn, $msginfo);
6526        section_time('post-do_spam');
6527      }
6528    }
6529
6530    if (@bad_headers) {  # invalid mail headers
6531      $which_section = "deal_with_bad_headers";
6532      ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
6533      my($is_bulk) = $msginfo->mime_entity->head->get('precedence', 0);
6534      chomp($is_bulk);
6535      do_log(1,sprintf("BAD HEADER from %s<%s>: %s",
6536                       $is_bulk eq '' ? '' : "($is_bulk) ", $msginfo->sender,
6537                       $bad_headers[0]));
6538      $is_bulk = $is_bulk=~/^(bulk|list|junk)/i ? $1 : undef;
6539      if (defined $is_bulk || $msginfo->sender eq '') {
6540        # have mercy on mailing lists and DSN
6541      } else {
6542        my($any_badh); my($final_destiny) = c('final_bad_header_destiny');
6543        for my $r (@{$msginfo->per_recip_data}) {
6544          next  if $r->recip_done;  # already dealt with
6545          if ($final_destiny == D_PASS ||
6546              lookup(0,$r->recip_addr, @{ca('bad_header_lovers_maps')}))
6547          {
6548            # recipient wants this message, broken or not
6549          } else {  # change mail destiny for those not wanting it
6550            $r->recip_destiny($final_destiny);
6551            my($reason) = (split(/\n/, $bad_headers[0]))[0];
6552            $r->recip_smtp_response(($final_destiny == D_DISCARD
6553                        ? "250 2.6.0 Ok, message with invalid header discarded"
6554                        : "554 5.6.0 Message with invalid header rejected"
6555                      ) . ", id=$am_id - $reason");
6556            $r->recip_done(1);
6557            $any_badh++;
6558          }
6559        }
6560        if ($any_badh) {  # we use the same code as for viruses or banned
6561                          # but only if it wasn't already handled as spam
6562          do_virus($conn, $msginfo, 0);  # send notifications, quarantine it
6563        }
6564      }
6565      section_time($which_section);
6566    }
6567
6568    my($mslm) = ca('message_size_limit_maps');
6569    if (@$mslm) {
6570      $which_section = "deal_with_mail_size";
6571      my($mail_size) = $msginfo->msg_size;
6572      for my $r (@{$msginfo->per_recip_data}) {
6573        next  if $r->recip_done;  # already dealt with
6574        my($size_limit) = lookup(0,$r->recip_addr, @$mslm);
6575        $size_limit = 65536
6576          if $size_limit && $size_limit < 65536;  # rfc2821
6577        if ($size_limit && $mail_size > $size_limit) {
6578          do_log(1,sprintf("OVERSIZE from <%s> to <%s>: size %s B, limit %s B",
6579                    $msginfo->sender, $r->recip_addr, $mail_size, $size_limit))
6580            if !$considered_oversize_by_some_recips;
6581          $considered_oversize_by_some_recips = 1;
6582          $r->recip_destiny(D_BOUNCE);
6583          $r->recip_smtp_response("552 5.3.4 Message size ($mail_size B) ".
6584            "exceeds fixed maximium message size of $size_limit B, id=$am_id");
6585          $r->recip_done(1);
6586        }
6587      }
6588      section_time($which_section);
6589    }
6590
6591    $which_section = "snooping_quarantine";
6592#   do_quarantine($conn, $msginfo, Amavis::Out::EditHeader->new,
6593#                 ['sender-quarantine'], 'local:user-%i-%n'
6594#     ) if lookup(0,$msginfo->sender, ['user1@domain','user2@domain']);
6595#   do_quarantine($conn, $msginfo, Amavis::Out::EditHeader->new,
6596#     ['incoming-quarantine'], 'local:all-%i-%n');
6597#   do_quarantine($conn, $msginfo, Amavis::Out::EditHeader->new,
6598#     ['archive@localhost'],   'local:all-%i-%n');
6599#   section_time($which_section);
6600
6601    $which_section = "checking_sender_ip";
6602    my(@recips) = @{$msginfo->recips};
6603    if ($considered_spam_by_some_recips && @recips==1 &&
6604        $recips[0] eq $msginfo->sender &&
6605        lookup(0,$msginfo->sender, @{ca('local_domains_maps')}))
6606    {
6607      my($cl_ip) = $msginfo->client_addr;
6608      if ($cl_ip eq '') {
6609        ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
6610        $cl_ip = fish_out_ip_from_received(
6611                               $msginfo->mime_entity->head->get('received',0));
6612      }
6613      if ($cl_ip ne '') {
6614        my($is_our_ip) =
6615          eval { lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) };
6616        if ($@ ne '' && !$is_our_ip) {
6617          do_log(0, "FAKE SENDER, SPAM: $cl_ip, " . $msginfo->sender);
6618          $msginfo->sender_contact(undef);  # believed to be faked
6619        }
6620      }
6621    }
6622
6623    if ($hold ne '') { do_log(-1, "NOTICE: HOLD reason: $hold") }
6624
6625    # THIRD: now that we know what to do with it, do it!
6626
6627    my($which_content_counter) =
6628        @virusname       ? 'ContentVirusMsgs'
6629      : @banned_filename ? 'ContentBannedMsgs'
6630      : $considered_spam_by_some_recips ? 'ContentSpamMsgs'
6631      : @bad_headers     ? 'ContentBadHdrMsgs'
6632      : $considered_oversize_by_some_recips ? 'ContentOversizeMsgs'
6633      : 'ContentCleanMsgs';
6634    snmp_count($which_content_counter);
6635
6636    my($hdr_edits) = $msginfo->header_edits;
6637    if (!$hdr_edits) {
6638      $hdr_edits = Amavis::Out::EditHeader->new;
6639      $msginfo->header_edits($hdr_edits);
6640    }
6641    if ($msginfo->delivery_method eq '') {   # AM.PDP or AM.CL (milter)
6642      $which_section = "AM.PDP headers";
6643      ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
6644      $hdr_edits = add_forwarding_header_edits_common(
6645        $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
6646        $virus_presence_checked, $spam_presence_checked, undef);
6647      my($done_all);
6648      my($recip_cl);  # ref to a list of similar recip objects
6649      ($hdr_edits, $recip_cl, $done_all) =
6650        add_forwarding_header_edits_per_recip(
6651          $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
6652          $virus_presence_checked, $spam_presence_checked, undef, undef);
6653      $msginfo->header_edits($hdr_edits);  # store edits (redundant?)
6654      if (@$recip_cl && !$done_all) {
6655        do_log(-1, "AM.PDP: CLIENTS REQUIRE DIFFERENT HEADERS");
6656      };
6657    } elsif (grep { !$_->recip_done } @{$msginfo->per_recip_data}) {  # forward
6658      # To be delivered explicitly - only to those recipients not yet marked
6659      # as 'done' by the above content filtering sections.
6660      $which_section = "forwarding";
6661      ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
6662      # a quick-fix solution to defang dangerous contents
6663      my($mail_defanged);  # nonempty indicates mail body is replaced
6664      my($explanation);
6665      if ($hold ne '') { $explanation =
6666        "WARNING: possible mail bomb, NOT CHECKED FOR VIRUSES:\n  $hold";
6667      } elsif (@virusname) { $explanation =
6668        'WARNING: contains virus '.join(' ',@virusname)  if c('defang_virus');
6669      } elsif (@banned_filename) { $explanation =
6670        "WARNING: contains banned part"  if c('defang_banned');
6671      } elsif ($any_undecipherable) { $explanation =
6672        "WARNING: contains undecipherable part"  if c('defang_undecipherable');
6673      } elsif ($considered_spam_by_some_recips) { $explanation =
6674        $spam_report  if c('defang_spam');
6675      } elsif (@bad_headers) { $explanation =
6676        'WARNING: bad headers '.join(' ',@bad_headers) if c('defang_bad_header');
6677      } else { $explanation = '(clean)'  if c('defang_all') }
6678      if (defined $explanation) {  # malware
6679        $explanation .= "\n"  if $explanation !~ /\n\z/;
6680        my($s) = $explanation; $s=~s/[ \t\n]+\z//;
6681        if (length($s) > 100) { $s = substr($s,0,100-3) . "..." }
6682        do_log(1, "DEFANGING MAIL: $s");
6683        my($d) = defanged_mime_entity($conn,$msginfo,$explanation);
6684        $msginfo->mail_text($d);  # substitute mail with rewritten version
6685        $msginfo->mail_text_fn(undef);  # remove filename information
6686        $mail_defanged = 'Original mail wrapped as attachment (defanged)';
6687        section_time('defang');
6688      }
6689      $hdr_edits = add_forwarding_header_edits_common(
6690        $conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
6691        $virus_presence_checked, $spam_presence_checked, $mail_defanged);
6692      for (;;) {  # do the delivery
6693        my($r_hdr_edits) = Amavis::Out::EditHeader->new;  # per-recip edits set
6694        $r_hdr_edits->inherit_header_edits($hdr_edits);
6695        my($done_all);
6696        my($recip_cl);  # ref to a list of similar recip objects
6697        ($r_hdr_edits, $recip_cl, $done_all) =
6698          add_forwarding_header_edits_per_recip(
6699            $conn, $msginfo, $r_hdr_edits, $hold, $any_undecipherable,
6700            $virus_presence_checked, $spam_presence_checked,
6701            $mail_defanged, undef);
6702        last  if !@$recip_cl;
6703        $msginfo->header_edits($r_hdr_edits);  # store edits
6704        mail_dispatch($conn, $msginfo, 0,
6705                      sub { my($r) = @_; grep { $_ eq $r } @$recip_cl });
6706        snmp_count('OutForwMsgs');
6707        snmp_count('OutForwHoldMsgs')  if $hold ne '';
6708        last  if $done_all;
6709      }
6710    }
6711    prolong_timer($which_section);
6712
6713    $which_section = "delivery-notification";
6714    my($dsn_needed);
6715    ($smtp_resp, $exit_code, $dsn_needed) =
6716      one_response_for_all($msginfo, $dsn_per_recip_capable, $am_id);
6717    my($warnsender_with_pass) =
6718      $smtp_resp =~ /^2/ && !$dsn_needed &&
6719      (@virusname       && c('warnvirussender')  ||
6720       @banned_filename && c('warnbannedsender') ||
6721       $considered_spam_by_some_recips && c('warnspamsender') ||
6722       @bad_headers     && c('warnbadhsender')  );
6723    ll(4) && do_log(4,sprintf(
6724      "warnsender_with_pass=%s (%s,%s,%s,%s), dsn_needed=%s, exit=%s, %s",
6725      $warnsender_with_pass,
6726      c('warnvirussender'),c('warnbannedsender'),
6727      c('warnbadhsender'),c('warnspamsender'),
6728      $dsn_needed,$exit_code,$smtp_resp));
6729    if ($dsn_needed || $warnsender_with_pass) {
6730      ensure_mime_entity($msginfo, $fh, $tempdir, \@virusname, $parts_root);
6731      my($what_bad_content) = join(' & ',
6732        !@virusname                      ? () : 'VIRUS',
6733        !@banned_filename                ? () : 'BANNED',
6734        !$considered_spam_by_some_recips ? () : 'SPAM',
6735        !@bad_headers                    ? () : 'BAD HEADER',
6736        !$considered_oversize_by_some_recips ? () : 'OVERSIZE');
6737      my($notification); my($dsn_cutoff_level);
6738      if ($msginfo->sender eq '') {  # don't respond to null reverse path
6739        my($msg) = "DSN contains $what_bad_content; bounce is not bouncible";
6740        if (!$dsn_needed) { do_log(4, $msg) }
6741        else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
6742        $msginfo->dsn_sent(2);       # pretend the message was bounced
6743      } elsif ($msginfo->sender_contact eq '') {
6744        my($msg) = sprintf("Not sending DSN to believed-to-be-faked "
6745                           . "sender <%s>, mail containing %s",
6746                           $msginfo->sender, $what_bad_content);
6747        if (!$dsn_needed) { do_log(4, $msg) }
6748        else { do_log(2, "NOTICE: $msg intentionally dropped") }
6749        $msginfo->dsn_sent(2);       # pretend the message was bounced
6750      } elsif ($banned_dsn_suppress) {
6751        my($msg) = "Not sending DSN, as suggested by banned rule";
6752        if (!$dsn_needed) { do_log(4, $msg) }
6753        else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
6754        $msginfo->dsn_sent(2);  # pretend the message was bounced
6755      } elsif (defined $spam_level &&
6756               !grep { $spam_level + $_->recip_score_boost <
6757                       lookup(0,$_->recip_addr,
6758                              @{ca('spam_dsn_cutoff_level_maps')}) }
6759                     @{$msginfo->per_recip_data} ) {
6760        my($msg) = "Not sending DSN, spam level exceeds DSN cutoff level";
6761        if (!$dsn_needed) { do_log(4, $msg) }
6762        else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
6763        $msginfo->dsn_sent(2);  # pretend the message was bounced
6764      } elsif ((@virusname || @banned_filename ||
6765                $considered_spam_by_some_recips || @bad_headers ||
6766                $considered_oversize_by_some_recips) &&
6767          $msginfo->mime_entity->head->get('precedence',0)
6768                                                      =~ /^(bulk|list|junk)/i )
6769      {
6770        my($msg) = sprintf("Not sending DSN in response to bulk mail "
6771                           . "from <%s> containing %s",
6772                           $msginfo->sender, $what_bad_content);
6773        if (!$dsn_needed) { do_log(4, $msg) }
6774        else { do_log(1, "NOTICE: $msg, mail intentionally dropped") }
6775        $msginfo->dsn_sent(2);       # pretend the message was bounced
6776      } else {  # prepare a notification
6777        my($which_dsn_counter,$dsnmsgref);
6778        ### TODO: better selection of DSN reason is needed!
6779        for my $r (@{$msginfo->per_recip_data}) {
6780          next  if !$r->recip_done;
6781          local($_) = $r->recip_smtp_response;
6782          ($which_dsn_counter,$dsnmsgref) =
6783              /^5.*\bVIRUS\b/ ?
6784                ('OutDsnVirusMsgs', cr('notify_virus_sender_templ'))
6785            : /^5.*\bBANNED\b/ ?
6786                ('OutDsnBannedMsgs',cr('notify_virus_sender_templ'))
6787            : /^5.*\b(?:UBE|blacklisted)\b/ ?
6788                ('OutDsnSpamMsgs',  cr('notify_spam_sender_templ'))
6789            : /^5.*\bheader\b/ ?
6790                ('OutDsnBadHdrMsgs',cr('notify_sender_templ'))
6791            :   ('OutDsnOtherMsgs', cr('notify_sender_templ'));
6792        }
6793        # generate delivery status notification according to rfc3462
6794        # and rfc3464 if needed
6795        $notification = delivery_status_notification($conn, $msginfo,
6796          $warnsender_with_pass, \%builtins, $dsnmsgref)  if $dsnmsgref;
6797        snmp_count($which_dsn_counter)  if defined $notification;
6798      }
6799      if (defined $notification) {  # dsn needed, send delivery notification
6800        mail_dispatch($conn, $notification, 1);
6801        snmp_count('OutDsnMsgs');
6802        my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
6803          one_response_for_all($notification, 0, $am_id);  # check status
6804        if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) {      # dsn successful?
6805          $msginfo->dsn_sent(1);  # mark the message as bounced
6806        } elsif ($n_smtp_resp =~ /^4/) {
6807          snmp_count('OutDsnTempFails');
6808          die sprintf("temporarily unable to send DSN to <%s>: %s",
6809                      $msginfo->sender_contact, $n_smtp_resp);
6810        } else {
6811          snmp_count('OutDsnRejects');
6812          do_log(-1,sprintf("NOTICE: UNABLE TO SEND DSN to <%s>: %s",
6813                            $msginfo->sender, $n_smtp_resp));
6814#         # if dsn can not be sent, try to send it to postmaster
6815#         $notification->recips(['postmaster']);
6816#         # attempt double bounce
6817#         mail_dispatch($conn, $notification, 1);
6818        }
6819      # $notification->purge;
6820      }
6821    }
6822    prolong_timer($which_section);
6823
6824    # generate customized log report at log level 0 - this is usually the
6825    # only log entry interesting to administrators during normal operation
6826    $which_section = 'main_log_entry';
6827    my(%mybuiltins) = %builtins;  # make a local copy
6828    { # do a per-mail log entry
6829      my($s) = $spam_status; $s =~ s/^tests=//; my(@s) = split(/,/,$s);
6830      if (@s > 10) { $#s = 9; push(@s,"...") }
6831      $mybuiltins{'T'} = \@s;     # macro %T has overloaded semantics, ugly
6832      my($strr) = expand(cr('log_templ'), \%mybuiltins);
6833      for my $logline (split(/[ \t]*\n/, $$strr)) {
6834        do_log(0, $logline)  if $logline ne '';
6835      }
6836    }
6837    if (c('log_recip_templ') ne '') {  # do per-recipient log entries
6838      # redefine macros with a by-recipient semantics
6839      for my $r (@{$msginfo->per_recip_data}) {
6840        # recipient counter in macro %. may indicate to the template
6841        # that a per-recipient expansion semantics is expected
6842        $mybuiltins{'.'}++;
6843        my($recip) = $r->recip_addr;
6844        my($smtp_resp)   = $r->recip_smtp_response;
6845        my($qrecip_addr) = scalar(qquote_rfc2821_local($recip));
6846        $mybuiltins{'D'} = $mybuiltins{'O'} = $mybuiltins{'N'} = undef;
6847        if ($r->recip_destiny==D_PASS && ($smtp_resp=~/^2/ || !$r->recip_done)){
6848          $mybuiltins{'D'} = $qrecip_addr;
6849        } else {
6850          $mybuiltins{'O'} = $qrecip_addr;
6851          my($remote_mta)  = $r->recip_remote_mta;
6852          $mybuiltins{'N'} = sprintf("%s:%s\n   %s", $qrecip_addr,
6853                  ($remote_mta eq '' ? '' : " $remote_mta said:"), $smtp_resp);
6854        }
6855        my($blacklisted) = $r->recip_blacklisted_sender;
6856        my($whitelisted) = $r->recip_whitelisted_sender;
6857        my($boost)       = $r->recip_score_boost;
6858        my($is_local,$tag_level,$tag2_level,$kill_level);
6859        $is_local   = lookup(0,$recip, @{ca('local_domains_maps')});
6860        $tag_level  = lookup(0,$recip, @{ca('spam_tag_level_maps')});
6861        $tag2_level = lookup(0,$recip, @{ca('spam_tag2_level_maps')});
6862        $kill_level = lookup(0,$recip, @{ca('spam_kill_level_maps')});
6863        my($do_tag) =
6864            $blacklisted || !defined $tag_level ||
6865            (defined $spam_level ?  $spam_level+$boost >= $tag_level
6866                                 : $whitelisted ? (-10 >= $tag_level) : 0);
6867        my($do_tag2) = !$whitelisted &&
6868          ( $blacklisted ||
6869            (defined $spam_level && defined $tag2_level ?
6870                                    $spam_level+$boost >= $tag2_level : 0) );
6871        my($do_kill) = !$whitelisted &&
6872          ( $blacklisted ||
6873            (defined $spam_level && defined $kill_level ?
6874                                    $spam_level+$boost >= $kill_level : 0) );
6875        for ($do_tag,$do_tag2,$do_kill) { $_ = $_ ? 'Y' : '0' }  # normalize
6876        for ($is_local)                 { $_ = $_ ? 'L' : '0' }  # normalize
6877        for ($tag_level,$tag2_level,$kill_level) { $_ = 'x'  if !defined($_) }
6878        $mybuiltins{'R'} = $recip;
6879        $mybuiltins{'c'} = !defined $spam_level ? '-'
6880                                        : 0+sprintf("%.3f",$spam_level+$boost);
6881        @mybuiltins{('0','1','2','k')} = ($is_local,$do_tag,$do_tag2,$do_kill);
6882        # macros %3, %4, %5 are experimental, until a better solution is found
6883        @mybuiltins{('3','4','5')}     = ($tag_level,$tag2_level,$kill_level);
6884        my($strr) = expand(cr('log_recip_templ'), \%mybuiltins);
6885        for my $logline (split(/[ \t]*\n/, $$strr)) {
6886          do_log(0, $logline)  if $logline ne '';
6887        }
6888      }
6889    }
6890    section_time($which_section);
6891
6892    $which_section = 'finishing';
6893    $snmp_db->update_counters  if defined $snmp_db;
6894    section_time('update_snmp');
6895
6896  };  # end eval
6897  if ($@ ne '') {
6898    chomp($@);
6899    $preserve_evidence = 0;
6900    my($msg) = "$which_section FAILED: $@";
6901    do_log(-2, "TROUBLE in check_mail: $msg");
6902    $smtp_resp = "451 4.5.0 Error in processing, id=$am_id, $msg";
6903    $exit_code = EX_TEMPFAIL;
6904    for my $r (@{$msginfo->per_recip_data}) {
6905      next  if $r->recip_done;
6906      $r->recip_smtp_response($smtp_resp); $r->recip_done(1);
6907    }
6908  }
6909# if ($hold ne '') {
6910#   do_log(-1, "NOTICE: Evidence is to be preserved: $hold");
6911#   $preserve_evidence = 1;
6912# }
6913  if (!$preserve_evidence && debug_oneshot()) {
6914    do_log(0, "DEBUG_ONESHOT CAUSES EVIDENCE TO BE PRESERVED");
6915    $preserve_evidence = 1;
6916  }
6917
6918  my($which_counter) = 'InUnknown';
6919  if    ($smtp_resp =~ /^4/) { $which_counter = 'InTempFails' }
6920  elsif ($smtp_resp =~ /^5/) { $which_counter = 'InRejects' }
6921  elsif ($smtp_resp =~ /^2/) {
6922    my($dsn_sent) = $msginfo->dsn_sent;
6923    if (!$dsn_sent) { $which_counter = $msginfo->delivery_method ne ''
6924                                       ? 'InAccepts' : 'InContinues' }
6925    elsif ($dsn_sent==1) { $which_counter = 'InBounces' }
6926    elsif ($dsn_sent==2) { $which_counter = 'InDiscards' }
6927  }
6928  snmp_count($which_counter);
6929  $snmp_db->register_proc('.')  if defined $snmp_db;  # content checking done
6930
6931  $MSGINFO = undef;  # release global reference to msginfo object
6932  ($smtp_resp, $exit_code, $preserve_evidence);
6933}
6934
6935# Ensure we have $msginfo->$entity defined when we expect we'll need it,
6936# e.g. to construct notifications. While at it, also get us some additional
6937# information on sender from the header.
6938#
6939sub ensure_mime_entity($$$$$) {
6940  my($msginfo, $fh, $tempdir, $virusname_list, $parts_root) = @_;
6941  if (!defined($msginfo->mime_entity)) {
6942    # header may not have been parsed yet, e.g. if the result was cached
6943    my($ent,$mime_err) = mime_decode($fh, $tempdir, $parts_root);
6944    $msginfo->mime_entity($ent);
6945    prolong_timer("ensure_mime_entity");
6946  }
6947}
6948
6949sub add_forwarding_header_edits_common($$$$$$) {
6950  my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
6951     $virus_presence_checked, $spam_presence_checked, $mail_defanged) = @_;
6952
6953  $hdr_edits->prepend_header('Received',
6954    received_line($conn,$msginfo,am_id(),1), 1)
6955    if $insert_received_line && $msginfo->delivery_method ne '';
6956  # discard existing X-Amavis-Hold header field, only allow our own
6957  $hdr_edits->delete_header('X-Amavis-Hold');
6958  if ($hold ne '') {
6959    $hdr_edits->append_header('X-Amavis-Hold', $hold);
6960    do_log(-1, "Inserting header field: X-Amavis-Hold: $hold");
6961  }
6962  if ($mail_defanged ne '') {
6963    # prepend Resent-* header fields, they must precede
6964    # corresponding Received header field (pushed in reverse order)
6965    $hdr_edits->prepend_header('Resent-Message-ID',
6966                               sprintf('<RE%s@%s>',am_id(),$myhostname) );
6967    $hdr_edits->prepend_header('Resent-Date',
6968                               rfc2822_timestamp($msginfo->rx_time));
6969    $hdr_edits->prepend_header('Resent-From', c('hdrfrom_notify_recip'));
6970    # append X-Amavis-Modified
6971    my($msg) = "$mail_defanged by $myhostname";
6972    $hdr_edits->append_header('X-Amavis-Modified', $msg);
6973    do_log(1, "Inserting header field: X-Amavis-Modified: $msg");
6974  }
6975  if ($extra_code_antivirus) {
6976    $hdr_edits->delete_header('X-Amavis-Alert');
6977    $hdr_edits->delete_header(c('X_HEADER_TAG'))
6978      if c('remove_existing_x_scanned_headers') &&
6979         (c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/);
6980  }
6981  if ($extra_code_antispam) {
6982    if (c('remove_existing_spam_headers')) {
6983      my(@which_headers) = qw(
6984          X-Spam-Status X-Spam-Level X-Spam-Flag X-Spam-Score
6985          X-Spam-Report X-Spam-Checker-Version X-Spam-Tests);
6986      push(@which_headers, qw(X-DSPAM-Result X-DSPAM-Signature X-DSPAM-User
6987                              X-DSPAM-Probability) )  if defined $dspam;
6988      for my $h (@which_headers) { $hdr_edits->delete_header($h) }
6989    }
6990  # $hdr_edits->append_header('X-Spam-Checker-Version',
6991  # sprintf("SpamAssassin %s (%s) on %s", Mail::SpamAssassin::Version(),
6992  #         $Mail::SpamAssassin::SUB_VERSION, $myhostname));
6993  }
6994  $hdr_edits;
6995}
6996
6997# Prepare header edits for the first not-yet-done recipient.
6998# Inspect remaining recipients, returning the list of recipient objects
6999# that are receiving the same set of header edits (so the message may be
7000# delivered to them in one SMTP transaction).
7001#
7002sub add_forwarding_header_edits_per_recip($$$$$$$$$) {
7003  my($conn, $msginfo, $hdr_edits, $hold, $any_undecipherable,
7004     $virus_presence_checked, $spam_presence_checked,
7005     $mail_defanged, $filter) = @_;
7006  my(@recip_cluster);
7007  my(@per_recip_data) = grep { !$_->recip_done && (!$filter || &$filter($_)) }
7008                             @{$msginfo->per_recip_data};
7009  my($per_recip_data_len) = scalar(@per_recip_data);
7010  my($first) = 1; my($cluster_key); my($cluster_full_spam_status);
7011  for my $r (@per_recip_data) {
7012    my($recip) = $r->recip_addr;
7013    my($is_local,$blacklisted,$whitelisted,$boost,$tag_level,$tag2_level,
7014       $do_tag_virus_checked,$do_tag_virus,$do_tag_banned,$do_tag_badh,
7015       $do_tag,$do_tag2,$do_subj,$do_subj_u,$subject_tag,$subject_tag2);
7016    $is_local = lookup(0,$recip, @{ca('local_domains_maps')});
7017    $do_tag_badh  = @bad_headers &&
7018                    !lookup(0,$recip,@{ca('bypass_header_checks_maps')});
7019    $do_tag_banned= @banned_filename &&
7020                    !lookup(0,$recip,@{ca('bypass_banned_checks_maps')});
7021    $do_tag_virus = @virusname &&
7022                    !lookup(0,$recip,@{ca('bypass_virus_checks_maps')});
7023    $do_tag_virus_checked =
7024      $virus_presence_checked &&
7025      (c('X_HEADER_LINE') ne '' && c('X_HEADER_TAG') =~ /^[!-9;-\176]+\z/) &&
7026      !lookup(0,$recip,@{ca('bypass_virus_checks_maps')});
7027    if ($extra_code_antispam) {
7028      my($bypassed);
7029      $blacklisted = $r->recip_blacklisted_sender;
7030      $whitelisted = $r->recip_whitelisted_sender;
7031      $boost       = $r->recip_score_boost;
7032      $bypassed    = lookup(0,$recip, @{ca('bypass_spam_checks_maps')});
7033      $tag_level   = lookup(0,$recip, @{ca('spam_tag_level_maps')});
7034      $tag2_level  = lookup(0,$recip, @{ca('spam_tag2_level_maps')});
7035      # spam-related headers should _not_ be inserted for:
7036      #  - nonlocal recipients (outgoing mail), as a matter of courtesy
7037      #    to our users;
7038      #  - recipients matching bypass_spam_checks: even though spam checking
7039      #    may have been done for other reasons, these recipients do not
7040      #    expect such headers, so let's pretend the check has not been done
7041      #    and not insert spam-related headers for them
7042      $do_tag = $is_local && !$bypassed &&
7043        ( $blacklisted || !defined $tag_level ||
7044          (defined $spam_level ? $spam_level+$boost  >= $tag_level
7045                               : $whitelisted ? (-10 >= $tag_level) : 0) );
7046      $do_tag2 = $is_local && !$bypassed && !$whitelisted &&
7047        ( $blacklisted ||
7048          (defined $spam_level && defined $tag2_level ?
7049            $spam_level+$boost >= $tag2_level : 0) );
7050      $subject_tag2 = !$do_tag2 ? undef
7051                           : lookup(0,$recip, @{ca('spam_subject_tag2_maps')});
7052      $subject_tag  = !($do_tag||$do_tag2) ? undef
7053                           : lookup(0,$recip, @{ca('spam_subject_tag_maps')});
7054      $do_subj = ($subject_tag2 ne '' || $subject_tag ne '') &&
7055                 lookup(0,$recip, @{ca('spam_modifies_subj_maps')});
7056    }
7057    if ($hold ne '' || $any_undecipherable) { # adding *UNCHECKED* subject tag?
7058       $do_subj_u = $is_local && c('undecipherable_subject_tag') ne '' &&
7059                    !(@virusname && !lookup(0,$recip,
7060                                          @{ca('bypass_virus_checks_maps')}) );
7061    }
7062    # normalize
7063    for ($do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh,
7064         $do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local) { $_ = $_?1:0 }
7065    my($spam_level_bar, $full_spam_status);
7066    if ($do_tag || $do_tag2) {
7067      my($slc) = c('sa_spam_level_char');
7068      $spam_level_bar =
7069        $slc x min($blacklisted ? 64 : $spam_level+$boost, 64)  if $slc ne '';
7070      my($s) = $spam_status; $s =~ s/,/,\n /g;  # allow header field wrapping
7071      $full_spam_status = sprintf("%s,\n hits=%s\n%s%s %s%s",
7072        $do_tag2 ? 'Yes' : 'No',
7073        !defined $spam_level  ? 'x' : 0+sprintf("%.3f",$spam_level+$boost),
7074        !defined $tag_level   ? '' : sprintf(" tagged_above=%s\n",$tag_level),
7075        !defined $tag2_level  ? '' : sprintf(" required=%s\n",  $tag2_level),
7076        join('', $blacklisted ? "BLACKLISTED\n " : (),
7077                 $whitelisted ? "WHITELISTED\n " : ()),
7078        $s);
7079    }
7080    my($key) = join("\000",
7081      $do_tag_virus_checked, $do_tag_virus, $do_tag_banned, $do_tag_badh,
7082      $do_tag, $do_tag2, $do_subj, $do_subj_u, $spam_level_bar,
7083      $full_spam_status);
7084    if ($first) {
7085      ll(4) && do_log(4,sprintf(
7086        "headers CLUSTERING: NEW CLUSTER <%s>: ".
7087        "hits=%s, tag=%s, tag2=%s, subj=%s, subj_u=%s, local=%s, bl=%s",
7088        $recip,
7089        (!defined $spam_level ? 'x'
7090         : !defined $boost ? $spam_level
7091         : $boost >= 0 ? $spam_level.'+'.$boost : $spam_level.$boost),
7092        $do_tag, $do_tag2, $do_subj, $do_subj_u, $is_local, $blacklisted));
7093      $cluster_key = $key; $cluster_full_spam_status = $full_spam_status;
7094    } elsif ($key eq $cluster_key) {
7095      do_log(5,"headers CLUSTERING: <$recip> joining cluster");
7096    } else {
7097      do_log(5,"headers CLUSTERING: skipping <$recip> (tag=$do_tag, tag2=$do_tag2)");
7098      next;  # this recipient will be handled in some later pass
7099    }
7100
7101    if ($first) {  # insert headers required for the new cluster
7102      if ($do_tag_virus_checked) {
7103        $hdr_edits->append_header(c('X_HEADER_TAG'), c('X_HEADER_LINE'));
7104      }
7105      if ($do_tag_virus) {
7106        $hdr_edits->append_header('X-Amavis-Alert',
7107          "INFECTED, message contains virus:\n " . join(",\n ",@virusname), 1);
7108      }
7109      if ($do_tag_banned) {
7110        my(@b) = @banned_filename>3 ? @banned_filename[0..2] :@banned_filename;
7111        my($msg) = "BANNED, message contains "
7112          . (@banned_filename==1 ? 'part' : 'parts') . ":\n "
7113          . join(",\n ", @b) . (@banned_filename > @b ? ", ..." : "");
7114        $hdr_edits->append_header('X-Amavis-Alert', $msg, 1);
7115      }
7116      if ($do_tag_badh) {
7117        $hdr_edits->append_header('X-Amavis-Alert',
7118                                  'BAD HEADER '.$bad_headers[0], 1);
7119      }
7120      if ($do_tag) {
7121        $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
7122#       $hdr_edits->append_header('X-Spam-Score',
7123#         !defined $spam_level ? '-' : 0+sprintf("%.3f",$spam_level+$boost) );
7124        $hdr_edits->append_header('X-Spam-Level',
7125                                  $spam_level_bar) if defined $spam_level_bar;
7126      }
7127      if ($do_tag2) {
7128        $hdr_edits->append_header('X-Spam-Flag', 'YES');
7129        $hdr_edits->append_header('X-Spam-Report', $spam_report,1)
7130          if $spam_report ne '' && c('sa_spam_report_header');
7131      }
7132      if ($do_subj || $do_subj_u) {
7133        my($s) = '';
7134        if ($do_subj_u) {
7135          $s = c('undecipherable_subject_tag');
7136          do_log(3,"adding $s, $any_undecipherable, $hold");
7137        }
7138        if ($do_subj) {
7139          $s .= $do_tag2 && $subject_tag2 ne '' ? $subject_tag2 : $subject_tag;
7140        }
7141        my($entity) = $msginfo->mime_entity;
7142        if (defined $entity && defined $entity->head->get('Subject',0)) {
7143          $hdr_edits->edit_header('Subject',
7144                                 sub { $_[1]=~/^([ \t]?)(.*)\z/s; ' '.$s.$2 });
7145        } else {  # no Subject header field present, insert one
7146          $s =~ s/[ \t]+\z//;  # trim
7147          $hdr_edits->append_header('Subject', $s);
7148          if (!defined $entity) {
7149            do_log(-1,"WARN: no MIME entity!? Inserting 'Subject'");
7150          } else {
7151            do_log(0,"INFO: no existing header field 'Subject', inserting it");
7152          }
7153        }
7154      }
7155    }
7156    push(@recip_cluster,$r);  $first = 0;
7157
7158    my($delim) = c('recipient_delimiter');
7159    if ($delim ne '' && $is_local) {
7160      # append address extensions to mailbox names if desired
7161      my($ext_map) = $do_tag_virus  ? ca('addr_extension_virus_maps')
7162                   : $do_tag_banned ? ca('addr_extension_banned_maps')
7163                   : $do_tag2       ? ca('addr_extension_spam_maps')
7164                   : $do_tag_badh   ? ca('addr_extension_bad_header_maps')
7165                   : undef;
7166      my($ext) = !ref($ext_map) ? undef : lookup(0,$recip, @$ext_map);
7167      if ($ext ne '') {
7168        my($localpart,$domain) = split_address($recip);
7169        if (c('replace_existing_extension'))  # strip existing address extensions
7170          { $localpart =~ s/^(.*?)\Q$delim\E.*\z/$1/s }
7171        do_log(5,"adding address extension $delim$ext to $localpart$domain");
7172        $r->recip_addr_modified($localpart.$delim.$ext.$domain);
7173      }
7174    }
7175  }
7176  my($done_all);
7177  if (@recip_cluster == $per_recip_data_len) {
7178    do_log(5,"headers CLUSTERING: " .
7179             "done all $per_recip_data_len recips in one go");
7180    $done_all = 1;
7181  } else {
7182    ll(4) && do_log(4,sprintf(
7183                        "headers CLUSTERING: got %d recips out of %d: %s",
7184                        scalar(@recip_cluster), $per_recip_data_len,
7185              join(", ", map { "<" . $_->recip_addr . ">" } @recip_cluster) ));
7186  }
7187  if (defined($cluster_full_spam_status) && @recip_cluster) {
7188    my($s) = $cluster_full_spam_status; $s =~ s/\n[ \t]/ /g;
7189    ll(2) && do_log(2,sprintf("SPAM-TAG, %s -> %s, %s",
7190                        qquote_rfc2821_local($msginfo->sender),
7191                        join(',', qquote_rfc2821_local(
7192                                 map { $_->recip_addr } @recip_cluster)), $s));
7193  }
7194  ($hdr_edits, \@recip_cluster, $done_all);
7195}
7196
7197sub do_quarantine($$$$$;$) {
7198  my($conn,$msginfo,$hdr_edits,$recips_ref,$quarantine_method,$snmp_id) = @_;
7199  if ($quarantine_method eq '') { do_log(5, "quarantine disabled") }
7200  else {
7201    # fudge to make %b access the body_digest of $msginfo, not of $quar_msg
7202    $quarantine_method =~ s/%b/$msginfo->body_digest/eg;
7203    my($sender) = $msginfo->sender;
7204    my($quar_msg) = Amavis::In::Message->new;
7205    $quar_msg->rx_time($msginfo->rx_time);  # copy the reception time
7206    $quar_msg->delivery_method($quarantine_method);
7207    if ($quarantine_method =~ /^bsmtp:/i) {
7208      $quar_msg->sender($sender);      # original sender & recipients
7209      $quar_msg->recips($msginfo->recips);
7210    } else {
7211      my($mftq) = c('mailfrom_to_quarantine');
7212      $quar_msg->sender(defined $mftq ? $mftq : $sender);
7213      $quar_msg->recips($recips_ref);  # e.g. per-recip quarantine
7214      # NOTE: RFC2821 mentions possible headers X-SMTP-MAIL and X-SMTP-RCPT
7215      # Exim uses: Envelope-To,  Sendmail uses X-Envelope-To;
7216      # No need with bsmtp, which already carries addresses in the envelope
7217      $hdr_edits->prepend_header('X-Envelope-To',
7218        join(",\n ", qquote_rfc2821_local(@{$msginfo->recips})), 1);
7219      $hdr_edits->prepend_header('X-Envelope-From',
7220        qquote_rfc2821_local($sender));
7221    }
7222    do_log(5, "DO_QUARANTINE, sender: " . $quar_msg->sender);
7223    $quar_msg->auth_submitter(qquote_rfc2821_local($quar_msg->sender));
7224    $quar_msg->auth_user(c('amavis_auth_user'));
7225    $quar_msg->auth_pass(c('amavis_auth_pass'));
7226    $quar_msg->header_edits($hdr_edits);
7227    $quar_msg->mail_text($msginfo->mail_text);  # use the same mail contents
7228
7229    snmp_count('QuarMsgs');
7230    mail_dispatch($conn, $quar_msg, 1);
7231    my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
7232      one_response_for_all($quar_msg, 0, am_id());  # check status
7233    if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) {   # ok
7234      snmp_count($snmp_id eq '' ? 'QuarOther' : $snmp_id);
7235    } elsif ($n_smtp_resp =~ /^4/) {
7236      snmp_count('QuarAttemptTempFails');
7237      die "temporarily unable to quarantine: $n_smtp_resp";
7238    } else {  # abort if quarantining not successful
7239      snmp_count('QuarAttemptFails');
7240      die "Can not quarantine: $n_smtp_resp";
7241    }
7242    my(@qa); my(%seen);  # collect unique quarantine mailboxes or addresses
7243    for my $r (@{$quar_msg->per_recip_data}) {
7244      my($mbxname) = $r->recip_mbxname;
7245      push(@qa,$mbxname)  if $mbxname ne '' && !$seen{$mbxname}++;
7246    }
7247    $msginfo->quarantined_to(\@qa);  # remember where it was quarantined to
7248    do_log(5, "DO_QUARANTINE done");
7249  }
7250}
7251
7252# If virus/banned/bad-header found - quarantine it and send notifications
7253sub do_virus($$$) {
7254  my($conn, $msginfo, $virus_dejavu) = @_;
7255  my($q_method, $quarantine_to_maps_ref,
7256     $bypass_checks_maps_ref, $admin_maps_ref) =
7257    @virusname ?
7258      (c('virus_quarantine_method'),
7259       ca('virus_quarantine_to_maps'),
7260       ca('bypass_virus_checks_maps'),
7261       ca('virus_admin_maps') )
7262    : @banned_filename ?
7263      (c('banned_files_quarantine_method'),
7264       ca('banned_quarantine_to_maps'),
7265       ca('bypass_banned_checks_maps'),
7266       ca('banned_admin_maps') )
7267    : @bad_headers ?
7268      (c('bad_header_quarantine_method'),
7269       ca('bad_header_quarantine_to_maps'),
7270       ca('bypass_header_checks_maps'),
7271       ca('bad_header_admin_maps') )
7272    : (undef, undef, undef, undef);
7273  # suggest a name to be used as 'X-Quarantine-Id:' or file name
7274  $VIRUSFILE = $q_method =~ /^(?:local|bsmtp):(.*)\z/si ? $1 : "virus-%i-%n";
7275  $VIRUSFILE =~ s{%(.)}
7276    {  $1 eq 'b' ? $msginfo->body_digest
7277     : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time))
7278     : $1 eq 'n' ? am_id()
7279     : $1 eq '%' ? '%' : '%'.$1 }egs;
7280  do_log(5, "do_virus: looking for per-recipient quarantine and admins");
7281  my($newvirus_admin_maps_ref) =
7282    @virusname && !$virus_dejavu ? ca('newvirus_admin_maps') : undef;
7283  my(@q_addr,@a_addr);  # get per-recipient quarantine address(es) and admins
7284  for my $r (@{$msginfo->per_recip_data}) {
7285    my($rec) = $r->recip_addr;
7286    my($q);  # quarantine (pseudo) address associated with the recipient
7287    my($a);  # administrator's e-mail address
7288    ($q) = lookup(0,$rec,@$quarantine_to_maps_ref)  if $quarantine_to_maps_ref;
7289    $q = $rec  if $q ne '' && $q_method =~ /^bsmtp:/i;  # orig.recip when BSMTP
7290    ($a) = lookup(0,$rec,@$admin_maps_ref)  if $admin_maps_ref;
7291    push(@q_addr, $q)  if $q ne '' && !grep {$_ eq $q} @q_addr;
7292    push(@a_addr, $a)  if $a ne '' && !grep {$_ eq $a} @a_addr;
7293    if ($newvirus_admin_maps_ref) {
7294      ($a) = lookup(0,$rec,@$newvirus_admin_maps_ref);
7295      push(@a_addr, $a)  if $a ne '' && !grep {$_ eq $a} @a_addr;
7296    }
7297  }
7298  if (@q_addr) {  # do the quarantining
7299    # prepare header edits for the quarantined message
7300    my($hdr_edits) = Amavis::Out::EditHeader->new;
7301    $hdr_edits->prepend_header('X-Quarantine-Id', "<$VIRUSFILE>");
7302    if (@virusname) {
7303      $hdr_edits->append_header('X-Amavis-Alert',
7304        "INFECTED, message contains virus:\n " . join(",\n ", @virusname), 1);
7305    }
7306    if (@banned_filename) {
7307      my(@b) = @banned_filename>3 ? @banned_filename[0..2] : @banned_filename;
7308      my($msg) = "BANNED, message contains "
7309                 . (@banned_filename==1 ? 'part' : 'parts') . ":\n "
7310                 . join(",\n ", @b) . (@banned_filename > @b ? ", ..." : "");
7311      $hdr_edits->append_header('X-Amavis-Alert', $msg, 1);
7312    }
7313    if (@bad_headers) {
7314      $hdr_edits->append_header('X-Amavis-Alert',
7315                                'BAD HEADER '.$bad_headers[0], 1);
7316    }
7317    do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method,
7318                  @virusname ? 'QuarVirusMsgs' :
7319                  @banned_filename ? 'QuarBannedMsgs' :
7320                  @bad_headers ? 'QuarBadHMsgs' : 'QuarOther');
7321  }
7322  my($hdr_edits) = Amavis::Out::EditHeader->new;
7323  if (!@a_addr) {
7324    do_log(4, "Skip admin notification, no administrators");
7325  } else {   # notify per-recipient virus administrators
7326    ll(5) && do_log(5, sprintf("DO_VIRUS - NOTIFICATIONS to %s; sender: %s",
7327                   join(",",qquote_rfc2821_local(@a_addr)), $msginfo->sender));
7328    my($notification) = Amavis::In::Message->new;
7329    $notification->rx_time($msginfo->rx_time);  # copy the reception time
7330    $notification->delivery_method(c('notify_method'));
7331    $notification->sender(c('mailfrom_notify_admin'));
7332    $notification->auth_submitter(
7333      qquote_rfc2821_local(c('mailfrom_notify_admin')));
7334    $notification->auth_user(c('amavis_auth_user'));
7335    $notification->auth_pass(c('amavis_auth_pass'));
7336    $notification->recips([@a_addr]);
7337    my(%mybuiltins) = %builtins;  # make a local copy
7338    $mybuiltins{'T'} = \@a_addr;                        # used in 'To:'
7339    $mybuiltins{'f'} = c('hdrfrom_notify_admin');       # From:
7340    $notification->mail_text(
7341      string_to_mime_entity(expand(cr('notify_virus_admin_templ'),
7342                                   \%mybuiltins)));
7343    $notification->header_edits($hdr_edits);
7344    mail_dispatch($conn, $notification, 1);
7345    my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
7346      one_response_for_all($notification, 0, am_id());  # check status
7347    if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) {       # ok
7348    } elsif ($n_smtp_resp =~ /^4/) {
7349      die "temporarily unable to notify virus admin: $n_smtp_resp";
7350    } else {
7351      do_log(-1, "FAILED to notify virus admin: $n_smtp_resp");
7352    }
7353    # $notification->purge;
7354  }
7355
7356# if (! defined($msginfo->sender_contact) ) {
7357#   do_log(5,"do_virus: skip recipient notifications for unknown senders");
7358# } else {  # send notification to recipients
7359    my(@recips_to_notify) =
7360      grep {
7361        @virusname       && !lookup(0,$_,@{ca('bypass_virus_checks_maps')}) ?
7362          scalar(lookup(0,$_,@{ca('warnvirusrecip_maps')}))
7363      : @banned_filename && !lookup(0,$_,@{ca('bypass_banned_checks_maps')}) ?
7364          scalar(lookup(0,$_,@{ca('warnbannedrecip_maps')}))
7365      : @bad_headers     && !lookup(0,$_,@{ca('bypass_header_checks_maps')}) ?
7366          scalar(lookup(0,$_,@{ca('warnbadhrecip_maps')}))
7367      : 0  }
7368      grep { c('warn_offsite') || lookup(0,$_,@{ca('local_domains_maps')}) }
7369           @{$msginfo->recips};
7370    if (!@recips_to_notify) {
7371      do_log(5,"do_virus: recipient notifications not required");
7372    } else {
7373      my($notification) = Amavis::In::Message->new;
7374      $notification->rx_time($msginfo->rx_time); # copy the reception time
7375      $notification->delivery_method(c('notify_method'));
7376      $notification->sender(c('mailfrom_notify_recip'));
7377      $notification->auth_submitter(
7378        qquote_rfc2821_local(c('mailfrom_notify_recip')));
7379      $notification->auth_user(c('amavis_auth_user'));
7380      $notification->auth_pass(c('amavis_auth_pass'));
7381      $notification->recips(\@recips_to_notify);
7382      my(%mybuiltins) = %builtins;  # make a local copy
7383      $mybuiltins{'f'} = c('hdrfrom_notify_recip');       # 'From:'
7384      $mybuiltins{'T'} = (@recips_to_notify==1 && $recips_to_notify[0] ne '') ?  # 'To:'
7385                           [quote_rfc2821_local($recips_to_notify[0])] : undef;
7386      $notification->mail_text(
7387        string_to_mime_entity(expand(cr('notify_virus_recips_templ'),
7388                                     \%mybuiltins)) );
7389      $notification->header_edits($hdr_edits);
7390      mail_dispatch($conn, $notification, 1);
7391      my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
7392        one_response_for_all($notification, 0, am_id());  # check status
7393      if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) {       # ok
7394      } elsif ($n_smtp_resp =~ /^4/) {
7395        die "temporarily unable to notify recipients: $n_smtp_resp";
7396      } else {
7397        do_log(-1, "FAILED to notify recipients: $n_smtp_resp");
7398      }
7399      # $notification->purge;
7400    }
7401# }
7402  do_log(5, "DO_VIRUS - DONE");
7403}
7404
7405#
7406# If Spam found - quarantine it and log report
7407sub do_spam($$) {
7408  my($conn, $msginfo) = @_;
7409  # suggest a name to be used as 'X-Quarantine-Id:' or file name
7410  my($q_method) = c('spam_quarantine_method');
7411  $VIRUSFILE = $q_method =~ /^(?:local|bsmtp):(.*)\z/si ? $1 : "spam-%b-%i-%n";
7412  $VIRUSFILE =~ s{%(.)}
7413    {  $1 eq 'b' ? $msginfo->body_digest
7414     : $1 eq 'i' ? strftime("%Y%m%d-%H%M%S",localtime($msginfo->rx_time))
7415     : $1 eq 'n' ? am_id()
7416     : $1 eq '%' ? '%' : '%'.$1 }egs;
7417  # use the smallest value as the level reported in quarantined headers!
7418  my($tag_level) =
7419    min(map { scalar(lookup(0,$_,@{ca('spam_tag_level_maps')}))  } @{$msginfo->recips});
7420  my($tag2_level) =
7421    min(map { scalar(lookup(0,$_,@{ca('spam_tag2_level_maps')})) } @{$msginfo->recips});
7422  my($kill_level) =
7423    min(map { scalar(lookup(0,$_,@{ca('spam_kill_level_maps')})) } @{$msginfo->recips});
7424  my($blacklisted) =
7425    scalar(grep { $_->recip_blacklisted_sender } @{$msginfo->per_recip_data});
7426  my($whitelisted) =
7427    scalar(grep { $_->recip_whitelisted_sender } @{$msginfo->per_recip_data});
7428  my($s) = $spam_status; $s =~ s/,/,\n /g;  # allow header field wrapping
7429  my($full_spam_status) = sprintf(
7430    "%s,\n hits=%s\n tag=%s\n tag2=%s\n kill=%s\n %s%s",
7431    (defined $spam_level && defined $tag2_level && $spam_level>=$tag2_level ?
7432       'Yes' : 'No'),
7433    (map { !defined $_ ? 'x' : 0+sprintf("%.3f",$_) }
7434         ($spam_level, $tag_level, $tag2_level, $kill_level)),
7435    join('', $blacklisted ? "BLACKLISTED\n " : (),
7436             $whitelisted ? "WHITELISTED\n " : ()),
7437    $s);
7438
7439  do_log(5, "do_spam: looking for a quarantine address");
7440  my(@q_addr,@a_addr);  # quarantine address(es) and administrators
7441  my($sqbsm) = ca('spam_quarantine_bysender_to_maps');
7442  if (@$sqbsm) {   # by-sender quarantine
7443    my($a);  $a = lookup(0,$msginfo->sender, @$sqbsm);
7444    push(@q_addr, $a)  if $a ne '';
7445  }
7446  # get per-recipient quarantine address(es) and admins
7447  for my $r (@{$msginfo->per_recip_data}) {
7448    my($rec) = $r->recip_addr;
7449    my($q);  # quarantine (pseudo) address associated with the recipient
7450    ($q) = lookup(0,$rec, @{ca('spam_quarantine_to_maps')});
7451    $q = $rec  if $q ne '' && $q_method =~ /^bsmtp:/i;  # orig.recip when BSMTP
7452    my($a) = lookup(0,$rec, @{ca('spam_admin_maps')});
7453    push(@q_addr, $q)  if $q ne '' && !grep {$_ eq $q} @q_addr;
7454    push(@a_addr, $a)  if $a ne '' && !grep {$_ eq $a} @a_addr;
7455  }
7456  if (@q_addr) {  # do the quarantining
7457    # prepare header edits for the quarantined message
7458    my($hdr_edits) = Amavis::Out::EditHeader->new;
7459    $hdr_edits->prepend_header('X-Quarantine-Id', "<$VIRUSFILE>");
7460    $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
7461#   $hdr_edits->append_header('X-Spam-Score',
7462#     !defined $spam_level ? 'x' : 0+sprintf("%.3f",$spam_level) );
7463    my($slc) = c('sa_spam_level_char');
7464    $hdr_edits->append_header('X-Spam-Level',
7465                              $slc x min(0+$spam_level,64))  if $slc ne '';
7466    $hdr_edits->append_header('X-Spam-Report', $spam_report,1)
7467      if c('sa_spam_report_header') && $spam_report ne '';
7468    do_quarantine($conn,$msginfo,$hdr_edits,\@q_addr,$q_method,'QuarSpamMsgs');
7469  }
7470  $s = $full_spam_status; $s =~ s/\n[ \t]/ /g;
7471  do_log(2,sprintf("SPAM, <%s> -> %s, %s%s", $msginfo->sender_source,
7472                   join(',', qquote_rfc2821_local(@{$msginfo->recips})),  $s,
7473                   !@q_addr ? '' : sprintf(", quarantine %s (%s)",
7474                                           $VIRUSFILE, join(',', @q_addr)) ));
7475  if (!@a_addr) {
7476    do_log(4, "Skip spam admin notification, no administrators");
7477  } else {  # notify per-recipient spam administrators
7478    ll(5) && do_log(5, sprintf("DO_SPAM - NOTIFICATIONS to %s; sender: %s",
7479                   join(",",qquote_rfc2821_local(@a_addr)), $msginfo->sender));
7480    my($notification) = Amavis::In::Message->new;
7481    $notification->rx_time($msginfo->rx_time);  # copy the reception time
7482    $notification->delivery_method(c('notify_method'));
7483    $notification->sender(c('mailfrom_notify_spamadmin'));
7484    $notification->auth_submitter(
7485      qquote_rfc2821_local(c('mailfrom_notify_spamadmin')));
7486    $notification->auth_user(c('amavis_auth_user'));
7487    $notification->auth_pass(c('amavis_auth_pass'));
7488    $notification->recips([@a_addr]);
7489    my(%mybuiltins) = %builtins;  # make a local copy
7490    $mybuiltins{'T'} = \@a_addr;                        # used in 'To:'
7491    $mybuiltins{'f'} = c('hdrfrom_notify_spamadmin');
7492    $notification->mail_text(
7493      string_to_mime_entity(expand(cr('notify_spam_admin_templ'),
7494                                   \%mybuiltins)));
7495    my($hdr_edits) = Amavis::Out::EditHeader->new;
7496    $notification->header_edits($hdr_edits);
7497    mail_dispatch($conn, $notification, 1);
7498    my($n_smtp_resp, $n_exit_code, $n_dsn_needed) =
7499      one_response_for_all($notification, 0, am_id());  # check status
7500    if ($n_smtp_resp =~ /^2/ && !$n_dsn_needed) {       # ok
7501    } elsif ($n_smtp_resp =~ /^4/) {
7502      die "temporarily unable to notify spam admin: $n_smtp_resp";
7503    } else {
7504      do_log(-1, "FAILED to notify spam admin: $n_smtp_resp");
7505    }
7506  # $notification->purge;
7507  }
7508  do_log(5, "DO_SPAM DONE");
7509}
7510
7511# Calculate message digest;
7512# While at it, also get the message size and store original header,
7513# since we need it for the %H macro, and MIME::Tools may modify it.
7514
7515sub get_body_digest($$) {
7516  my($fh, $msginfo) = @_;
7517  $fh->seek(0,0) or die "Can't rewind mail file: $!";
7518  local($_);
7519
7520  # choose message digest method:
7521  my($ctx) = Digest::MD5->new;  # 128 bits (32 hex digits)
7522# my($ctx) = Digest::SHA1->new; # 160 bits (40 hex digits), slightly slower
7523
7524  my(@orig_header);
7525  my($header_size) = 0;
7526  my($body_size) = 0;
7527  while (<$fh>) {               # skip mail header
7528    last  if $_ eq $eol;
7529    $header_size += length($_);
7530    push(@orig_header, $_);     # with trailing EOL
7531  }
7532  my($len);
7533  while (($len = read($fh,$_,16384)) > 0)
7534    { $ctx->add($_); $body_size += $len }
7535  my($signature) = $ctx->hexdigest;
7536# my($signature) = $ctx->b64digest;
7537  $signature = untaint($signature)  # checked (either 32 or 40 char)
7538    if $signature =~ /^ [0-9a-fA-F]{32} (?: [0-9a-fA-F]{8} )? \z/x;
7539  # store information obtained
7540  $msginfo->orig_header(\@orig_header);
7541  $msginfo->orig_header_size($header_size);
7542  $msginfo->orig_body_size($body_size);
7543  $msginfo->body_digest($signature);
7544
7545  section_time('body_hash');
7546  do_log(3, "body hash: $signature");
7547  $signature;
7548}
7549
7550sub find_program_path($$$) {
7551  my($fv_list, $path_list_ref, $may_log) = @_;
7552  $fv_list = [$fv_list]  if !ref $fv_list;
7553  my($found);
7554  for my $fv (@$fv_list) {
7555    my(@fv_cmd) = split(' ',$fv);
7556    if (!@fv_cmd) {  # empty, not available
7557    } elsif ($fv_cmd[0] =~ /^\//) {  # absolute path
7558      my($errn) = stat($fv_cmd[0]) ? 0 : 0+$!;
7559      if    ($errn == ENOENT) { }
7560      elsif ($errn)           {
7561        do_log(-1, "find_program_path: " . "$fv_cmd[0] inaccessible: $!")
7562          if $may_log;
7563      } elsif (-x _ && !-d _) { $found = join(' ', @fv_cmd) }
7564    } elsif ($fv_cmd[0] =~ /\//) {   # relative path
7565      die "find_program_path: relative paths not implemented: @fv_cmd\n";
7566    } else {                         # walk through the specified PATH
7567      for my $p (@$path_list_ref) {
7568        my($errn) = stat("$p/$fv_cmd[0]") ? 0 : 0+$!;
7569        if    ($errn == ENOENT) { }
7570        elsif ($errn)           {
7571          do_log(-1, "find_program_path: " . "$p/$fv_cmd[0] inaccessible: $!")
7572            if $may_log;
7573        } elsif (-x _ && !-d _) {
7574          $found = $p . '/' . join(' ', @fv_cmd);
7575          last;
7576        }
7577      }
7578    }
7579    last  if defined $found;
7580  }
7581  $found;
7582}
7583
7584sub find_external_programs($) {
7585  my($path_list_ref) = @_;
7586  for my $f (qw($file $arc $gzip $bzip2 $lzop $lha $unarj $uncompress
7587                $unfreeze $unrar $zoo $pax $cpio $ar $rpm2cpio $cabextract
7588                $ripole $dspam))
7589  {
7590    my($g) = $f;
7591    $g =~ s/\$/Amavis::Conf::/;
7592    my($fv_list) = eval('$' . $g);
7593    my($found) = find_program_path($fv_list, $path_list_ref, 1);
7594    { no strict 'refs'; $$g = $found }  # NOTE: a symbolic reference
7595    if (!defined $found) {
7596      do_log(-1, sprintf("No %-14s not using it", "$f,"));
7597    } else {
7598      do_log(0,sprintf("Found %-11s at %s%s", $f,
7599              $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
7600              $found));
7601    }
7602  }
7603  # map program name hints to full paths
7604  my($tier) = 'primary';  # primary, secondary, ...   av scanners
7605  for my $f (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) {
7606    if ($f eq "\000") {   # next tier
7607      $tier = 'secondary';
7608    } elsif (!defined $f || !ref $f) {  # empty, skip
7609    } elsif (ref($f->[1]) eq 'CODE') {
7610      do_log(0, "Using internal av scanner code for ($tier) " . $f->[0]);
7611    } else {
7612      my($found) = $f->[1] = find_program_path($f->[1], $path_list_ref, 1);
7613      if (!defined $found) {
7614        do_log(3, "No $tier av scanner: " . $f->[0]);
7615        $f = undef;                     # release its storage
7616      } else {
7617        do_log(0,sprintf("Found $tier av scanner %-11s at %s%s", $f->[0],
7618              $daemon_chroot_dir ne '' ? "(chroot: $daemon_chroot_dir/) " : '',
7619              $found));
7620      }
7621    }
7622  }
7623}
7624
7625# Fetch all remaining modules.
7626sub fetch_modules_extra() {
7627  my(@modules);
7628  if ($extra_code_sql) {
7629    push(@modules, 'DBI');
7630    for (@lookup_sql_dsn) {
7631      my(@dsn) = split(/:/,$_->[0],-1);
7632      push(@modules, 'DBD::'.$dsn[1])  if uc($dsn[0]) eq 'DBI';
7633    }
7634  }
7635  push(@modules, qw(Net::LDAP Net::LDAP::Util Net::LDAP::Search))
7636    if $extra_code_ldap;
7637  if (c('bypass_decode_parts') &&
7638      !grep {exists $policy_bank{$_}{'bypass_decode_parts'} &&
7639             !$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) {
7640  } else {
7641    push(@modules, qw(Compress::Zlib Convert::TNEF Convert::UUlib
7642                      Archive::Zip Archive::Tar));
7643  }
7644  push(@modules, 'Mail::SpamAssassin')  if $extra_code_antispam;
7645  push(@modules, 'Authen::SASL')  if c('auth_required_out');
7646  Amavis::Boot::fetch_modules('REQUIRED ADDITIONAL MODULES', 1, @modules);
7647  my($sa_version);
7648  $sa_version = Mail::SpamAssassin::Version()  if $extra_code_antispam;
7649  @modules = ();  # now start collecting optional modules
7650  if ($unicode_aware) {
7651    push(@modules, qw(
7652      bytes bytes_heavy.pl utf8 utf8_heavy.pl Encode Encode::Byte
7653      unicore::Canonical.pl unicore::Exact.pl unicore::PVA.pl
7654      unicore::To::Fold.pl unicore::To::Title.pl
7655      unicore::To::Lower.pl unicore::To::Upper.pl
7656    ));
7657  }
7658  if ($extra_code_antispam) {  # must be loaded before chroot takes place
7659    push(@modules, qw(
7660      Mail::SpamAssassin::Locker::Flock
7661      Mail::SpamAssassin::Locker::UnixNFSSafe
7662      Mail::SpamAssassin::DBBasedAddrList
7663      Mail::SpamAssassin::SQLBasedAddrList
7664      Mail::SpamAssassin::BayesStore::DBM
7665      Mail::SpamAssassin::BayesStore::SQL
7666      Mail::SpamAssassin::Plugin::SPF
7667      Mail::SpamAssassin::Plugin::URIDNSBL
7668      Mail::SpamAssassin::Plugin::Hashcash
7669      Mail::SpamAssassin::PerMsgLearner
7670
7671      DBD::mysql Sys::Hostname::Long
7672      Mail::SPF::Query Razor2::Client Net::CIDR::Lite
7673      Net::DNS::RR::SOA Net::DNS::RR::NS Net::DNS::RR::MX
7674      Net::DNS::RR::A Net::DNS::RR::AAAA Net::DNS::RR::PTR
7675      Net::DNS::RR::CNAME Net::DNS::RR::TXT Net::Ping
7676    ));
7677    # ??? ArchiveIterator Reporter Plugin::RelayCountry
7678    # ??? Mail::SpamAssassin::Plugin::Razor2
7679  }
7680  # *** note that $sa_version could be 3.0.1, which is not really numeric!
7681  if ($extra_code_antispam && defined $sa_version && $sa_version < 3) {
7682    push(@modules, qw(
7683      Mail::SpamAssassin::UnixLocker Mail::SpamAssassin::BayesStoreDBM
7684      Mail::SpamAssassin::SpamCopURI
7685      URI URI::Escape URI::Heuristic URI::QueryParam URI::Split URI::URL
7686      URI::WithBase URI::_foreign URI::_generic URI::_ldap URI::_login
7687      URI::_query URI::_segment URI::_server URI::_userpass URI::data URI::ftp
7688      URI::gopher URI::http URI::https URI::ldap URI::ldapi URI::ldaps
7689      URI::mailto URI::mms URI::news URI::nntp URI::pop URI::rlogin URI::rsync
7690      URI::rtsp URI::rtspu URI::sip URI::sips URI::snews URI::ssh URI::telnet
7691      URI::tn3270 URI::urn URI::urn::isbn URI::urn::oid
7692      URI::file URI::file::Base URI::file::Unix URI::file::Win32
7693    ));
7694  }
7695  my($missing);
7696  $missing = Amavis::Boot::fetch_modules('PRE-COMPILE OPTIONAL MODULES', 0,
7697                                         @modules)  if @modules;
7698  do_log(2, 'INFO: no optional modules: '.join(' ',@$missing))
7699    if ref $missing && @$missing;
7700  # load optional modules SAVI and Mail::ClamAV if available and requested
7701  if ($extra_code_antivirus) {
7702    my($savi_obj, $savi_module_ok, $clamav_module_ok);
7703    for my $entry (@{ca('av_scanners')}, @{ca('av_scanners_backup')}) {
7704      if (ref($entry) ne 'ARRAY') {  # none
7705      } elsif ($entry->[1] eq \&ask_sophos_savi ||
7706               $entry->[1] eq \&sophos_savi ||
7707               $entry->[0] eq 'Sophos SAVI') {
7708        if (!defined($savi_module_ok)) {
7709          $savi_module_ok = eval { require SAVI };
7710          $savi_module_ok = 0  if !defined $savi_module_ok;
7711#
7712# if the Amavis::AV::sophos_savi_init() in the following line _is_
7713# called from here, SAVI-Perl will only initialize once during startup;
7714# otherwise (if left uninitialized here) each child process will do the
7715# SAVI-Perl initialization at its birth, which costs some time, but avoids
7716# the need to restart amavisd when IDE database changes:
7717###       $savi_obj = Amavis::AV::sophos_savi_init(@$entry) if $savi_module_ok;
7718
7719        }
7720        if (!$savi_module_ok) { $entry->[1] = undef }  # disable entry
7721        else { $entry->[2] = $savi_obj  if defined $savi_obj }  # save as args
7722      } elsif ($entry->[1] eq \&ask_clamav ||
7723               $entry->[0] =~ /^Mail::ClamAV/) {
7724        if (!defined($clamav_module_ok)) {
7725          $clamav_module_ok = eval { require Mail::ClamAV };
7726          $clamav_module_ok = 0  if !defined $clamav_module_ok;
7727        }
7728        $entry->[1] = undef  if !$clamav_module_ok;  # disable entry
7729      }
7730    }
7731  }
7732}
7733
7734#
7735# Main program starts here
7736#
7737
7738# Read dynamic source code, and logging and notification message templates
7739# from the end of this file (pseudo file handle DATA)
7740#
7741$Amavis::Conf::notify_spam_admin_templ  = '';  # not used
7742$Amavis::Conf::notify_spam_recips_templ = '';  # not used
7743do { local($/) = "__DATA__\n";   # set line terminator to this string
7744  chomp($_ = <Amavis::DATA>)  for (
7745    $extra_code_db, $extra_code_cache,
7746    $extra_code_sql, $extra_code_ldap,
7747    $extra_code_in_amcl, $extra_code_in_smtp, $extra_code_in_qmqpqq,
7748    $extra_code_antivirus, $extra_code_antispam, $extra_code_unpackers,
7749    $Amavis::Conf::log_templ, $Amavis::Conf::log_recip_templ);
7750  if ($unicode_aware) {
7751#   binmode(\*Amavis::DATA, ":encoding(utf8)")    #  :encoding(iso-8859-1)
7752#     or die "Can't set \*DATA encoding: $!";
7753  }
7754  chomp($_ = <Amavis::DATA>)  for (
7755    $Amavis::Conf::notify_sender_templ,
7756    $Amavis::Conf::notify_virus_sender_templ,
7757    $Amavis::Conf::notify_virus_admin_templ,
7758    $Amavis::Conf::notify_virus_recips_templ,
7759    $Amavis::Conf::notify_spam_sender_templ,
7760    $Amavis::Conf::notify_spam_admin_templ );
7761}; # restore line terminator
7762close(\*Amavis::DATA) or die "Can't close *Amavis::DATA: $!";
7763# close(STDIN)        or die "Can't close STDIN: $!";
7764# note: don't close STDIN just yet to prevent some other file taking up fd 0
7765
7766# discard trailing NL
7767$Amavis::Conf::log_templ = $1
7768  if $Amavis::Conf::log_templ=~/^(.*?)[\r\n]+\z/s;
7769$Amavis::Conf::log_recip_templ = $1
7770  if $Amavis::Conf::log_recip_templ=~/^(.*?)[\r\n]+\z/s;
7771
7772# Consider droping privileges early, before reading config file.
7773# This is only possible if running under chroot will not be needed.
7774#
7775my($desired_group);                      # defaults to $desired_user's group
7776my($desired_user);                       # username or UID
7777if ($> != 0) { $desired_user = $> }      # use effective UID if not root
7778#else {
7779# for my $u ('amavis', 'vscan') {        # try to guess a good default username
7780#   my($username,$passwd,$uid,$gid) = getpwnam($u);
7781#   if (defined $uid && $uid != 0) { $desired_user = $u; last }
7782# }
7783#}
7784
7785# collect and parse command line options
7786while (@ARGV >= 2 && $ARGV[0] =~ /^-[ugc]\z/) {
7787  my($opt) = shift @ARGV;
7788  if ($opt eq '-u') {       # -u username
7789    my($val) = shift @ARGV;
7790    if ($> == 0) { $desired_user = $val }
7791    else { print STDERR "Ignoring option -u when not running as root\n" }
7792  } elsif ($opt eq '-g') {  # -g group
7793    my($val) = shift @ARGV;
7794    if ($> == 0) { $desired_group = $val }
7795    else { print STDERR "Ignoring option -g when not running as root\n" }
7796  } elsif ($opt eq '-c') {  # -c config_file
7797    push(@config_files, untaint(shift @ARGV));
7798  }
7799}
7800
7801if (defined $desired_user && ($> == 0 || $< == 0)) {   # drop privileges early
7802  my($username,$passwd,$uid,$gid) =
7803    $desired_user=~/^(\d+)$/ ? (undef,undef,$1,undef) :getpwnam($desired_user);
7804  defined $uid or die "No such username: $desired_user\n";
7805  if ($desired_group eq '') { $desired_group = $gid }  # for logging purposes
7806  else { $gid = $desired_group=~/^(\d+)$/ ? $1 : getgrnam($desired_group) }
7807  defined $gid or die "No such group: $desired_group\n";
7808  $( = $gid;  # real GID
7809  $) = "$gid $gid";  # effective GID
7810  POSIX::setuid($uid) or die "Can't setuid to $uid: $!";
7811  $> = $uid; $< = $uid;  # just in case
7812# print STDERR "desired user=$desired_user ($uid), current: EUID: $> ($<)\n";
7813# print STDERR "desired group=$desired_group, current: EGID: $) ($()\n";
7814  $> != 0 or die "Still running as root, aborting\n";
7815  $< != 0 or die "Effective UID changed, but Real UID is 0\n";
7816}
7817
7818umask(0027);
7819
7820# default location of the config file if none specified
7821push(@config_files, '/etc/amavisd.conf')  if !@config_files;
7822
7823# Read config file, which may override default settings
7824Amavis::Conf::build_default_maps();
7825Amavis::Conf::read_config(@config_files);
7826# chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
7827
7828if (defined $desired_user && $daemon_user ne '') {
7829  # compare the config file settings to current UID
7830  my($username,$passwd,$uid,$gid) =
7831    $daemon_user=~/^(\d+)$/ ? (undef,undef,$1,undef) : getpwnam($daemon_user);
7832  $uid == $> or warn sprintf(
7833    "WARN: running under user '%s' (UID=%s), the config file".
7834    " specifies \$daemon_user='%s' (UID=%s)\n",
7835    $desired_user, $>, $daemon_user, defined $uid ? $uid : '?');
7836}
7837
7838# compile optional modules if needed
7839# %modules_basic = %INC;  # helps to track missing modules in chroot
7840if (!$enable_db) { $extra_code_db = undef }
7841else {
7842  eval $extra_code_db or die "Problem in Amavis::DB or Amavis::DB::SNMP code: $@";
7843  $extra_code_db = 1;         # release memory occupied by the source code
7844}
7845if (!$enable_global_cache || !$extra_code_db) { $extra_code_cache = undef }
7846else {
7847  eval $extra_code_cache or die "Problem in the Amavis::Cache code: $@";
7848  $extra_code_cache = 1;      # release memory occupied by the source code
7849}
7850if (!@lookup_sql_dsn) { $extra_code_sql = undef }
7851else {
7852  eval $extra_code_sql or die "Problem in the Lookup::SQL code: $@";
7853  $extra_code_sql = 1;        # release memory occupied by the source code
7854}
7855if (!$enable_ldap) { $extra_code_ldap = undef }
7856else {
7857  eval $extra_code_ldap or die "Problem in the Lookup::LDAP code: $@";
7858  $extra_code_ldap = 1;       # release memory occupied by the source code
7859}
7860
7861if (c('protocol') eq 'COURIER') {
7862  die "In::Courier code not available";
7863} elsif (c('protocol') eq 'AM.PDP' || $unix_socketname ne '') {
7864  eval $extra_code_in_amcl or die "Problem in the In::AMCL code: $@";
7865  $extra_code_in_amcl = 1;    # release memory occupied by the source code
7866} else {
7867  $extra_code_in_amcl = undef;
7868}
7869
7870if (c('protocol') eq 'QMQPqq') { # simpleminded, not checking all policy banks
7871  eval $extra_code_in_qmqpqq or die "Problem in the In::QMQPqq code: $@";
7872  $extra_code_in_qmqpqq = 1;  # release memory occupied by the source code
7873  $extra_code_in_smtp = undef;
7874} elsif (c('protocol') =~ /^(SMTP|LMTP)\z/ ||
7875         $inet_socket_port ne '' &&
7876           (!ref $inet_socket_port || @$inet_socket_port)) { # assume SMTP/LMTP
7877  eval $extra_code_in_smtp or die "Problem in the In::SMTP code: $@";
7878  $extra_code_in_smtp = 1;    # release memory occupied by the source code
7879  $extra_code_in_qmqpqq = undef;
7880} else {
7881  $extra_code_in_smtp = undef;
7882  $extra_code_in_qmqpqq = undef;
7883}
7884
7885my($bpvcm) = ca('bypass_virus_checks_maps');
7886if (!@{ca('av_scanners')} && !@{ca('av_scanners_backup')}) {
7887  $extra_code_antivirus = undef;
7888} elsif (@$bpvcm && !ref($bpvcm->[0]) && $bpvcm->[0]) {
7889  # do a simple-minded test to make it easy to turn off virus checks
7890  $extra_code_antivirus = undef;
7891} else {
7892  eval $extra_code_antivirus or die "Problem in the antivirus code: $@";
7893  $extra_code_antivirus = 1;  # release memory occupied by the source code
7894}
7895if (!$extra_code_antivirus)  # release storage
7896  { @Amavis::Conf::av_scanners = @Amavis::Conf::av_scanners_backup = () }
7897
7898my($bpscm) = ca('bypass_spam_checks_maps');
7899if (@$bpscm && !ref($bpscm->[0]) && $bpscm->[0]) {
7900  # do a simple-minded test to make it easy to turn off spam checks
7901  $extra_code_antispam = undef;
7902} else {
7903  eval $extra_code_antispam or die "Problem in the antispam code: $@";
7904  $extra_code_antispam = 1;   # release memory occupied by the source code
7905}
7906
7907if (c('bypass_decode_parts') &&
7908    !grep {exists $policy_bank{$_}{'bypass_decode_parts'} &&
7909           !$policy_bank{$_}{'bypass_decode_parts'} } keys %policy_bank) {
7910  $extra_code_unpackers = undef;
7911} else {
7912  eval $extra_code_unpackers or die "Problem in the Amavis::Unpackers code: $@";
7913  $extra_code_unpackers = 1;  # release memory occupied by the source code
7914}
7915
7916# act on command line parameters
7917my($cmd) = lc($ARGV[0]);
7918if ($cmd =~ /^(start|debug|debug-sa|foreground)?\z/) {
7919  $DEBUG=1      if $cmd eq 'debug';
7920  $daemonize=0  if $cmd eq 'foreground';
7921  $daemonize=0, $sa_debug=1  if $cmd eq 'debug-sa';
7922} elsif ($cmd !~ /^(reload|stop)\z/) {
7923  die "$myversion: Unknown argument.  Usage:\n  $0 [-u user] [-g group] [-c config-file] ( [start] | stop | reload | debug | debug-sa | foreground )\n";
7924} else {  # stop or reload
7925  eval {  # first stop a running daemon
7926    $pid_file ne '' or die "Config parameter \$pid_file not defined";
7927    my($errn) = stat($pid_file) ? 0 : 0+$!;
7928    $errn != ENOENT or die "No PID file $pid_file\n";
7929    $errn == 0      or die "PID file $pid_file inaccessible: $!";
7930    my($amavisd_pid);
7931    open(PID_FILE, "< $pid_file\0") or die "Can't read file $pid_file: $!";
7932    while (<PID_FILE>) { chomp; $amavisd_pid = $_ if /^\d+\z/ }
7933    close(PID_FILE) or die "Can't close file $pid_file: $!";
7934    defined($amavisd_pid) or die "Invalid PID in the $pid_file";
7935    $amavisd_pid = untaint($amavisd_pid);
7936    kill('TERM',$amavisd_pid) or die "Can't SIGTERM amavisd[$amavisd_pid]: $!";
7937    my($delay) = 1;  # seconds
7938    for (;;) {
7939      sleep($delay); $delay = 5;
7940      last  if !kill(0,$amavisd_pid);  # is the old daemon still there?
7941      print STDERR "Waiting for the process $amavisd_pid to terminate\n";
7942    }
7943  };
7944  if ($@ ne '') { chomp($@); die "$@, can't $cmd the process\n" }
7945  exit 0  if $cmd eq 'stop';
7946  print STDERR "daemon terminated, waiting for the dust to settle...\n";
7947  sleep 5;  # wait for the TCP socket to be released
7948  print STDERR "becoming a new daemon...\n";
7949}
7950$daemonize = 0  if $DEBUG;
7951
7952# Set path, home and term explictly.  Don't trust environment
7953$ENV{PATH} = $path          if $path ne '';
7954$ENV{HOME} = $helpers_home  if $helpers_home ne '';
7955$ENV{TERM} = 'dumb'; $ENV{COLUMNS} = '80'; $ENV{LINES} = '100';
7956
7957Amavis::Log::init("amavis", !$daemonize, $DO_SYSLOG, $SYSLOG_LEVEL, $LOGFILE);
7958
7959# report version of Perl and process UID
7960do_log(1, "user=$desired_user, EUID: $> ($<);  group=$desired_group, EGID: $) ($()");
7961do_log(0, "Perl version               $]");
7962
7963# $SIG{USR2} = sub {
7964#   my($msg) = Carp::longmess("SIG$_[0] received, backtrace:");
7965#   print STDERR "\n",$msg,"\n";  do_log(-1,$msg);
7966# };
7967
7968fetch_modules_extra();  # bring additional modules into memory and compile them
7969
7970# set up Net::Server configuration
7971my $server = bless {
7972  server => {
7973    # command args to be used after HUP must be untainted, deflt: [$0,@ARGV]
7974  # commandline => ['/usr/local/sbin/amavisd','-c',$config_file[0] ],
7975    commandline => [],  # disable
7976
7977    # listen on the following sockets (one or more):
7978    port => [ ($unix_socketname eq '' ? () : "$unix_socketname|unix"), # helper
7979              map { "$_/tcp" }             # accept SMTP on this port(s)
7980                  (ref $inet_socket_port ? @$inet_socket_port
7981                   : $inet_socket_port ne '' ? $inet_socket_port : () ),
7982            ],
7983    # limit socket bind (e.g. to the loopback interface)
7984    host => ($inet_socket_bind eq '' ? '*' : $inet_socket_bind),
7985
7986    max_servers  => $max_servers,  # number of pre-forked children
7987    max_requests => $max_requests, # restart child after that many accept's
7988    user       => (($> == 0 || $< == 0) ? $daemon_user  : undef),
7989    group      => (($> == 0 || $< == 0) ? $daemon_group : undef),
7990    pid_file   => $pid_file,
7991    lock_file  => $lock_file,  # serialization lockfile
7992  # serialize  => 'flock',     # flock, semaphore, pipe
7993    background => $daemonize ? 1 : undef,
7994    setsid     => $daemonize ? 1 : undef,
7995    chroot     => $daemon_chroot_dir ne '' ? $daemon_chroot_dir : undef,
7996    no_close_by_child => 1,
7997
7998    # controls log level for Net::Server internal log messages:
7999    #   0=err, 1=warning, 2=notice, 3=info, 4=debug
8000    log_level  => ($DEBUG ? 4 : 2),
8001    log_file   => undef,  # will be overridden to call do_log()
8002  },
8003}, 'Amavis';
8004
8005$0 = 'amavisd (master)';
8006$server->run;  # transfer control to Net::Server
8007
8008# shouldn't get here
8009exit 1;
8010
8011# we read text (especially notification templates) from DATA sections
8012# to avoid any interpretations of special characters (e.g. \ or ') by Perl
8013#
8014
8015__DATA__
8016#
8017package Amavis::DB::SNMP;
8018use strict;
8019use re 'taint';
8020
8021BEGIN {
8022  import Amavis::Conf qw($myversion $myhostname);
8023  import Amavis::Util qw(ll do_log snmp_counters_get);
8024}
8025
8026use BerkeleyDB;
8027
8028BEGIN {
8029  use Exporter ();
8030  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
8031  $VERSION = '2.034';
8032  @ISA = qw(Exporter);
8033}
8034
8035# open existing databases (called by each child process)
8036sub new {
8037  my($class,$db_env) = @_; my($env) = $db_env->get_db_env;
8038  defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!.";
8039  my($dbs) = BerkeleyDB::Hash->new(-Filename=>'snmp.db', -Env=>$env);
8040  defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!.";
8041  my($dbn) = BerkeleyDB::Hash->new(-Filename=>'nanny.db', -Env=>$env);
8042  defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!.";
8043  bless { 'db_snmp'=>$dbs, 'db_nanny'=>$dbn }, $class;
8044}
8045
8046sub DESTROY {
8047  my($self) = shift;
8048  eval { do_log(5,"Amavis::DB::SNMP called") };
8049  for my $db ($self->{'db_snmp'}, $self->{'db_nanny'}) {
8050    if (defined $db) {
8051      eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." };
8052      if ($@ ne '') { warn "BDB S+N DESTROY $@" }
8053      $db = undef;
8054    }
8055  }
8056}
8057
8058#sub lock_stat($) {
8059# my($label) = @_;
8060# my($s) = qx'/usr/local/bin/db_stat-4.2 -c -h /var/amavis/db | /usr/local/bin/perl -ne \'$a{$2}=$1 if /^(\d+)\s+Total number of locks (requested|released)/; END {printf("%d, %d\n",$a{requested}, $a{requested}-$a{released})}\'';
8061# do_log(0, "lock_stat $label: $s");
8062#}
8063
8064# insert startup time SNMP entry, called from the master process at startup
8065# (a classical subroutine, not a method)
8066sub put_initial_snmp_data($) {
8067  my($db) = @_;
8068  my($cursor) = $db->db_cursor(DB_WRITECURSOR);
8069  defined $cursor or die "BDB S db_cursor: $BerkeleyDB::Error, $!.";
8070  for my $obj (['sysDescr',    'STR', $myversion],
8071               ['sysObjectID', 'OID', '1.3.6.1.4.1.15312.2.1'],
8072                 # iso.org.dod.internet.private.enterprise.ijs.amavisd-new.snmp
8073               ['sysUpTime',   'INT', int(time)],
8074                 # later it must be converted to timeticks (10ms since start)
8075               ['sysContact',  'STR', ''],
8076               ['sysName',     'STR', $myhostname],
8077               ['sysLocation', 'STR', ''],
8078               ['sysServices', 'INT', 64],  # application
8079  ) {
8080    my($key,$type,$val) = @$obj;
8081    $cursor->c_put($key, sprintf("%s %s",$type,$val), DB_KEYLAST) == 0
8082      or die "BDB S c_put: $BerkeleyDB::Error, $!.";
8083  };
8084  $cursor->c_close==0 or die "BDB S c_close: $BerkeleyDB::Error, $!.";
8085}
8086
8087sub update_counters {
8088  my($self) = @_;
8089  my($counter_names_ref) = snmp_counters_get();
8090  my($eval_stat,$interrupt); $interrupt = '';
8091  if (defined $counter_names_ref && @$counter_names_ref) {
8092    my($db) = $self->{'db_snmp'}; my($cursor);
8093    my($h1) = sub { $interrupt = $_[0] };
8094    local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
8095    eval {  # ensure cursor will be unlocked even in case of errors or signals
8096      $cursor = $db->db_cursor(DB_WRITECURSOR);  # obtain write lock
8097      defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
8098      for my $key (@$counter_names_ref) {
8099        my($counter_name,$counter_incr) = ref($key) ? @$key : ($key,1);
8100        my($val,$flags); my($type) = 'C32';
8101        my($stat) = $cursor->c_get($counter_name,$val,DB_SET);
8102        if ($stat==0) {  # exists, update it
8103          if ($val =~ /^\Q$type\E (\d+)\z/o) { $val = $1 }
8104          else { do_log(-2,"WARN: counter syntax? $val, clearing"); $val = 0 }
8105          $flags = DB_CURRENT; $val = $val+$counter_incr;
8106        } else {  # create new entry
8107          $stat==DB_NOTFOUND  or die "c_get: $BerkeleyDB::Error, $!.";
8108          $flags = DB_KEYLAST; $val = $counter_incr;
8109        }
8110        $cursor->c_put($counter_name, sprintf("%s %010d",$type,$val),$flags)==0
8111          or die "c_put: $BerkeleyDB::Error, $!.";
8112      }
8113      $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
8114      $cursor = undef;
8115    };
8116    $eval_stat = $@;
8117    if (defined $db) {
8118      $cursor->c_close  if defined $cursor;  # unlock, ignoring status
8119      $cursor = undef;
8120      if ($eval_stat eq '') {
8121#       my($stat); $db->db_sync();  # not really needed
8122#       $stat==0 or warn "BDB S db_sync, status $stat: $BerkeleyDB::Error, $!.";
8123      }
8124    }
8125  }
8126  delete $self->{'cnt'};
8127  if ($interrupt ne '') { kill($interrupt,$$) }  # resignal
8128  elsif ($eval_stat ne '')
8129    { chomp($eval_stat); die "update_counters: BDB S $eval_stat\n" }
8130}
8131
8132sub read_counters {
8133  my($self,@counter_names) = @_;
8134  my($eval_stat,$interrupt); $interrupt = '';
8135  my($db) = $self->{'db_snmp'}; my($cursor); my(@values);
8136  my($h1) = sub { $interrupt = $_[0] };
8137  local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
8138  eval {  # ensure cursor will be unlocked even in case of errors or signals
8139    $cursor = $db->db_cursor;  # obtain read lock
8140    defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
8141    for my $cname (@counter_names) {
8142      my($val); my($stat) = $cursor->c_get($cname,$val,DB_SET);
8143      push(@values, $stat==0 ? $val : undef);
8144      $stat==0 || $stat==DB_NOTFOUND  or die "c_get: $BerkeleyDB::Error, $!.";
8145    }
8146    $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
8147    $cursor = undef;
8148  };
8149  $eval_stat = $@;
8150  if (defined $db) {
8151    $cursor->c_close  if defined $cursor;  # unlock, ignoring status
8152    $cursor = undef;
8153  }
8154  if ($interrupt ne '') { kill($interrupt,$$) }  # resignal
8155  elsif ($eval_stat ne '')
8156    { chomp($eval_stat); die "read_counters: BDB S $eval_stat\n" }
8157  my($type) = 'C32';
8158  for my $val (@values) {
8159    if (!defined($val)) {}  # keep undefined
8160    elsif ($val =~ /^\Q$type\E (\d+)\z/o) { $val = 0+$1 }
8161    else { do_log(-2,"WARN: counter syntax? $val"); $val = undef }
8162  }
8163  \@values;
8164}
8165
8166sub register_proc {
8167  my($self,$task_id) = @_;
8168  my($db) = $self->{'db_nanny'}; my($cursor);
8169  my($val,$new_val); my($key) = sprintf("%05d",$$);
8170  $new_val = sprintf("%010d %-12s", time, $task_id)  if defined $task_id;
8171  my($eval_stat,$interrupt); $interrupt = '';
8172  my($h1) = sub { $interrupt = $_[0] };
8173  local(@SIG{qw(INT HUP TERM TSTP QUIT ALRM USR1 USR2)}) = ($h1) x 8;
8174  eval {  # ensure cursor will be unlocked even in case of errors or signals
8175    $cursor = $db->db_cursor(DB_WRITECURSOR);  # obtain write lock
8176    defined $cursor or die "db_cursor: $BerkeleyDB::Error, $!.";
8177    my($stat) = $cursor->c_get($key,$val,DB_SET);
8178    $stat==0 || $stat==DB_NOTFOUND or die "c_get: $BerkeleyDB::Error, $!.";
8179    if ($stat==0 && !defined $task_id) {  # remove existing entry
8180      $cursor->c_del==0 or die "c_del: $BerkeleyDB::Error, $!.";
8181    } elsif (defined $task_id && !($stat==0 && $new_val eq $val)) {
8182      # add new, or update existing entry if different
8183      $cursor->c_put($key, $new_val,
8184                     $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0
8185        or die "c_put: $BerkeleyDB::Error, $!.";
8186    }
8187    $cursor->c_close==0 or die "c_close: $BerkeleyDB::Error, $!.";
8188    $cursor = undef;
8189  };
8190  $eval_stat = $@;
8191  if (defined $db) {
8192    $cursor->c_close  if defined $cursor;  # unlock, ignoring status
8193    $cursor = undef;
8194    if ($eval_stat eq '') {
8195#     my($stat) = $db->db_sync();  # not really needed
8196#     $stat==0 or warn "BDB N db_sync, status $stat: $BerkeleyDB::Error, $!.";
8197    }
8198  }
8199  if ($interrupt ne '') { kill($interrupt,$$) }  # resignal
8200  elsif ($eval_stat ne '')
8201    { chomp($eval_stat); die "register_proc: BDB N $eval_stat\n" }
8202}
8203
82041;
8205
8206#
8207package Amavis::DB;
8208use strict;
8209use re 'taint';
8210
8211BEGIN {
8212  import Amavis::Conf qw($db_home $daemon_chroot_dir);
8213  import Amavis::Util qw(untaint ll do_log);
8214}
8215
8216use BerkeleyDB;
8217
8218BEGIN {
8219  use Exporter ();
8220  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
8221  $VERSION = '2.034';
8222  @ISA = qw(Exporter);
8223}
8224
8225# create new databases, then close them (called by the parent process)
8226# (called only if $db_home is nonempty)
8227sub init($) {
8228  my($predelete) = @_;  # delete existing db files first?
8229  my($name) = $db_home;
8230  $name = "$daemon_chroot_dir $name"  if $daemon_chroot_dir ne '';
8231  if ($predelete) {     # delete old database files
8232    local(*DIR); my($f);
8233    opendir(DIR,$db_home) or die "Can't open directory $name: $!";
8234    while (defined($f = readdir(DIR))) {
8235      next  if ($f eq '.' || $f eq '..') && -d _;
8236      if ($f =~ /^(__db\.\d+|(cache-expiry|cache|snmp|nanny)\.db)\z/s) {
8237        $f = untaint($f);
8238        unlink("$db_home/$f") or die "Can't delete file $name/$f: $!";
8239      }
8240    }
8241    closedir(DIR) or die "Can't close directory $name: $!";
8242  }
8243  my($env) = BerkeleyDB::Env->new(-Home=>$db_home, -Mode=>0640,
8244    -Flags=> DB_CREATE | DB_INIT_CDB | DB_INIT_MPOOL);
8245  defined $env or die "BDB bad db env. at $db_home: $BerkeleyDB::Error, $!.";
8246  do_log(0, sprintf("Creating db in %s/; BerkeleyDB %s, libdb %s",
8247                    $name, BerkeleyDB->VERSION, $BerkeleyDB::db_version));
8248  my($dbc) = BerkeleyDB::Hash->new(
8249    -Filename=>'cache.db', -Flags=>DB_CREATE, -Env=>$env );
8250  defined $dbc or die "BDB no dbC: $BerkeleyDB::Error, $!.";
8251  my($dbq) = BerkeleyDB::Queue->new(
8252    -Filename=>'cache-expiry.db', -Flags=>DB_CREATE, -Env=>$env,
8253    -Len=>15+1+32 );  # '-ExtentSize' needs DB 3.2.x, e.g. -ExtentSize=>2
8254  defined $dbq or die "BDB no dbQ: $BerkeleyDB::Error, $!.";
8255  my($dbs) = BerkeleyDB::Hash->new(
8256    -Filename=>'snmp.db', -Flags=>DB_CREATE, -Env=>$env );
8257  defined $dbs or die "BDB no dbS: $BerkeleyDB::Error, $!.";
8258  my($dbn) = BerkeleyDB::Hash->new(
8259    -Filename=>'nanny.db', -Flags=>DB_CREATE, -Env=>$env );
8260  defined $dbn or die "BDB no dbN: $BerkeleyDB::Error, $!.";
8261
8262  Amavis::DB::SNMP::put_initial_snmp_data($dbs);
8263  for my $db ($dbc, $dbq, $dbs, $dbn)
8264    { $db->db_close==0 or die "BDB db_close: $BerkeleyDB::Error, $!." }
8265}
8266
8267# open an existing databases environment (called by each child process)
8268sub new {
8269  my($class) = @_; my($env);
8270  if (defined $db_home) {
8271    $env = BerkeleyDB::Env->new(
8272      -Home=>$db_home, -Mode=>0640, -Flags=> DB_INIT_CDB | DB_INIT_MPOOL);
8273    defined $env or die "BDB bad db env. at $db_home: $BerkeleyDB::Error, $!.";
8274  }
8275  bless \$env, $class;
8276}
8277sub get_db_env { my($self) = shift; $$self }
8278
82791;
8280
8281__DATA__
8282#
8283package Amavis::Cache;
8284# offer an 'IPC::Cache'-compatible interface to a BerkeleyDB-based cache.
8285# Replaces methods new,get,set of the memory-based cache.
8286use strict;
8287use re 'taint';
8288
8289BEGIN {
8290  import Amavis::Util qw(ll do_log);
8291}
8292
8293use BerkeleyDB;
8294
8295BEGIN {
8296  use Exporter ();
8297  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
8298  $VERSION = '2.0342';
8299  @ISA = qw(Exporter);
8300}
8301
8302# open existing databases (called by each child process);
8303# if $db_env is undef a memory-based cache is created, otherwise use BerkeleyDB
8304sub new {
8305  my($class,$db_env) = @_;
8306  my($dbc,$dbq,$mem_cache);
8307  if (!defined($db_env)) {
8308    do_log(1,"BerkeleyDB not available, using memory-based local cache");
8309    $mem_cache = {};
8310  } else {
8311    my($env) = $db_env->get_db_env;
8312    defined $env or die "BDB bad db env.: $BerkeleyDB::Error, $!.";
8313    $dbc = BerkeleyDB::Hash->new(-Filename=>'cache.db', -Env=>$env);
8314    defined $dbc or die "BDB no dbC: $BerkeleyDB::Error, $!.";
8315    $dbq = BerkeleyDB::Queue->new(-Filename=>'cache-expiry.db', -Env=>$env,
8316      -Len=>15+1+32);  # '-ExtentSize' needs DB 3.2.x, e.g. -ExtentSize=>2
8317    defined $dbq or die "BDB no dbQ: $BerkeleyDB::Error, $!.";
8318  }
8319  bless {'db_cache'=>$dbc, 'db_queue'=>$dbq, 'mem_cache'=>$mem_cache}, $class;
8320}
8321
8322sub DESTROY {
8323  my($self) = shift;
8324  eval { do_log(5,"Amavis::Cache called") };
8325  for my $db ($self->{'db_cache'}, $self->{'db_queue'}) {
8326    if (defined $db) {
8327      eval { $db->db_close==0 or die "db_close: $BerkeleyDB::Error, $!." };
8328      if ($@ ne '') { warn "BDB C+Q DESTROY $@" }
8329      $db = undef;
8330    }
8331  }
8332}
8333
8334# purge expired entries from the queue head and enqueue new entry at the tail
8335sub enqueue {
8336  my($self,$str,$now_utc_iso8601,$expires_utc_iso8601) = @_;
8337  my($db) = $self->{'db_cache'};  my($dbq) = $self->{'db_queue'};
8338  local($1,$2); my($stat,$key,$val); $key = '';
8339  my($qcursor) = $dbq->db_cursor(DB_WRITECURSOR);
8340  defined $qcursor or die "BDB Q db_cursor: $BerkeleyDB::Error, $!.";
8341  while ( ($stat=$qcursor->c_get($key,$val,DB_NEXT)) == 0 ) {
8342    if ($val !~ /^([^ ]+) (.*)\z/s) {
8343      do_log(-2,"WARN: queue head invalid, deleting: $val");
8344    } else {
8345      my($t,$digest) = ($1,$2);
8346      last  if $t ge $now_utc_iso8601;
8347      my($cursor) = $db->db_cursor(DB_WRITECURSOR);
8348      defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!.";
8349      my($v); my($st1) = $cursor->c_get($digest,$v,DB_SET);
8350      $st1==0 || $st1==DB_NOTFOUND or die "BDB C c_get: $BerkeleyDB::Error, $!.";
8351      if ($st1==0 && $v=~/^([^ ]+) /s) {  # record exists and appears valid
8352         if ($1 ne $t) {
8353           do_log(5,"enqueue: not deleting: $digest, was refreshed since");
8354         } else {  # its expiration time correspond to timestamp in the queue
8355           do_log(5,"enqueue: deleting: $digest");
8356           my($st2) = $cursor->c_del;     # delete expired entry from the cache
8357           $st2==0 || $st2==DB_KEYEMPTY
8358             or die "BDB C c_del: $BerkeleyDB::Error, $!.";
8359         }
8360      }
8361      $cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!.";
8362    }
8363    my($st3) = $qcursor->c_del;
8364    $st3==0 || $st3==DB_KEYEMPTY or die "BDB Q c_del: $BerkeleyDB::Error, $!.";
8365  }
8366  $stat==0 || $stat==DB_NOTFOUND or die "BDB Q c_get: $BerkeleyDB::Error, $!.";
8367  $qcursor->c_close==0 or die "BDB Q c_close: $BerkeleyDB::Error, $!.";
8368  # insert new expiration request in the queue
8369  $dbq->db_put($key, "$expires_utc_iso8601 $str", DB_APPEND) == 0
8370    or die "BDB Q db_put: $BerkeleyDB::Error, $!.";
8371  # syncing would only be worth doing if we would want the cache to persist
8372  # across restarts - but we scratch the databases to avoid rebuild worries
8373# $stat = $dbq->db_sync();
8374# $stat==0 or warn "BDB Q db_sync, status $stat: $BerkeleyDB::Error, $!.";
8375# $stat = $db->db_sync();
8376# $stat==0 or warn "BDB C db_sync, status $stat: $BerkeleyDB::Error, $!.";
8377}
8378
8379sub get {
8380  my($self,$key) = @_;
8381  my($val); my($db) = $self->{'db_cache'};
8382  if (!defined($db)) {
8383    $val = $self->{'mem_cache'}{$key};  # simple local memory-based cache
8384  } else {
8385    my($stat) = $db->db_get($key,$val);
8386    $stat==0 || $stat==DB_NOTFOUND
8387      or die "BDB C c_get: $BerkeleyDB::Error, $!.";
8388    local($1,$2);
8389    if ($stat==0 && $val=~/^([^ ]+) (.*)/s) { $val = $2 } else { $val = undef }
8390  }
8391  thaw($val);
8392}
8393
8394sub set {
8395  my($self,$key,$obj,$now_utc_iso8601,$expires_utc_iso8601) = @_;
8396  my($db) = $self->{'db_cache'};
8397  if (!defined($db)) {
8398    $self->{'mem_cache'}{$key} = freeze($obj);
8399  } else {
8400    my($cursor) = $db->db_cursor(DB_WRITECURSOR);
8401    defined $cursor or die "BDB C db_cursor: $BerkeleyDB::Error, $!.";
8402    my($val); my($stat) = $cursor->c_get($key,$val,DB_SET);
8403    $stat==0 || $stat==DB_NOTFOUND
8404      or die "BDB C c_get: $BerkeleyDB::Error, $!.";
8405    $cursor->c_put($key, $expires_utc_iso8601.' '.freeze($obj),
8406                   $stat==0 ? DB_CURRENT : DB_KEYLAST ) == 0
8407      or die "BDB C c_put: $BerkeleyDB::Error, $!.";
8408    $cursor->c_close==0 or die "BDB C c_close: $BerkeleyDB::Error, $!.";
8409  # $stat = $db->db_sync();  # only worth doing if cache were persistent
8410  # $stat==0 or warn "BDB C db_sync, status $stat: $BerkeleyDB::Error, $!.";
8411    $self->enqueue($key,$now_utc_iso8601,$expires_utc_iso8601);
8412  }
8413  $obj;
8414}
8415
84161;
8417
8418__DATA__
8419#
8420package Amavis::Lookup::SQLfield;
8421use strict;
8422use re 'taint';
8423
8424BEGIN {
8425  use Exporter ();
8426  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
8427  $VERSION = '2.034';
8428  @ISA = qw(Exporter);
8429}
8430BEGIN { import Amavis::Util qw(ll do_log) }
8431
8432sub new($$$;$$) {
8433  my($class, $sql_query,$fieldname, $fieldtype,$implied_args) = @_;
8434  # fieldtype: B=boolean, N=numeric, S=string,
8435  #            N-: numeric, nonexistent field returns undef without complaint
8436  #            S-: string,  nonexistent field returns undef without complaint
8437  #            B-: boolean, nonexistent field returns undef without complaint
8438  #            B0: boolean, nonexistent field treated as false
8439  #            B1: boolean, nonexistent field treated as true
8440  return undef  if !defined($sql_query);
8441  my($self) = bless {}, $class;
8442  $self->{sql_query} = $sql_query;
8443  $self->{fieldname} = lc($fieldname);
8444  $self->{fieldtype} = uc($fieldtype);
8445  $self->{args} = ref($implied_args) eq 'ARRAY' ? [@$implied_args]  # copy
8446                  : [$implied_args]  if defined $implied_args;
8447  $self;
8448}
8449
8450sub lookup_sql_field($$$) {
8451  my($self,$addr,$get_all) = @_;
8452  my(@result,@matchingkey);
8453  if (!defined($self)) {
8454    do_log(5, "lookup_sql_field - undefined, \"$addr\" no match");
8455  } elsif (!defined($self->{sql_query})) {
8456    do_log(5, sprintf("lookup_sql_field(%s) - null query, \"%s\" no match",
8457                      $self->{fieldname}, $addr));
8458  } else {
8459    my($field) = $self->{fieldname};
8460    my($res_ref,$mk_ref) = $self->{sql_query}->lookup_sql($addr,1,
8461                                  !exists($self->{args}) ? () : $self->{args});
8462    do_log(5, "lookup_sql_field($field), \"$addr\" no matching record")
8463      if !@$res_ref;
8464    for my $ind (0..$#$res_ref) {
8465      my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind];
8466      if (!exists($h_ref->{$field})) {
8467        # record found, but no field with that name in the table
8468        # fieldtype: B0: boolean, nonexistent field treated as false,
8469        #            B1: boolean, nonexistent field treated as true
8470        if (     $self->{fieldtype} =~ /^B0/) {  # boolean, defaults to false
8471          $match = 0;  # nonexistent field treated as 0
8472          do_log(5, "lookup_sql_field($field), no field, \"$addr\" result=$match");
8473        } elsif ($self->{fieldtype} =~ /^B1/) {  # defaults to true
8474          $match = 1;  # nonexistent field treated as 1
8475          do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=$match");
8476        } elsif ($self->{fieldtype}=~/^.-/s) {   # allowed to not exist
8477          do_log(5,"lookup_sql_field($field), no field, \"$addr\" result=undef");
8478        } else {       # treated as 'no match', issue a warning
8479          do_log(1,"lookup_sql_field($field) ".
8480                   "(WARN: no such field in the SQL table), ".
8481                   "\"$addr\" result=undef");
8482        }
8483      } else {  # field exists
8484        # fieldtype: B=boolean, N=numeric, S=string
8485        $match = $h_ref->{$field};
8486        if (!defined($match)) {   # NULL field values represented as undef
8487        } elsif ($self->{fieldtype} =~ /^B/) {  # boolean
8488          # convert values 'N', 'F', '0', ' ' and "\000" to 0
8489          # to allow value to be used directly as a Perl boolean
8490          $match = 0  if $match =~ /^([NnFf ]|0+|\000+)[ ]*\z/;
8491        } elsif ($self->{fieldtype} =~ /^N/) {   # numeric
8492          $match = $match + 0;  # unify different numeric forms
8493        } elsif ($self->{fieldtype} =~ /^S/) {   # string
8494          $match =~ s/ +\z//;   # trim trailing spaces
8495        }
8496        do_log(5, "lookup_sql_field($field) \"$addr\" result=" .
8497                  (defined $match ? $match : 'undef') );
8498      }
8499      if (defined $match) {
8500        push(@result,$match); push(@matchingkey,$mk);
8501        last  if !$get_all;
8502      }
8503    }
8504  }
8505  if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
8506  else           { !wantarray ? \@result   : (\@result,   \@matchingkey)   }
8507}
8508
85091;
8510
8511#
8512package Amavis::Lookup::SQL;
8513use strict;
8514use re 'taint';
8515
8516BEGIN {
8517  use Exporter ();
8518  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
8519  $VERSION = '2.034';
8520  @ISA = qw(Exporter);
8521}
8522
8523use DBI;
8524
8525BEGIN {
8526  import Amavis::Conf qw(:platform :confvars c cr ca);
8527  import Amavis::Timing qw(section_time);
8528  import Amavis::Util qw(untaint snmp_count ll do_log);
8529  import Amavis::rfc2821_2822_Tools qw(make_query_keys);
8530}
8531
8532use vars qw($sql_connected);
8533
8534# Connect to a database.  Take a list of database connection
8535# parameters and try each until one succeeds.
8536#  -- based on code from Ben Ransford <amavis@uce.ransford.org> 2002-09-22
8537sub connect_to_sql(@) {
8538  my(@dsns) = @_;  # a list of DSNs to try connecting to sequentially
8539  my($dbh);
8540  do_log(3,"Connecting to SQL database server");
8541  for my $tmpdsn (@dsns) {
8542    my($dsn, $username, $password) = @$tmpdsn;
8543    do_log(4, "connect_to_sql: trying '$dsn'");
8544    $dbh = DBI->connect($dsn, $username, $password,
8545                        {PrintError => 0, RaiseError => 0, Taint => 1} );
8546    if ($dbh) { do_log(3,"connect_to_sql: '$dsn' succeeded"); last }
8547    do_log(-1,"connect_to_sql: unable to connect to DSN '$dsn': ".$DBI::errstr);
8548  }
8549  do_log(-2,"connect_to_sql: unable to connect to any DSN at all!")
8550    if !$dbh && @dsns > 1;
8551  $sql_connected = 1  if $dbh;
8552  $dbh;
8553}
8554
8555# return a new Lookup::SQL object to contain DBI handle and prepared selects
8556sub new {
8557  my($class) = @_;  bless {}, $class;
8558}
8559
8560# explicitly disconnect from SQL server
8561sub DESTROY {
8562  my($self) = shift;
8563  eval { do_log(5,"Amavis::Lookup::SQL called") };
8564  if (defined $self && $self->{dbh} && $sql_connected) {
8565    $sql_connected = 0;
8566    eval { $self->{dbh}->disconnect }; $self->{dbh} = undef;
8567  }
8568}
8569
8570# store DBI handle and prepared selects into existing Lookup::SQL obj
8571sub store_dbh($$$) {
8572  my($self, $dbh, $select_clause) = @_;
8573  $self->{dbh} = $dbh;  # save DBI handle
8574  $self->{select_clause} = $select_clause;
8575  $self->clear_cache;   # let's start afresh just in case
8576  $self;
8577}
8578
8579sub clear_cache {
8580  my($self) = @_;
8581  delete $self->{cache};
8582}
8583
8584# lookup_sql() performs a lookup for an e-mail address against a SQL map.
8585# If a match is found it returns whatever the map returns (a reference
8586# to a hash containing values of requested fields), otherwise returns undef.
8587# A match aborts further fetching sequence, unless $get_all is true.
8588#
8589# SQL lookups (e.g. for user+foo@example.com) are performed in order
8590# which can be requested by 'ORDER BY' in the SELECT statement, otherwise
8591# the order is unspecified, which is only useful if only specific entries
8592# exist in a database (e.g. only full addresses, not domains).
8593#
8594# The following order is recommended, going from specific to more general:
8595#  - lookup for user+foo@example.com
8596#  - lookup for user@example.com (only if $recipient_delimiter nonempty)
8597#  - lookup for user+foo ('naked lookup': only if local)
8598#  - lookup for user  ('naked lookup': local and $recipient_delimiter nonempty)
8599#  - lookup for @sub.example.com
8600#  - lookup for @.sub.example.com
8601#  - lookup for @.example.com
8602#  - lookup for @.com
8603#  - lookup for @.       (catchall)
8604# NOTE:
8605#  this is different from hash and ACL lookups in two important aspects:
8606#    - a key without '@' implies mailbox (=user) name, not domain name;
8607#    - the naked mailbox name lookups are only performed when the e-mail addr
8608#      (usually its domain part) matches the static local_domains* lookups.
8609#
8610# The domain part is always lowercased when constructing a key,
8611# the localpart is lowercased unless $localpart_is_case_sensitive is true.
8612#
8613sub lookup_sql($$$;$) {
8614  my($self, $addr,$get_all,$extra_args) = @_;
8615  my(@matchingkey,@result);
8616  if (!defined $extra_args &&
8617      exists $self->{cache} && exists $self->{cache}->{$addr})
8618  { # cached ?
8619    my($c) = $self->{cache}->{$addr}; @result = @$c  if ref $c;
8620    @matchingkey = map {'/cached/'} @result; #will do for now, improve some day
8621    if (!ll(5)) {
8622      # don't bother preparing log report which will not be printed
8623    } elsif (!@result) {
8624      do_log(5,"lookup_sql (cached): \"$addr\" no match");
8625    } else {
8626      for my $m (@result) {
8627        do_log(5, sprintf("lookup_sql (cached): \"%s\" matches, result=(%s)",
8628          $addr, join(", ", map { sprintf("%s=>%s", $_,
8629                                  !defined($m->{$_})?'-':'"'.$m->{$_}.'"'
8630                                         ) } sort keys(%$m) ) ));
8631      }
8632    }
8633    if (!$get_all) {
8634      return(!wantarray ? $result[0] : ($result[0], $matchingkey[0]));
8635    } else {
8636      return(!wantarray ? \@result   : (\@result,   \@matchingkey));
8637    }
8638  }
8639  if (!$sql_connected) {
8640    my($sql_dbh) = connect_to_sql(@lookup_sql_dsn);
8641    section_time('sql-connect');
8642    defined($sql_dbh) or die "SQL server(s) not reachable";
8643    $sql_dbh->{'RaiseError'} = 1;
8644    $Amavis::sql_policy->store_dbh($sql_dbh, $sql_select_policy)
8645      if defined $sql_select_policy;
8646    $Amavis::sql_wblist->store_dbh($sql_dbh, $sql_select_white_black_list)
8647      if defined $sql_select_white_black_list;
8648  }
8649  my($is_local);  # $local_domains_sql is not looked up to avoid recursion!
8650  $is_local = Amavis::Lookup::lookup(0,$addr,
8651                                     grep {ref ne 'Amavis::Lookup::SQL' &&
8652                                           ref ne 'Amavis::Lookup::SQLfield' &&
8653                                           ref ne 'Amavis::Lookup::LDAP' &&
8654                                           ref ne 'Amavis::Lookup::LDAPattr'}
8655                                           @{ca('local_domains_maps')});
8656  my($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local);
8657  my($n) = sprintf("%d",scalar(@$keys_ref));  # number of keys
8658  my($sel) = $self->{select_clause};
8659  my(@pos_args);  my(@extras_tmp) = !ref $extra_args ? () : @$extra_args;
8660  $sel =~ s{ ( %k | \? ) }  # substitute %k for keys and ? for each extra arg
8661           { push(@pos_args, map { untaint($_) }
8662                  $1 eq '%k' ? @$keys_ref : shift @extras_tmp),
8663             $1 eq '%k' ? join(',', ('?') x $n) : '?' }gxe;
8664  if (!exists $self->{"sth$n"}) {
8665    # 'prepare' appropriate query only when needed, and save it for reuse
8666    do_log(5,"SQL prepare($n): $sel");
8667    $self->{"sth$n"} = $self->{dbh}->prepare($sel);
8668  }
8669  my($sth) = $self->{"sth$n"};
8670  do_log(4,"lookup_sql \"$addr\", query args: ".
8671           join(', ', map{"\"$_\""} @pos_args));
8672  do_log(4,"lookup_sql select: $sel");
8673  my($a_ref,$found); my($match) = {};
8674  eval {
8675    snmp_count('OpsSqlSelect');
8676    $sth->execute(@pos_args);  # do the query
8677    while ( defined($a_ref=$sth->fetchrow_arrayref) ) {  # fetch query results
8678      my(@names) = @{$sth->{NAME_lc}};
8679      $match = {}; @$match{@names} = @$a_ref;
8680      if (!exists $match->{'local'} && $match->{'email'} eq '@.') {
8681        # UGLY HACK to let a catchall (@.) imply that field 'local' has
8682        # a value undef (NULL) when that field is not present in the
8683        # database. This overrides B1 fieldtype default by an explicit
8684        # undef for '@.', causing a fallback to static lookup tables.
8685        # The purpose is to provide a useful default for local_domains
8686        # lookup if the field 'local' is not present in the SQL table.
8687        # NOTE: field names 'local' and 'email' are hardwired here!!!
8688        push(@names,'local'); $match->{'local'} = undef;
8689        do_log(5, "lookup_sql: \"$addr\" matches catchall, local=>undef");
8690      }
8691      push(@result, {%$match});  # copy hash
8692      push(@matchingkey, join(", ", map { sprintf("%s=>%s", $_,
8693                                !defined($match->{$_})?'-':'"'.$match->{$_}.'"'
8694                                ) } @names));
8695      last  if !$get_all;
8696    }
8697    $sth->finish();
8698  };  # eval
8699  if ($@ ne '') {
8700    my($err) = $@;
8701    do_log(-1, "lookup_sql: $DBI::err, $DBI::errstr");
8702    if (!$sth) {}
8703    elsif ($sth->err eq '2006' || $sth->errstr =~ /\bserver has gone away\b/ ||
8704           $sth->err eq '2013' || $sth->errstr =~ /\bLost connection to\b/) {
8705      do_log(-1,"NOTICE: Disconnected from SQL server");
8706      $sql_connected = 0;  $self->{dbh}->disconnect;  $self->{dbh} = undef;
8707    }
8708    die $err;
8709  }
8710  if (!ll(4)) {
8711    # don't bother preparing log report which will not be printed
8712  } elsif (!@result) {
8713    do_log(4, "lookup_sql, \"$addr\" no match")
8714  } else {
8715    do_log(4, "lookup_sql($addr) matches, result=($_)")  for @matchingkey;
8716  }
8717  # save for future use, but only within processing of this message
8718  $self->{cache}->{$addr} = \@result;
8719  section_time('lookup_sql');
8720  if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
8721  else           { !wantarray ? \@result   : (\@result,   \@matchingkey)   }
8722}
8723
87241;
8725
8726__DATA__
8727#^L
8728package Amavis::Lookup::LDAPattr;
8729
8730use strict;
8731use re 'taint';
8732
8733BEGIN {
8734  use Exporter ();
8735  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
8736  $VERSION = '2.034';
8737  @ISA = qw(Exporter);
8738
8739  import Amavis::Util qw(ll do_log)
8740}
8741
8742# attrtype: B=boolean, N=numeric, S=string, L=list
8743#           N-: numeric, nonexistent field returns undef without complaint
8744#           S-: string,  nonexistent field returns undef without complaint
8745#           L-: list,    nonexistent field returns undef without complaint
8746#           B-: boolean, nonexistent field returns undef without complaint
8747#           B0: boolean, nonexistent field treated as false
8748#           B1: boolean, nonexistent field treated as true
8749
8750sub new($$$;$) {
8751  my($class,$ldap_query,$attrname,$attrtype) = @_;
8752  return undef  if !defined($ldap_query);
8753  my($self) = bless {}, $class;
8754  $self->{ldap_query} = $ldap_query;
8755  $self->{attrname}   = lc($attrname);
8756  $self->{attrtype}   = uc($attrtype);
8757  $self;
8758}
8759
8760sub lookup_ldap_attr($$$) {
8761  my($self,$addr,$get_all) = @_;
8762  my(@result,@matchingkey);
8763  if (!defined($self)) {
8764    do_log(5,"lookup_ldap_attr - undefined, \"$addr\" no match");
8765  } elsif (!defined($self->{ldap_query})) {
8766    do_log(5,sprintf("lookup_ldap_attr(%s) - null query, \"%s\" no match",
8767                      $self->{attrname}, $addr));
8768  } else {
8769    my($attr) = $self->{attrname};
8770    my($res_ref,$mk_ref) = $self->{ldap_query}->lookup_ldap($addr,1);
8771    do_log(5,"lookup_ldap_attr($attr), \"$addr\" no matching record")
8772      if !@$res_ref;
8773    for my $ind (0..$#$res_ref) {
8774      my($match); my($h_ref) = $res_ref->[$ind]; my($mk) = $mk_ref->[$ind];
8775      if (!exists($h_ref->{$attr})) {
8776        # record found, but no attribute with that name in the table
8777        if (     $self->{attrtype} =~ /^B0/) { # boolean, defaults to false
8778          $match = 0; # nonexistent attribute treated as 0
8779          do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match");
8780        } elsif ($self->{attrtype} =~ /^B1/) { # boolean, defaults to true
8781          $match = 1; # nonexistent attribute treated as 1
8782          do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=$match");
8783        } elsif ($self->{attrtype}=~/^.-/s) { # allowed to not exist
8784          do_log(5,"lookup_ldap_attr($attr), no attribute, \"$addr\" result=undef");
8785        } else { # treated as 'no match', issue a warning
8786          do_log(1,"lookup_ldap_attr($attr) ".
8787                   "(WARN: no such attribute in LDAP entry), ".
8788                   "\"$addr\" result=undef");
8789        }
8790      } else { # attribute exists
8791        $match = $h_ref->{$attr};
8792        if (!defined($match)) { # NULL attribute values represented as undef
8793        } elsif ($self->{attrtype} =~ /^B/) { # boolean
8794          $match = $match eq "TRUE" ? 1 : 0; # convert TRUE|FALSE to 1|0
8795        } elsif ($self->{attrtype} =~ /^N/) { # numeric
8796          $match = $match + 0;  # unify different numeric forms
8797        } elsif ($self->{attrtype} =~ /^S/) { # string
8798          $match =~ s/ +\z//;   # trim trailing spaces
8799        } elsif ($self->{attrtype} =~ /^L/) { # list
8800          #$match = join(", ",@$match);
8801        }
8802        do_log(5,sprintf("lookup_ldap_attr(%s) \"%s\" result=(%s)",
8803                  $attr, $addr, defined($match) ? $match : 'undef'));
8804      }
8805      if (defined $match) {
8806        push(@result,$match); push(@matchingkey,$mk);
8807        last  if !$get_all;
8808      }
8809    }
8810  }
8811  if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
8812  else           { !wantarray ? \@result   : (\@result,   \@matchingkey)   }
8813}
8814
88151;
8816
8817#
8818package Amavis::Lookup::LDAP;
8819
8820use strict;
8821use re 'taint';
8822
8823BEGIN {
8824  use Exporter ();
8825  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION
8826              $ldap_sys_default @ldap_attrs);
8827  $VERSION = '2.034';
8828  @ISA = qw(Exporter);
8829
8830  import Amavis::Conf qw(:platform :confvars c cr ca);
8831  import Amavis::Timing qw(section_time);
8832  import Amavis::Util qw(untaint snmp_count ll do_log);
8833  import Amavis::rfc2821_2822_Tools qw(make_query_keys);
8834
8835  $ldap_sys_default = {
8836    hostname      => 'localhost',
8837    port          => 389,
8838    version       => 3,
8839    timeout       => 120,
8840    tls           => 0,
8841    base          => undef,
8842    scope         => 'sub',
8843    query_filter  => '(&(objectClass=amavisAccount)(mail=%m))',
8844    bind_dn       => undef,
8845    bind_password => undef,
8846  };
8847
8848  @ldap_attrs = qw(amavisVirusLover amavisSpamLover amavisBannedFilesLover
8849    amavisBadHeaderLover amavisBypassVirusChecks amavisBypassSpamChecks
8850    amavisBypassBannedChecks amavisBypassHeaderChecks amavisSpamTagLevel
8851    amavisSpamTag2Level amavisSpamKillLevel amavisSpamModifiesSubj
8852    amavisVirusQuarantineTo amavisSpamQuarantineTo amavisBannedQuarantineTo
8853    amavisBadHeaderQuarantineTo amavisBlacklistSender amavisWhitelistSender
8854    amavisLocal amavisMessageSizeLimit mail
8855    amavisWarnVirusRecip amavisWarnBannedRecip amavisWarnBadHeaderRecip
8856  );
8857}
8858
8859use vars qw($ldap_connected);
8860
8861sub new {
8862  my($class,$default) = @_;
8863  my($self) = bless {}, $class;
8864  for (qw(hostname port timeout tls base scope query_filter
8865          bind_dn bind_password)) {
8866    # replace undefined attributes with defaults
8867    $self->{$_} = $default->{$_}          unless defined($self->{$_});
8868    $self->{$_} = $ldap_sys_default->{$_} unless defined($self->{$_});
8869  }
8870  $self;
8871}
8872
8873# explicitly disconnect from LDAP server
8874sub DESTROY {
8875  my($self) = shift;
8876  eval { do_log(5,"Amavis::Lookup::LDAP called") };
8877  if (defined $self && $self->{ldap} && $ldap_connected) {
8878    $ldap_connected = 0;
8879    eval { $self->{ldap}->disconnect }; $self->{ldap} = undef;
8880  }
8881}
8882
8883sub connect_to_ldap {
8884  my($self) = @_;
8885  my($ldap);
8886  do_log(3,"Connecting to LDAP host");
8887  my $hostlist = ref $self->{hostname} eq 'ARRAY' ?
8888                     join(", ",@{$self->{hostname}}) : $self->{hostname};
8889  do_log(4,"connect_to_ldap: trying $hostlist");
8890  $ldap = Net::LDAP->new($self->{hostname},
8891                         port    => $self->{port},
8892                         version => $self->{version},
8893                         timeout => $self->{timeout},
8894                         onerror => 'undef');
8895  if ($ldap) {
8896    do_log(3,"connect_to_ldap: connected to $hostlist");
8897  } else {
8898    do_log(-1,"connect_to_ldap: unable to connect to host $hostlist");
8899    return undef;
8900  }
8901  if ($self->{tls}) {  # TLS required
8902    my $tlsVer = $ldap->start_tls(verify=>'none');
8903    do_log(3,"connect_to_ldap: TLS version $tlsVer enabled");
8904  }
8905  if ($self->{bind_dn}) {  # binding required
8906    if ($ldap->bind($self->{bind_dn}, password => $self->{bind_password})) {
8907      do_log(3,"connect_to_ldap: bind $self->{bind_dn} succeeded");
8908    } else {
8909      do_log(-1,"connect_to_ldap: bind $self->{bind_dn} failed");
8910      return undef;
8911    }
8912  }
8913  $ldap_connected = 1  if $ldap;
8914  $ldap;
8915}
8916
8917sub store_ldap($$) {
8918  my($self,$ldap) = @_;
8919  $self->{ldap} = $ldap; # save LDAP handle
8920  $self->clear_cache;    # let's start afresh just in case
8921  $self;
8922}
8923
8924sub clear_cache {
8925  my($self) = @_;
8926  delete $self->{cache};
8927}
8928
8929sub lookup_ldap($$$) {
8930  my($self,$addr,$get_all) = @_;
8931  my(@matchingkey,@result);
8932  if (exists $self->{cache} && exists $self->{cache}->{$addr}) { # cached?
8933    my($c) = $self->{cache}->{$addr}; @result = @$c if ref $c;
8934    @matchingkey = map {'/cached/'} @result; # will do for now, improve some day
8935    if (!ll(5)) {
8936      # don't bother preparing log report which will not be printed
8937    } elsif (!@result) {
8938      do_log(5,"lookup_ldap (cached): \"$addr\" no match");
8939    } else {
8940      for my $m (@result) {
8941        do_log(5, sprintf("lookup_ldap (cached): \"%s\" matches, result=(%s)",
8942          $addr, join(", ", map { sprintf("%s=>%s", $_,
8943                                  !defined($m->{$_})?'-':'"'.$m->{$_}.'"'
8944                                         ) } sort keys(%$m) ) ));
8945      }
8946    }
8947    if (!$get_all) {
8948      return(!wantarray ? $result[0] : ($result[0], $matchingkey[0]));
8949    } else {
8950      return(!wantarray ? \@result   : (\@result,   \@matchingkey));
8951    }
8952  }
8953  if (!$ldap_connected) {
8954    my($ldap) = $self->connect_to_ldap;
8955    defined($ldap) or die "LDAP server(s) not reachable";
8956    $self->store_ldap($ldap);
8957    section_time('ldap-connect');
8958  }
8959  my($is_local);  # LDAP is not looked up to avoid recursion!
8960  $is_local = Amavis::Lookup::lookup(0,$addr,
8961                                     grep {ref ne 'Amavis::Lookup::SQL' &&
8962                                           ref ne 'Amavis::Lookup::SQLfield' &&
8963                                           ref ne 'Amavis::Lookup::LDAP' &&
8964                                           ref ne 'Amavis::Lookup::LDAPattr'}
8965                                           @{ca('local_domains_maps')});
8966  my($keys_ref,$rhs_ref) = make_query_keys($addr,0,$is_local);
8967  my(@keys) = @$keys_ref;
8968  $_ = untaint($_) for @keys; # untaint keys
8969  do_log(4,sprintf("lookup_ldap \"%s\", query keys: %s, %s",
8970               $addr, join(', ',map{"\"$_\""}@keys), $self->{query_filter}));
8971  my($result);
8972  eval {
8973    snmp_count('OpsLDAPSearch');
8974    for my $key (@keys) {
8975      my($match) = {};
8976      (my $filter = $self->{query_filter}) =~ s/%m/$key/g;
8977      do_log(9,sprintf(
8978        "lookup_ldap: searching base=\"%s\", scope=\"%s\", filter=\"%s\"",
8979         $self->{base}, $self->{scope}, $filter));
8980      $result = $self->{ldap}->search(base   => $self->{base},
8981                                      scope  => $self->{scope},
8982                                      filter => $filter,
8983                                      attrs  => [@ldap_attrs],);
8984      my $entry = $result->entry(0); # only use first record returned
8985      next unless $entry;            # no entry found, try next key
8986      for my $attr (@ldap_attrs) {
8987        my($value);
8988        $attr = lc($attr);
8989        do_log(9,"lookup_ldap: reading attribute \"$attr\" from object");
8990        if ($attr =~ /^amavis(white|black)listsender\z/) { # multivalued (list)
8991          $value = $entry->get_value($attr, asref => 1);
8992        } else {
8993          $value = $entry->get_value($attr);
8994        }
8995        $match->{$attr} = $value if $value;
8996      }
8997      if (!exists $match->{'amavislocal'} && $match->{'mail'} eq '@.') {
8998        # NOTE: see lookup_sql
8999        $match->{'amavislocal'} = undef;
9000        do_log(5,"lookup_ldap: \"$addr\" matches catchall, amavislocal=>undef");
9001      }
9002      push(@result, {%$match}); # copy hash
9003      push(@matchingkey, join(", ", map { sprintf("%s=>%s", $_,
9004           !defined($match->{$_})?'-':'"'.$match->{$_}.'"' ) } keys(%$match)));
9005      last if !$get_all;
9006    }
9007  }; # eval
9008  if ($@ ne '') {
9009    my($err) = $@;
9010    do_log(-1,"lookup_ldap: $err");
9011    if (!$result || $result->code() != 'LDAP_SUCCESS') {
9012      my $code = $result?$result->code():-1;
9013      my $errname = Net::LDAP::Util::ldap_error_name->($code);
9014      if ($errname eq 'LDAP_PARAM_ERROR') {
9015        do_log(-1,"NOTICE: LDAP error - LDAP_PARAM_ERROR");
9016      }
9017      else { # probably lost connection to server
9018        do_log(-1,"NOTICE: Check LDAP server, lost connection?");
9019        $ldap_connected = 0; $self->{ldap}->disconnect; $self->{ldap} = undef;
9020      }
9021    }
9022    die $err;
9023  }
9024  if (!ll(4)) {
9025    # don't bother preparing log report which will not be printed
9026  } elsif (!@result) {
9027    do_log(4,"lookup_ldap, \"$addr\" no match")
9028  } else {
9029    do_log(4,"lookup_ldap($addr) matches, result=($_)") for @matchingkey;
9030  }
9031  # save for future use, but only within processing of this message
9032  $self->{cache}->{$addr} = \@result;
9033  section_time('lookup_ldap');
9034  if (!$get_all) { !wantarray ? $result[0] : ($result[0], $matchingkey[0]) }
9035  else           { !wantarray ? \@result   : (\@result,   \@matchingkey)   }
9036}
9037
90381;
9039
9040__DATA__
9041#
9042package Amavis::In::AMCL;
9043use strict;
9044use re 'taint';
9045
9046BEGIN {
9047  use Exporter ();
9048  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
9049  $VERSION = '2.034';
9050  @ISA = qw(Exporter);
9051}
9052
9053use subs @EXPORT;
9054use Errno qw(ENOENT EACCES);
9055use IO::File ();
9056
9057BEGIN {
9058  import Amavis::Conf qw(:platform :confvars c cr ca);
9059  import Amavis::Util qw(ll do_log debug_oneshot snmp_counters_init snmp_count
9060                         am_id new_am_id untaint rmdir_recursively);
9061  import Amavis::Lookup qw(lookup);
9062  import Amavis::Timing qw(section_time);
9063  import Amavis::rfc2821_2822_Tools;
9064  import Amavis::In::Message;
9065  import Amavis::In::Connection;
9066  import Amavis::Out::EditHeader qw(hdr);
9067  import Amavis::rfc2821_2822_Tools qw(/^EX_/);
9068}
9069
9070sub new($) { my($class) = @_;  bless {}, $class }
9071
9072# (used with sendmail milter and traditional (non-SMTP) MTA interface)
9073#
9074sub process_policy_request($$$$) {
9075  my($self, $sock, $conn, $check_mail, $old_amcl) = @_;
9076  # $sock:       connected socket from Net::Server
9077  # $conn:       information about client connection
9078  # $check_mail: subroutine ref to be called with file handle
9079
9080  my(%attr);
9081  $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
9082  if ($old_amcl) {
9083    # Accept a single request from traditional amavis helper program.
9084    # Receive TEMPDIR/SENDER/RCPTS/LDA/LDAARGS from client
9085    # Simple protocol: \2 means LDA follows; \3 means EOT (end of transmission)
9086    my($state) = 0; $attr{'request'} = 'AM.CL'; my($response) = "\001";
9087    my($rv,@recips,@ldaargs,$inbuff); local($1);
9088    my(@attr_names) = qw(tempdir sender recipient ldaargs);
9089    while (defined($rv = recv($sock, $inbuff, 8192, 0))) {
9090      $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
9091      if ($state < 2) {
9092        $attr{$attr_names[$state]} = $inbuff; $state++;
9093      } elsif ($state == 2 && $inbuff eq "\002") {
9094        $state++;
9095      } elsif ($state >= 2 && $inbuff eq "\003") {
9096        section_time('got data');
9097        $attr{'recipient'} = \@recips; $attr{'ldaargs'} = \@ldaargs;
9098        $attr{'delivery_care_of'} = @ldaargs ? 'client' : 'server';
9099        eval {
9100          my($msginfo) = preprocess_policy_query(\%attr);
9101          $response = (map { /^exit_code=(\d+)\z/ ? $1 : () }
9102                           check_amcl_policy($conn,$msginfo,$check_mail,1))[0];
9103        };
9104        if ($@ ne '') {
9105          chomp($@); do_log(-2,"policy_server FAILED: $@");
9106          $response = EX_TEMPFAIL;
9107        }
9108        $state = 4;
9109      } elsif ($state == 2) {
9110        push(@recips, $inbuff);
9111      } else {
9112        push(@ldaargs, $inbuff);
9113      }
9114      defined(send($sock,$response,0))
9115        or die "send failed in state $state: $!";
9116      last  if $state >= 4;
9117      $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
9118    }
9119    if ($state==4 && defined($rv)) {
9120      # normal termination
9121    } elsif (!defined($rv) && $! != 0) {
9122      die "recv failed in state $state: $!";
9123    } else {  # eof or a runaway state
9124      die "helper client session terminated unexpectedly, state: $state";
9125    }
9126    do_log(2, Amavis::Timing::report());  # report elapsed times
9127
9128  } else {  # new amavis helper protocol AM.PDP or a Postfix policy server
9129    # for Postfix policy server see Postfix docs SMTPD_POLICY_README
9130    my(@response); local($1,$2,$3);
9131    local($/) = "\012";  # set line terminator to LF (Postfix idiosyncrasy)
9132    $! = undef;
9133    while(<$sock>) {     # can accept multiple tasks
9134      $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
9135      Amavis::Timing::init(); snmp_counters_init();
9136      # must not use \r and \n, not \015 and \012 on certain platforms
9137      if (/^\015?\012\z/) {  # end of request
9138        section_time('got data');
9139        eval {
9140          my($msginfo) = preprocess_policy_query(\%attr);
9141          @response = $attr{'request'} eq 'smtpd_access_policy'
9142                        ? postfix_policy($conn,$msginfo,\%attr)
9143                        : check_amcl_policy($conn,$msginfo,$check_mail,0);
9144        };
9145        if ($@ ne '') {
9146          chomp($@); do_log(-2, "policy_server FAILED: $@");
9147          @response = (proto_encode('setreply','450','4.5.0',"Failure: $@"),
9148                       proto_encode('return_value','tempfail'),
9149                       proto_encode('exit_code',sprintf("%d",EX_TEMPFAIL)));
9150        # last;
9151        }
9152        $sock->print( map { $_."\015\012" } (@response,'') )
9153          or die "Can't write response to socket: $!";
9154        %attr = (); @response = ();
9155        do_log(2, Amavis::Timing::report());
9156      } elsif (/^ ([^=\000\012]*?) (=|:[ \t]*)
9157                  ([^\012]*?) \015?\012 \z/xsi) {
9158        my($attr_name) = Amavis::tcp_lookup_decode($1);
9159        my($attr_val)  = Amavis::tcp_lookup_decode($3);
9160        if (!exists $attr{$attr_name}) {
9161          $attr{$attr_name} = $attr_val;
9162        } else {
9163          if (!ref($attr{$attr_name}))
9164            { $attr{$attr_name} = [ $attr{$attr_name} ] }
9165          push(@{$attr{$attr_name}}, $attr_val);
9166        }
9167        my($known_attr) = scalar(grep {$_ eq $attr_name} qw(
9168          request helo_name protocol_state protocol_name queue_id
9169          client_name client_address sender recipient) );
9170        do_log(!$known_attr?-1:1, "policy protocol: $attr_name=$attr_val");
9171      } else {
9172        do_log(-1, "policy protocol: INVALID ATTRIBUTE LINE: $_");
9173      }
9174      $0 = sprintf("amavisd (ch%d-P-idle)", $Amavis::child_invocation_count);
9175      $! = undef;  # we'll be interested in the errno from the I/O op in while
9176    }
9177    if (!defined($_) && $! != 0) { die "read from client socket FAILED: $!" }
9178  };
9179  $0 = sprintf("amavisd (ch%d-P)", $Amavis::child_invocation_count);
9180}
9181
9182# Return a new Amavis::In::Message object
9183# based on given policy query attributes
9184#
9185sub preprocess_policy_query($) {
9186  my($attr_ref) = @_;
9187
9188  my($msginfo) = Amavis::In::Message->new;
9189  $msginfo->rx_time(time);  # now
9190
9191  # amavisd -> amavis-helper protocol query consists of any number of
9192  # the following lines, the response is terminated by an empty line.
9193  # The 'request=AM.PDP' is a required first field, the order of
9194  # remaining fields is arbitrary.
9195  # Required fields are: request, tempdir, sender, recipient (one or more)
9196  #   request=AM.PDP
9197  #   tempdir=/var/amavis/amavis-milter-MWZmu9Di
9198  #   tempdir_removed_by=client    (tempdir_removed_by=server is a default)
9199  #   mail_file=/var/amavis/am.../email.txt (defaults to tempdir/email.txt)
9200  #   sender=<foo@example.com>
9201  #   recipient=<bar1@example.net>
9202  #   recipient=<bar2@example.net>
9203  #   recipient=<bar3@example.net>
9204  #   delivery_care_of=server      (client or server, client is a default)
9205  #   queue_id=qid
9206  #   protocol_name=ESMTP
9207  #   helo_name=b.example.com
9208  #   client_address=10.2.3.4
9209
9210  my($sender,@recips);
9211  $msginfo->delivery_method(
9212    lc($attr_ref->{'delivery_care_of'}) eq 'server' ? c('forward_method') :'');
9213  $msginfo->client_delete(lc($attr_ref->{'tempdir_removed_by'}) eq 'client'
9214                          ? 1 : 0);
9215  $msginfo->queue_id($attr_ref->{'queue_id'})
9216    if exists $attr_ref->{'queue_id'};
9217  $msginfo->client_addr($attr_ref->{'client_address'})
9218    if exists $attr_ref->{'client_address'};
9219  $msginfo->client_name($attr_ref->{'client_name'})
9220    if exists $attr_ref->{'client_name'};
9221  $msginfo->client_proto($attr_ref->{'protocol_name'})
9222    if exists $attr_ref->{'protocol_name'};
9223  $msginfo->client_helo($attr_ref->{'helo_name'})
9224    if exists $attr_ref->{'helo_name'};
9225  if (exists $attr_ref->{'sender'}) {
9226    $sender = $attr_ref->{'sender'};
9227    $sender = unquote_rfc2821_local($sender);
9228    $msginfo->sender($sender);
9229  }
9230  if (exists $attr_ref->{'recipient'}) {
9231    my($r) = $attr_ref->{'recipient'};
9232    @recips = !ref($r) ? $r : @$r;
9233    $_ = unquote_rfc2821_local($_)  for @recips;
9234    $msginfo->recips(\@recips);
9235  }
9236  if (!exists $attr_ref->{'tempdir'}) {
9237    $msginfo->mail_tempdir($TEMPBASE);  # defaults to $TEMPBASE
9238  } else {
9239    local($1,$2); my($tempdir) = $attr_ref->{tempdir};
9240    $tempdir =~ /^ (?: \Q$TEMPBASE\E | \Q$MYHOME\E )
9241                   \/ (?! \.{1,2} \z) [A-Za-z0-9_.-]+ \z/xso
9242      or die "Invalid/unexpected temporary directory name '$tempdir'";
9243    $msginfo->mail_tempdir(untaint($tempdir));
9244  }
9245  if (!exists $attr_ref->{'mail_file'}) {
9246    $msginfo->mail_text_fn($msginfo->mail_tempdir . '/email.txt');
9247  } else {
9248    # SECURITY: just believe the supplied file name, blindly untainting it
9249    $msginfo->mail_text_fn(untaint($attr_ref->{'mail_file'}));
9250  }
9251  if ($msginfo->mail_text_fn ne '') {
9252    my($fname) = $msginfo->mail_text_fn;
9253    do_log(5, "preprocess_policy_query: opening mail file '$fname'");
9254    # set new amavis message id
9255    new_am_id( ($fname =~ m{amavis-(milter-)?([^/]+?)\z}s ? $2 : undef) );
9256    # file created by amavis helper program, just open it
9257    my($fh) = IO::File->new;
9258    $fh->open($fname,'<') or die "Can't open file $fname: $!";
9259    binmode($fh,":bytes") or die "Can't cancel :utf8 mode: $!"
9260      if $unicode_aware;
9261    $msginfo->mail_text($fh);  # save file handle to object
9262  }
9263  if ($attr_ref->{'request'} =~ /^AM\.(CL|PDP)\z/) {
9264    do_log(1, sprintf("%s %s: <%s> -> %s",
9265                      $attr_ref->{'request'}, $msginfo->mail_tempdir, $sender,
9266                      join(',', qquote_rfc2821_local(@recips)) ));
9267  } else {
9268    do_log(1, sprintf("%s(%s): %s %s: %s[%s] <%s> -> <%s>",
9269                    @$attr_ref{qw(request protocol_state protocol_name queue_id
9270                    client_name client_address sender recipient)}));
9271  }
9272  $msginfo;
9273}
9274
9275sub check_amcl_policy($$$$) {
9276  my($conn,$msginfo,$check_mail,$old_amcl) = @_;
9277
9278  my($smtp_resp, $exit_code, $preserve_evidence);
9279  my(%baseline_policy_bank); my($policy_changed) = 0;
9280  %baseline_policy_bank = %current_policy_bank;
9281  # do some sanity checks before deciding to call check_mail()
9282  if (!ref($msginfo->per_recip_data) || !defined($msginfo->mail_text)) {
9283    $smtp_resp = '450 4.5.0 Incomplete request'; $exit_code = EX_TEMPFAIL;
9284  } else {
9285    my($cl_ip) = $msginfo->client_addr;
9286    if ($cl_ip ne '' && defined $policy_bank{'MYNETS'}
9287        && lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) {
9288      Amavis::load_policy_bank('MYNETS'); $policy_changed = 1;
9289    }
9290    debug_oneshot(1)  if lookup(0,$msginfo->sender,@{ca('debug_sender_maps')});
9291    # check_mail() expects open file on $fh, need not be rewound
9292    my($fh) = $msginfo->mail_text;  my($tempdir) = $msginfo->mail_tempdir;
9293    ($smtp_resp, $exit_code, $preserve_evidence) =
9294      &$check_mail($conn,$msginfo,0,$tempdir);
9295    $fh->close or die "Can't close temp file: $!"   if $fh;
9296    $fh = undef; $msginfo->mail_text(undef);
9297    my($errn) = $tempdir eq '' ? ENOENT : (stat($tempdir) ? 0 : 0+$!);
9298    if ($tempdir eq '' || $errn == ENOENT) {
9299      # do nothing
9300    } elsif ($msginfo->client_delete) {
9301      do_log(4, "AM.PDP: deletion of $tempdir is client's responsibility");
9302    } elsif ($preserve_evidence) {
9303      do_log(-1,"AM.PDP: tempdir is to be PRESERVED: $tempdir");
9304    } else {
9305      do_log(4, "AM.PDP: tempdir being removed: $tempdir");
9306      my($fname) = $msginfo->mail_text_fn;
9307      unlink($fname) or die "Can't remove file $fname: $!"  if $fname ne '';
9308      rmdir_recursively($tempdir);
9309    }
9310  }
9311  # amavisd -> amavis-helper protocol response consists of any number of
9312  # the following lines, the response is terminated by an empty line
9313  #   addrcpt=recipient
9314  #   delrcpt=recipient
9315  #   addheader=hdr_head hdr_body
9316  #   chgheader=index hdr_head hdr_body
9317  #   delheader=index hdr_head
9318  #   replacebody=new_body  (not implemented)
9319  #   return_value=continue|reject|discard|accept|tempfail
9320  #   setreply=rcode xcode message
9321  #   exit_code=n           (old amavis helper, not applicable to milter)
9322
9323  my(@response); my($rcpt_deletes,$rcpt_count)=(0,0);
9324  if (ref($msginfo->per_recip_data)) {
9325    for my $r (@{$msginfo->per_recip_data})
9326      { $rcpt_count++;  if ($r->recip_done) { $rcpt_deletes++ } }
9327  }
9328  if ($smtp_resp=~/^([1-5]\d\d) ([245]\.\d{1,3}\.\d{1,3})(?: |\z)(.*)\z/s)
9329    { push(@response, proto_encode('setreply', $1,$2,$3)) }
9330  if (     $exit_code == EX_TEMPFAIL) {
9331    push(@response, proto_encode('return_value','tempfail'));
9332  } elsif ($exit_code == EX_NOUSER) {          # reject the whole message
9333    push(@response, proto_encode('return_value','reject'));
9334  } elsif ($exit_code == EX_UNAVAILABLE) {     # reject the whole message
9335    push(@response, proto_encode('return_value','reject'));
9336  } elsif ($exit_code == 99) {                 # discard the whole message
9337    push(@response, proto_encode('return_value','discard'));
9338  } elsif ($msginfo->delivery_method ne '') {  # explicit forwarding by server
9339    $rcpt_count==$rcpt_deletes or die "Not all recips done";  # just in case
9340    # MTA is relieved of duty to deliver a message, amavisd did the forwarding
9341    $exit_code = EX_OK; # *** 99 or EX_OK; ??? (doesn't really matter with
9342                        # helper client programs which can't do the delivery)
9343    push(@response, proto_encode('return_value','continue')); # 'discard' ???
9344  } elsif ($rcpt_count-$rcpt_deletes <= 0) {   # none left, should be discarded
9345    # discarding could have been requested (?)
9346    do_log(-1, "WARN: no recips left (forgot to set ".
9347               "\$forward_method=undef using milter?), $smtp_resp");
9348    $exit_code = 99;
9349    push(@response, proto_encode('return_value','discard'));
9350  } else {  # EX_OK
9351    for my $r (@{$msginfo->per_recip_data}) {  # modified recipient addresses?
9352      my($addr,$newaddr) = ($r->recip_addr, $r->recip_final_addr);
9353      if ($r->recip_done) {          # delete
9354        push(@response, proto_encode('delrcpt',
9355                                     quote_rfc2821_local($addr)));
9356      } elsif ($newaddr ne $addr) {  # modify, e.g. adding extension
9357        push(@response, proto_encode('delrcpt',
9358                                     quote_rfc2821_local($addr)));
9359        push(@response, proto_encode('addrcpt',
9360                                     quote_rfc2821_local($newaddr)));
9361      }
9362    }
9363    my($hdr_edits) = $msginfo->header_edits;
9364    if ($hdr_edits) {  # any added or modified header fields?
9365      local($1,$2);
9366      # Inserting. Not posible to specify placement of header fields in milter!
9367      for my $hf (@{$hdr_edits->{prepend}}, @{$hdr_edits->{append}}) {
9368        if ($hf =~ /^([^:]+):[ \t]*(.*?)$/s)
9369          { push(@response, proto_encode('addheader',$1,$2)) }
9370      }
9371      my($field_name,$edit,$field_body);
9372      while ( ($field_name,$edit) = each %{$hdr_edits->{edit}} ) {
9373        $field_body = $msginfo->mime_entity->head->get($field_name,0);
9374        if (!defined($field_body)) {
9375          # such header field does not exist, do nothing
9376        } elsif (!defined($edit)) {  # delete existing header field
9377          push(@response, proto_encode('delheader',"1",$field_name));
9378        } else {                     # edit the first occurrence
9379          chomp($field_body);
9380          $field_body = hdr($field_name, &$edit($field_name,$field_body));
9381          $field_body = $1  if $field_body =~ /^[^:]+:[ \t]*(.*?)$/s;
9382          push(@response, proto_encode('chgheader', "1",
9383                                       $field_name, $field_body));
9384        }
9385      }
9386    }
9387    if ($old_amcl) {   # milter via old amavis helper program
9388      # warn if there is anything that should be done but MTA is not capable of
9389      # (or a helper program can not pass the request)
9390      for (grep { /^(delrcpt|addrcpt)=/ } @response)
9391        { do_log(-1, "WARN: MTA can't do: $_") }
9392      if ($rcpt_deletes && $rcpt_count-$rcpt_deletes > 0) {
9393        do_log(-1, "WARN: ACCEPT THE WHOLE MESSAGE, ".
9394                   "MTA-in can't do selective recips deletion");
9395      }
9396    }
9397    push(@response, proto_encode('return_value','continue'));
9398  }
9399  push(@response, proto_encode('exit_code',"$exit_code"));
9400  do_log(2, "mail checking ended: ".join("\n",@response));
9401  if ($policy_changed) {
9402    %current_policy_bank = %baseline_policy_bank; $policy_changed = 0;
9403  }
9404  @response;
9405}
9406
9407sub postfix_policy($$$) {
9408  my($conn,$msginfo,$attr_ref) = @_;
9409  my(@response);
9410  if (!exists($attr_ref->{'request'})) {
9411    die "no 'request' attribute";
9412  } elsif ($attr_ref->{'request'} ne 'smtpd_access_policy') {
9413    die ("unknown 'request' value: " . $attr_ref->{'request'});
9414  } else {
9415    @response = 'action=DUNNO';
9416  }
9417  @response;
9418}
9419
9420sub proto_encode($@) {
9421  my($attribute_name,@strings) = @_; local($1);
9422  $attribute_name =~    # encode all but alfanumerics, '_' and '-'
9423    s/([^0-9a-zA-Z_-])/sprintf("%%%02x",ord($1))/eg;
9424  for (@strings) {      # encode % and nonprintables
9425    s/([^\041-\044\046-\176])/sprintf("%%%02x",ord($1))/eg;
9426  }
9427  ## encode % and nonprintables, but leave SP and TAB as-is
9428  # $str =~ s/[^\011\040-\044\046-\176]/sprintf("%%%02x",ord($&))/eg;
9429  $attribute_name . '=' . join(' ',@strings);
9430}
9431
94321;
9433
9434__DATA__
9435#
9436package Amavis::In::SMTP;
9437use strict;
9438use re 'taint';
9439
9440BEGIN {
9441  use Exporter ();
9442  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
9443  $VERSION = '2.034';
9444  @ISA = qw(Exporter);
9445}
9446use Errno qw(ENOENT EACCES);
9447use MIME::Base64;
9448
9449BEGIN {
9450  import Amavis::Conf qw(:platform :confvars c cr ca);
9451  import Amavis::Util qw(ll do_log am_id new_am_id snmp_counters_init
9452                         prolong_timer debug_oneshot sanitize_str
9453                         strip_tempdir rmdir_recursively);
9454  import Amavis::Lookup qw(lookup lookup_ip_acl);
9455  import Amavis::Timing qw(section_time);
9456  import Amavis::rfc2821_2822_Tools;
9457  import Amavis::In::Message;
9458  import Amavis::In::Connection;
9459}
9460
9461sub new($) {
9462  my($class) = @_;
9463  my($self) = bless {}, $class;
9464  $self->{sock} = undef;              # SMTP socket
9465  $self->{proto} = undef;             # SMTP / ((ESMTP / LMTP) (A | S | SA)? )
9466  $self->{pipelining}  = undef;       # may we buffer responses?
9467  $self->{smtp_outbuf} = undef;       # SMTP responses buffer for PIPELINING
9468  $self->{fh_pers} = undef;           # persistent file handle for email.txt
9469  $self->{tempdir_persistent} = undef;# temporary directory for check_mail
9470  $self->{preserve} = undef;          # don't delete tempdir on exit
9471  $self->{tempdir_empty} = 1;         # anything of interest in tempdir?
9472  $self->{session_closed_normally} = undef; # closed properly with QUIT
9473  $self;
9474}
9475
9476sub preserve_evidence  # try to preserve temporary files etc in case of trouble
9477  { my($self)=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift) }
9478
9479sub DESTROY {
9480  my($self) = shift;
9481  eval { do_log(5,"Amavis::In::SMTP::DESTROY called") };
9482  eval {
9483    $self->{fh_pers}->close
9484      or die "Can't close temp file: $!"  if $self->{fh_pers};
9485    my($errn) = $self->{tempdir_pers} eq '' ? ENOENT
9486                  : (stat($self->{tempdir_pers}) ? 0 : 0+$!);
9487    if (defined $self->{tempdir_pers} && $errn != ENOENT) {
9488      # this will not be included in the TIMING report,
9489      # but it only occurs infrequently and doesn't take that long
9490      if ($self->preserve_evidence && !$self->{tempdir_empty}) {
9491        do_log(-1,"SMTP shutdown: tempdir is to be PRESERVED: ".
9492                  $self->{tempdir_pers});
9493      } else {
9494        do_log(3, sprintf("SMTP shutdown: %s is being removed: %s%s",
9495                  $self->{tempdir_empty} ? 'empty tempdir' : 'tempdir',
9496                  $self->{tempdir_pers},
9497                  $self->preserve_evidence ? ', nothing to preserve' : ''));
9498        rmdir_recursively($self->{tempdir_pers});
9499      }
9500    }
9501    if (! $self->{session_closed_normally}) {
9502      $self->smtp_resp(1,"421 4.3.2 Service shutting down, closing channel");
9503    }
9504  };
9505  if ($@ ne '')
9506    { my($eval_stat) = $@; eval { do_log(1,"SMTP shutdown: $eval_stat") } }
9507}
9508
9509sub prepare_tempdir($) {
9510  my($self) = @_;
9511  if (! defined $self->{tempdir_pers} ) {
9512    # invent a name for a temporary directory for this child, and create it
9513    my($now_iso8601) = iso8601_timestamp(time,1);  # or: iso8601_utc_timestamp
9514    $self->{tempdir_pers} = sprintf("%s/amavis-%s-%05d",
9515                                    $TEMPBASE, $now_iso8601, $$);
9516  }
9517  my($dname) = $self->{tempdir_pers};
9518  my(@stat_list) = lstat($dname); my($errn) = @stat_list ? 0 : 0+$!;
9519  if (!$errn && ! -d _) {  # exists, but is not a directory !?
9520    die "prepare_tempdir: $dname is not a directory!!!";
9521  } elsif (!$errn) {
9522    my($dev,$ino) = @stat_list;
9523    if ($dev != $self->{tempdir_dev} || $ino != $self->{tempdir_ino}) {
9524      do_log(-1,"prepare_tempdir: $dname is no longer the same directory!!!");
9525      ($self->{tempdir_dev},$self->{tempdir_ino}) = @stat_list;
9526    }
9527  } elsif ($errn == ENOENT) {
9528    do_log(4,"prepare_tempdir: creating directory $dname");
9529    mkdir($dname,0750) or die "Can't create directory $dname: $!";
9530    ($self->{tempdir_dev},$self->{tempdir_ino}) = lstat($dname);
9531    $self->{tempdir_empty} = 1;
9532    section_time('mkdir tempdir');
9533  }
9534  # prepare temporary file for writing (and reading later)
9535  my($fname) = $dname . '/email.txt';
9536  @stat_list = lstat($fname); $errn = @stat_list ? 0 : 0+$!;
9537  if (!$errn && ! -f _) {  # exists, but is not a regular file !?
9538    die "prepare_tempdir: $fname is not a regular file!!!";
9539  } elsif ($self->{fh_pers} && !$errn && -f _) {
9540    my($dev,$ino) = @stat_list;
9541    if ($dev != $self->{file_dev} || $ino != $self->{file_ino}) {
9542      do_log(-1,"prepare_tempdir: $fname is no longer the same file!!!");
9543      ($self->{file_dev}, $self->{file_ino}) = @stat_list;
9544    }
9545    $self->{fh_pers}->seek(0,0) or die "Can't rewind mail file: $!";
9546    $self->{fh_pers}->truncate(0) or die "Can't truncate mail file: $!";
9547  } else {
9548    do_log(4,"prepare_tempdir: creating file $fname");
9549    $self->{fh_pers} = IO::File->new($fname,'+>',0640)
9550      or die "Can't create file $fname: $!";
9551    ($self->{file_dev}, $self->{file_ino}) = lstat($fname);
9552    section_time('create email.txt');
9553  }
9554}
9555
9556sub authenticate($$$) {
9557  my($state,$auth_mech,$auth_resp) = @_;
9558  my($result,$newchallenge);
9559  if ($auth_mech eq 'ANONYMOUS') {   # rfc2245
9560    $result = [$auth_resp,undef];
9561  } elsif ($auth_mech eq 'PLAIN') {  # rfc2595, "user\0authname\0pass"
9562    if (!defined($auth_resp)) { $newchallenge = '' }
9563    else { $result = [ (split(/\000/,$auth_resp,-1))[0,2] ] }
9564  } elsif ($auth_mech eq 'LOGIN' && !defined $state) {
9565    $newchallenge = 'Username:'; $state = [];
9566  } elsif ($auth_mech eq 'LOGIN' && @$state==0) {
9567    push(@$state, $auth_resp); $newchallenge = 'Password:';
9568  } elsif ($auth_mech eq 'LOGIN' && @$state==1) {
9569    push(@$state, $auth_resp); $result = $state;
9570  } # CRAM-MD5:rfc2195,  DIGEST-MD5:rfc2831
9571    ($state,$result,$newchallenge);
9572}
9573
9574# Accept a SMTP or LMTP connect (which can do any number of SMTP transactions)
9575# and call content checking for each message received
9576#
9577sub process_smtp_request($$$$) {
9578  my($self, $sock, $lmtp, $conn, $check_mail) = @_;
9579  # $sock:       connected socket from Net::Server
9580  # $lmtp:       use LMTP protocol instead of (E)SMTP
9581  # $conn:       information about client connection
9582  # $check_mail: subroutine ref to be called with file handle
9583
9584  my($msginfo,$authenticated,$auth_user,$auth_pass);
9585  $self->{sock} = $sock;
9586  $self->{pipelining} = 0;    # may we buffer responses?
9587  $self->{smtp_outbuf} = [];  # SMTP responses buffer for PIPELINING
9588
9589  my($myheloname);
9590# $myheloname = $myhostname;
9591# $myheloname = 'localhost';
9592# $myheloname = '[127.0.0.1]';
9593  $myheloname = '[' . $conn->socket_ip . ']';
9594
9595  new_am_id(undef, $Amavis::child_invocation_count, undef);
9596  my($initial_am_id) = 1; my($sender,@recips); my($got_rcpt);
9597  my($max_recip_size_limit);  # maximum of per-recipient message size limits
9598  my($terminating,$aborting,$eof,$voluntary_exit); my($seq) = 0;
9599  my(%xforward_args); my(%baseline_policy_bank); my($policy_changed);
9600  %baseline_policy_bank = %current_policy_bank; $policy_changed = 0;
9601  $conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'SMTP');
9602
9603  # system-wide message size limit, if any
9604  my($message_size_limit) = c('smtpd_message_size_limit');
9605  if ($message_size_limit && $message_size_limit < 65536)
9606    { $message_size_limit = 65536 }   # rfc2821 requires at least 64k
9607  my($smtpd_greeting_banner_tmp) = c('smtpd_greeting_banner');
9608  $smtpd_greeting_banner_tmp =~
9609    s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) }
9610     { { 'helo-name'    => $myheloname,
9611         'version'      => $myversion,
9612         'version-id'   => $myversion_id,
9613         'version-date' => $myversion_date,
9614         'product'      => $myproduct_name,
9615         'protocol'     => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
9616     }egx;
9617  $self->smtp_resp(1, "220 $smtpd_greeting_banner_tmp");
9618
9619  $0 = sprintf("amavisd (ch%d-idle)", $Amavis::child_invocation_count);
9620  Amavis::Timing::go_idle(4);
9621  undef $!;
9622  while(<$sock>) {
9623    $0 = sprintf("amavisd (ch%d-%s)",
9624                 $Amavis::child_invocation_count, am_id());
9625    Amavis::Timing::go_busy(5);
9626    prolong_timer('reading SMTP command');
9627    { # a block is used as a 'switch' statement - 'last' will exit from it
9628      my($cmd) = $_;
9629      do_log(4, $self->{proto} . "< $cmd");
9630      !/^ \s* ([A-Za-z]+) (?: \s+ (.*?) )? \s* \015\012 \z/xs && do {
9631        $self->smtp_resp(1,"500 5.5.2 Error: bad syntax", 1, $cmd); last;
9632      };
9633      $_ = uc($1); my($args) = $2;
9634      /^RSET|DATA|QUIT\z/ && $args ne '' && do {
9635        $self->smtp_resp(1,"501 5.5.4 Error: $_ does not accept arguments",
9636                         1,$cmd);
9637        last;
9638      };
9639      /^RSET\z/ && do { $sender = undef; @recips = (); $got_rcpt = 0;
9640                        $max_recip_size_limit = undef; $msginfo = undef;
9641                        if ($policy_changed) {
9642                          %current_policy_bank = %baseline_policy_bank;
9643                          $policy_changed = 0;
9644                        }
9645                        $self->smtp_resp(0,"250 2.0.0 Ok $_"); last };
9646      /^NOOP\z/ && do { $self->smtp_resp(1,"250 2.0.0 Ok $_"); last };
9647      /^QUIT\z/ && do {
9648        my($smtpd_quit_banner_tmp) = c('smtpd_quit_banner');
9649        $smtpd_quit_banner_tmp =~
9650          s{ \$ (?: \{ ([^\}]*) \} | ([a-zA-Z0-9_-]+) ) }
9651           { { 'helo-name'    => $myheloname,
9652               'version'      => $myversion,
9653               'version-id'   => $myversion_id,
9654               'version-date' => $myversion_date,
9655               'product'      => $myproduct_name,
9656               'protocol'     => $lmtp?'LMTP':'ESMTP' }->{lc($1.$2)}
9657           }egx;
9658        $self->smtp_resp(1,"221 2.0.0 $smtpd_quit_banner_tmp");
9659        $terminating=1; last;
9660      };
9661###   !$lmtp && /^HELO\z/ && do {  # strict
9662      /^HELO\z/ && do {
9663        $sender = undef; @recips = (); $got_rcpt = 0;     # implies RSET
9664        $max_recip_size_limit = undef; $msginfo = undef;  # forget previous
9665        if ($policy_changed)
9666          { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
9667        $self->{pipelining} = 0; $self->smtp_resp(0,"250 $myheloname");
9668        $lmtp = 0; $conn->smtp_proto($self->{proto} = 'SMTP');
9669        $conn->smtp_helo($args); section_time('SMTP HELO'); last;
9670      };
9671###   (!$lmtp && /^EHLO\z/ || $lmtp && /^LHLO\z/) && do {  # strict
9672      (/^EHLO\z/ || /^LHLO\z/) && do {
9673        $sender = undef; @recips = (); $got_rcpt = 0;     # implies RSET
9674        $max_recip_size_limit = undef; $msginfo = undef;  # forget previous
9675        if ($policy_changed)
9676          { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
9677        $lmtp = /^LHLO\z/ ? 1 : 0;
9678        $conn->smtp_proto($self->{proto} = $lmtp ? 'LMTP' : 'ESMTP');
9679        $self->{pipelining} = 1;
9680        $self->smtp_resp(0,"250 $myheloname\n" . join("\n",
9681          'PIPELINING',
9682          !defined($message_size_limit) ? 'SIZE'
9683            : sprintf('SIZE %d',$message_size_limit),
9684          '8BITMIME',
9685          'ENHANCEDSTATUSCODES',
9686          !@{ca('auth_mech_avail')} ? ()
9687                                   : join(' ','AUTH',@{ca('auth_mech_avail')}),
9688          'XFORWARD NAME ADDR PROTO HELO' ));
9689        $conn->smtp_helo($args); section_time("SMTP $_");
9690        last;
9691      };
9692      /^XFORWARD\z/ && do {  # Postfix extension
9693        if (defined($sender)) {
9694          $self->smtp_resp(0,"503 5.5.1 Error: XFORWARD not allowed within transaction", 1, $cmd);
9695          last;
9696        }
9697        my($bad);
9698        for (split(' ',$args)) {
9699          if (!/^( [A-Za-z0-9] [A-Za-z0-9-]* ) = ( [\041-\176]{0,255} )\z/xs) {
9700            $self->smtp_resp(0,"501 5.5.4 Syntax error in XFORWARD parameters",
9701                             1, $cmd);
9702            $bad = 1; last;
9703          } else {
9704            my($name,$val) = (uc($1), $2);
9705            if ($name =~ /^(?:NAME|ADDR|PROTO|HELO)\z/) {
9706              $val = undef  if uc($val) eq '[UNAVAILABLE]';
9707              $xforward_args{$name} = $val;
9708            } else {
9709              $self->smtp_resp(0,"501 5.5.4 XFORWARD command parameter error: $name=$val",1,$cmd);
9710              $bad = 1; last;
9711            }
9712          }
9713        }
9714        $self->smtp_resp(1,"250 2.5.0 Ok")  if !$bad;
9715        last;
9716      };
9717      /^HELP\z/ && do {
9718        $self->smtp_resp(1,"214 2.0.0 See amavisd-new home page at:\n".
9719                           "http://www.ijs.si/software/amavisd/");
9720        last;
9721      };
9722      /^AUTH\z/ && @{ca('auth_mech_avail')} && do {  # rfc2554
9723        if ($args !~ /^([^ ]+)(?: ([^ ]*))?\z/is) {
9724          $self->smtp_resp(0,"501 5.5.2 Syntax: AUTH mech [initresp]",1,$cmd);
9725          last;
9726        }
9727        my($auth_mech,$auth_resp) = (uc($1), $2);
9728        if ($authenticated) {
9729          $self->smtp_resp(0,"503 5.5.1 Error: session already authenticated", 1, $cmd);
9730        } elsif (defined($sender)) {
9731          $self->smtp_resp(0,"503 5.5.1 Error: AUTH not allowed within transaction", 1, $cmd);
9732        } elsif (!grep {uc($_) eq $auth_mech} @{ca('auth_mech_avail')}) {
9733          $self->smtp_resp(0,"504 5.7.6 Error: requested authentication mechanism not supported", 1, $cmd);
9734        } else {
9735          my($state,$result,$challenge);
9736          if   ($auth_resp eq '=') { $auth_resp = '' }  # zero length
9737          elsif ($auth_resp eq '') { $auth_resp = undef }
9738          for (;;) {
9739            if ($auth_resp !~ m{^[A-Za-z0-9+/=]*\z}) {
9740              $self->smtp_resp(0,"501 5.5.4 Authentication failed: malformed authentication response", 1, $cmd);
9741              last;
9742            } else {
9743              $auth_resp = decode_base64($auth_resp)  if $auth_resp ne '';
9744              ($state,$result,$challenge) =
9745                authenticate($state, $auth_mech, $auth_resp);
9746              if (ref($result) eq 'ARRAY') {
9747                $self->smtp_resp(0,"235 2.7.1 Authentication successful");
9748                $authenticated = 1; ($auth_user,$auth_pass) = @$result;
9749                do_log(2,"AUTH $auth_mech, user=$auth_user");
9750              # do_log(2,"AUTH $auth_mech, user=$auth_user, pass=$auth_resp");
9751                last;
9752              } elsif (defined $result && !$result) {
9753                $self->smtp_resp(0,"535 5.7.1 Authentication failed", 1, $cmd);
9754                last;
9755              }
9756            }
9757            # server challenge or ready prompt
9758            $self->smtp_resp(1,"334 ".encode_base64($challenge,''));
9759            $auth_resp = <$sock>;
9760            do_log(5, $self->{proto} . "< $auth_resp");
9761            $auth_resp =~ s/\015?\012\z//;
9762            if ($auth_resp eq '*') {
9763              $self->smtp_resp(0,"501 5.7.1 Authentication aborted");
9764              last;
9765            }
9766          }
9767        }
9768        last;
9769      };
9770      /^VRFY\z/ && do {
9771        $self->smtp_resp(1,"502 5.5.1 Command $_ not implemented", 1, $cmd);
9772      # if ($args eq '') {
9773      #   $self->smtp_resp(1,"501 5.5.2 Syntax: VRFY address", 1, $cmd);
9774      # } else {
9775      #   $self->smtp_resp(1,"252 2.0.0 Cannot VRFY user, but will accept ".
9776      #                      "message and attempt delivery", 0, $cmd);
9777      # }
9778        last;
9779      };
9780      /^MAIL\z/ && do {  # begin new transaction
9781        if (defined($sender)) {
9782          $self->smtp_resp(0,"503 5.5.1 Error: nested MAIL command", 1, $cmd);
9783          last;
9784        }
9785        if (!$authenticated &&
9786            c('auth_required_inp') && @{ca('auth_mech_avail')} ) {
9787          $self->smtp_resp(0,"530 5.7.1 Authentication required", 1, $cmd);
9788          last;
9789        }
9790        # begin SMTP transaction
9791        my($now) = time;
9792        prolong_timer('MAIL FROM received - timer reset', $child_timeout);
9793        if (!$seq) { # the first connect
9794          section_time('SMTP pre-MAIL');
9795        } else {     # establish new time reference for each transaction
9796          Amavis::Timing::init(); snmp_counters_init();
9797        }
9798        $seq++;
9799        new_am_id(undef,$Amavis::child_invocation_count,$seq)
9800          if !$initial_am_id;
9801        $initial_am_id = 0; $self->prepare_tempdir;
9802        my($cl_ip) = $xforward_args{'ADDR'};
9803        if ($cl_ip ne '' && defined $policy_bank{'MYNETS'}
9804            && lookup_ip_acl($cl_ip,@{ca('mynetworks_maps')}) ) {
9805          Amavis::load_policy_bank('MYNETS'); $policy_changed = 1;
9806        }
9807        $msginfo = Amavis::In::Message->new;
9808        $msginfo->rx_time($now);
9809        $msginfo->delivery_method(c('forward_method'));
9810        my($submitter);
9811        if ($authenticated) {
9812          $msginfo->auth_user($auth_user); $msginfo->auth_pass($auth_pass);
9813          $conn->smtp_proto($self->{proto}.'A')  # rfc3848
9814            if $self->{proto} =~ /^(LMTP|ESMTP)\z/i;
9815        } elsif (c('auth_reauthenticate_forwarded') &&
9816                 c('amavis_auth_user') ne '') {
9817          $msginfo->auth_user(c('amavis_auth_user'));
9818          $msginfo->auth_pass(c('amavis_auth_pass'));
9819          $submitter = qquote_rfc2821_local(c('mailfrom_notify_recip'));
9820        }
9821        $msginfo->client_addr($xforward_args{'ADDR'});
9822        $msginfo->client_name($xforward_args{'NAME'});
9823        $msginfo->client_proto($xforward_args{'PROTO'});
9824        $msginfo->client_helo($xforward_args{'HELO'});
9825        %xforward_args = ();  # reset values for the next transation
9826        # permit some sloppy syntax without angle brackets
9827        if ($args !~ /^FROM: \s*
9828                      ( < (?: " (?: \\. | [^\\"] )* " | [^"@] )*
9829                          (?: @ (?: \[ (?: \\. | [^\]\\] )* \] |
9830                                    [^\[\]\\>] )* )?
9831                        > |
9832                        [^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )*
9833                      ) (?: \s+ ([\040-\176]+) )? \z/isx ) {
9834            $self->smtp_resp(0,"501 5.5.2 Syntax: MAIL FROM: <address>",1,$cmd);
9835            last;
9836        }
9837        my($bad);  my($addr,$opt) = ($1,$2);
9838        for (split(' ',$opt)) {
9839          if (!/^ ( [A-Za-z0-9] [A-Za-z0-9-]*  ) =
9840                  ( [\041-\074\076-\176]+ ) \z/xs) { # printable, not '=' or SP
9841            $self->smtp_resp(0,"501 5.5.4 Syntax error in MAIL FROM parameters",
9842                             1,$cmd);
9843            $bad = 1; last;
9844          } else {
9845            my($name,$val) = (uc($1),$2);
9846            if ($name eq 'SIZE' && $val=~/^\d{1,20}\z/) {  # rfc1870
9847              $msginfo->msg_size($val+0);
9848              if ($message_size_limit && $val > $message_size_limit) {
9849                my($msg) = "552 5.3.4 Declared message size ($val B) exceeds ".
9850                   "fixed maximium message size of $message_size_limit B";
9851                do_log(0, $self->{proto}." REJECT 'MAIL FROM': $msg");
9852                $self->smtp_resp(0,$msg, 0,$cmd);
9853                $bad = 1; last;
9854              }
9855            } elsif ($name eq 'BODY' && $val=~/^7BIT|8BITMIME\z/i){
9856              $msginfo->body_type(uc($val));
9857            } elsif ($name eq 'AUTH' && @{ca('auth_mech_avail')} &&
9858                     !defined($submitter) ) {  # rfc2554
9859              $submitter = $val;  # encoded as xtext: rfc3461
9860              $submitter =~ s/\+([0-9a-fA-F]{2})/pack("C",hex($1))/eg;
9861              do_log(5, "MAIL command, $authenticated, submitter: $submitter");
9862            } else {
9863              my($msg);
9864              if ($name eq 'AUTH' && !@{ca('auth_mech_avail')}) {
9865                $msg = "503 5.7.4 Error: authentication disabled";
9866              } else {
9867                $msg = "504 5.5.4 MAIL command parameter error: $name=$val";
9868              }
9869              $self->smtp_resp(0,$msg,1,$cmd);
9870              $bad = 1; last;
9871            }
9872          }
9873        }
9874        if (!$bad) {
9875          $addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr;
9876          $self->smtp_resp(0,"250 2.1.0 Sender $addr OK");
9877          $sender = unquote_rfc2821_local($addr);
9878          debug_oneshot(lookup(0,$sender,@{ca('debug_sender_maps')}) ? 1 : 0,
9879                        $self->{proto} . "< $cmd");
9880        # $submitter = "<$addr>" if !defined($submitter);  # rfc2554: MAY
9881          $submitter = '<>'      if !defined($msginfo->auth_user);
9882          $msginfo->auth_submitter($submitter);
9883        };
9884        last;
9885      };
9886      /^RCPT\z/ && do {
9887        if (!defined($sender)) {
9888          $self->smtp_resp(0,"503 5.5.1 Need MAIL command before RCPT",1,$cmd);
9889          @recips = (); $got_rcpt = 0;
9890          last;
9891        }
9892        $got_rcpt++;
9893        # permit some sloppy syntax without angle brackets
9894        if ($args !~ /^TO: \s*
9895                      ( < (?: " (?: \\. | [^\\"] )* " | [^"@] )*
9896                          (?: @ (?: \[ (?: \\. | [^\]\\] )* \] |
9897                                    [^\[\]\\>] )* )?
9898                        > |
9899                        [^<\s] (?: " (?: \\. | [^\\"] )* " | [^"\s] )*
9900                      ) (?: \s+ ([\040-\176]+) )? \z/isx ) {
9901          $self->smtp_resp(0,"501 5.5.2 Syntax: RCPT TO: <address>",1,$cmd);
9902          last;
9903        }
9904        if ($2 ne '') {
9905          $self->smtp_resp(0,"504 5.5.4 RCPT command parameter not implemented: $2",
9906                           1, $cmd);
9907        ### $self->smtp_resp(0,"555 5.5.4 RCPT command parameter unrecognized: $2", 1, $cmd);
9908        } elsif ($got_rcpt > $smtpd_recipient_limit) {
9909          $self->smtp_resp(0,"452 4.5.3 Too many recipients");
9910        } else {
9911          my($addr,$opt) = ($1, $2);
9912          $addr = ($addr =~ /^<(.*)>\z/s) ? $1 : $addr;
9913          my($addr_unq) = unquote_rfc2821_local($addr);
9914          my($recip_size_limit); my($mslm) = ca('message_size_limit_maps');
9915          $recip_size_limit = lookup(0,$addr_unq, @$mslm)  if @$mslm;
9916          if ($recip_size_limit && $recip_size_limit < 65536)
9917            { $recip_size_limit = 65536 }  # rfc2821 requires at least 64k
9918          if ($recip_size_limit > $max_recip_size_limit)
9919            { $max_recip_size_limit = $recip_size_limit }
9920          my($mail_size) = $msginfo->msg_size;
9921          if (defined $mail_size && $recip_size_limit && $mail_size > $recip_size_limit) {
9922            my($msg) = "552 5.3.4 Declared message size ($mail_size B) ".
9923                "exceeds fixed maximium message size of $recip_size_limit B, ".
9924                "recipient $addr";
9925            do_log(0, $self->{proto}." REJECT 'RCPT TO': $msg");
9926            $self->smtp_resp(0,$msg, 0,$cmd);
9927          } else {
9928            push(@recips,$addr_unq);
9929            $self->smtp_resp(0,"250 2.1.5 Recipient $addr OK");
9930          }
9931        };
9932        last;
9933      };
9934      /^DATA\z/ && !@recips && do {
9935        if (!defined($sender)) {
9936          $self->smtp_resp(1,"503 5.5.1 Need MAIL command before DATA",1,$cmd);
9937        } elsif (!$got_rcpt) {
9938          $self->smtp_resp(1,"503 5.5.1 Need RCPT command before DATA",1,$cmd);
9939        } elsif ($lmtp) {  # rfc2033 requires 503 code!
9940          $self->smtp_resp(1,"503 5.1.1 Error (DATA): no valid recipients",0,$cmd);
9941        } else {
9942          $self->smtp_resp(1,"554 5.1.1 Error (DATA): no valid recipients",0,$cmd);
9943        }
9944        last;
9945      };
9946      /^DATA\z/ && do {
9947        # set timer to the initial value, MTA timer starts here
9948        prolong_timer('DATA received - timer reset', $child_timeout);
9949        if ($message_size_limit) {  # enforce system-wide size limit
9950          if (!$max_recip_size_limit ||
9951              $max_recip_size_limit > $message_size_limit) {
9952            $max_recip_size_limit = $message_size_limit;
9953          }
9954        }
9955        my($within_data_transfer,$complete);
9956        my($size) = 0; my($over_size) = 0;
9957        eval {
9958          $msginfo->sender($sender); $msginfo->recips(\@recips);
9959          ll(1) && do_log(1, sprintf("%s:%s:%s %s: <%s> -> %s Received: %s",
9960                            $conn->smtp_proto,
9961                            $conn->socket_ip eq $inet_socket_bind ? ''
9962                              : '['.$conn->socket_ip.']',
9963                            $conn->socket_port, $self->{tempdir_pers},
9964                            $sender, join(',', qquote_rfc2821_local(@recips)),
9965                            join(' ', ($msginfo->msg_size  eq '' ? ()
9966                                        : 'SIZE='.$msginfo->msg_size),
9967                                      ($msginfo->body_type eq '' ? ()
9968                                        : 'BODY='.$msginfo->body_type),
9969                                      received_line($conn,$msginfo,am_id(),0) )
9970                            ) );
9971          $self->smtp_resp(1,"354 End data with <CR><LF>.<CR><LF>");
9972          $within_data_transfer = 1;
9973          section_time('SMTP pre-DATA-flush')  if $self->{pipelining};
9974          $self->{tempdir_empty} = 0;
9975          do { local($/) = "\015\012";      # set input line terminator to CRLF
9976            while(<$sock>) {                # use native I/O for speed
9977            # do_log(5, $self->{proto} . "< $_");
9978              if (/^\./) {
9979                if ($_ eq ".\015\012")
9980                  { $complete = 1; $within_data_transfer = 0; last }
9981                s/^\.(.+\015\012)\z/$1/s;   # dot de-stuffing, rfc2821
9982              }
9983              $size += length($_);  # message size is defined in rfc1870
9984              if (!$over_size) {
9985                chomp;  # remove \015\012 (the $/), faster than s///
9986                print {$self->{fh_pers}} $_,$eol
9987                  or die "Can't write to mail file: $!";
9988                if ($max_recip_size_limit && $size > $max_recip_size_limit) {
9989                  do_log(1,"Message size exceeded $max_recip_size_limit B, ".
9990                           "skiping further input");
9991                  print {$self->{fh_pers}} $eol,"***TRUNCATED***",$eol
9992                    or die "Can't write to mail file: $!";
9993                  $over_size = 1;
9994                }
9995              }
9996            }
9997            $eof = 1  if !$complete;
9998          }; # restores line terminator
9999          # normal data termination, or eof on socket, or fatal error
10000          do_log(4, $self->{proto} . "< .\015\012")  if $complete;
10001          $self->{fh_pers}->flush or die "Can't flush mail file: $!";
10002          # On some systems you have to do a seek whenever you
10003          # switch between reading and writing. Amongst other things,
10004          # this may have the effect of calling stdio's clearerr(3).
10005          $self->{fh_pers}->seek(0,1) or die "Can't seek on file: $!";
10006          section_time('SMTP DATA');
10007        };
10008        if ($@ ne '' || !$complete || $over_size) {  # err or connection broken
10009          chomp($@);
10010          # on error, either send: '421 Shutting down',
10011          # or: '451 Aborted, error in processing' and NOT shut down!
10012          if ($over_size && $@ eq '' && !$within_data_transfer) {
10013            my($msg) = "552 5.3.4 Message size ($size B) exceeds ".
10014                     "fixed maximium message size of $max_recip_size_limit B";
10015            do_log(0, $self->{proto}." REJECT: $msg");
10016            $self->smtp_resp(0,$msg, 0,$cmd);
10017          } elsif (!$within_data_transfer) {
10018            my($msg) = "Error in processing: " .
10019                       !$complete && $@ eq '' ? 'incomplete' : $@;
10020            do_log(-2, $self->{proto}." TROUBLE: 451 4.5.0 $msg");
10021            $self->smtp_resp(1, "451 4.5.0 $msg");
10022        ### $aborting = $msg;
10023          } else {
10024            $aborting = "client broke the connection ".
10025                        "during data transfer"  if $eof;
10026            $aborting .= ', '  if $aborting ne '' && $@ ne '';
10027            $aborting .= $@;
10028            $aborting = '???'  if $aborting eq '';
10029            do_log($@ ne '' ? -1 : 3,
10030                   $self->{proto}." TROUBLE, ABORTING: $aborting");
10031          }
10032        } else {  # all OK
10033          #
10034          # Is it acceptable to do all this processing here,
10035          # before returning response???  According to rfc1047
10036          # it is not a good idea! But at the moment we do not have
10037          # much choice, amavis has no queueing mechanism and can not
10038          # accept responsibility for delivery.
10039          #
10040          # check contents before responding
10041          # check_mail() expects open file on $self->{fh_pers},
10042          # need not be rewound
10043          $msginfo->mail_tempdir($self->{tempdir_pers});
10044          $msginfo->mail_text_fn($self->{tempdir_pers} . '/email.txt');
10045          $msginfo->mail_text($self->{fh_pers});
10046          my($declared_size) = $msginfo->msg_size;
10047          if (!defined($declared_size)) {
10048            $msginfo->msg_size($size);
10049          } elsif ($size > $declared_size) { # shouldn't happen with decent MTA
10050            do_log(2,"Actual message size $size B greater than the ".
10051                     "declared $declared_size B");
10052            $msginfo->msg_size($size);
10053          } elsif ($size < $declared_size) { # not unusual
10054            do_log(4,"Actual message size $size B, declared $declared_size B");
10055            $msginfo->msg_size($size);
10056          }
10057          my($smtp_resp, $exit_code, $preserve_evidence) =
10058            &$check_mail($conn,$msginfo, $lmtp,$self->{tempdir_pers});
10059          if ($preserve_evidence) { $self->preserve_evidence(1) }
10060          if ($smtp_resp !~ /^4/ &&
10061              grep { !$_->recip_done } @{$msginfo->per_recip_data}) {
10062            die "TROUBLE: (MISCONFIG) not all recipients done, " .
10063                "forward_method is: " . $msginfo->delivery_method;
10064          }
10065          if (!$lmtp) {
10066            do_log(4, "sending SMTP response: \"$smtp_resp\"");
10067            $self->smtp_resp(0, $smtp_resp);
10068          } else {
10069            my($bounced) = $msginfo->dsn_sent;
10070            for my $r (@{$msginfo->per_recip_data}) {
10071              my($resp) = $r->recip_smtp_response;
10072              if ($bounced && $smtp_resp=~/^2/ && $resp!~/^2/) {
10073                # as the message was already bounced by us,
10074                # MTA must not bounce it again; failure status
10075                # needs to be converted into success!
10076                $resp = sprintf("250 2.5.0 Ok, DSN %s (%s)",
10077                                $bounced==1 ? 'sent' : 'muted', $resp);
10078              }
10079              do_log(4, sprintf("sending LMTP response for <%s>: \"%s\"",
10080                                $r->recip_addr, $resp));
10081              $self->smtp_resp(0, $resp);
10082            }
10083          }
10084        };
10085        alarm(0); do_log(5,"timer stopped after DATA end");
10086        if ($self->preserve_evidence && !$self->{tempdir_empty}) {
10087          # keep evidence in case of trouble
10088          do_log(-1,"PRESERVING EVIDENCE in ".$self->{tempdir_pers});
10089          $self->{fh_pers}->close or die "Can't close mail file: $!";
10090          $self->{fh_pers} = undef; $self->{tempdir_pers} = undef;
10091          $self->{tempdir_empty} = 1;
10092        }
10093        # cleanup, but leave directory (and file handle if possible) for reuse
10094        if ($self->{fh_pers} && !$can_truncate) {
10095          # truncate is not standard across all Unix variants,
10096          # it is not Posix, but is XPG4-UNIX.
10097          # So if we can't truncate a file and leave it open,
10098          # we have to create it anew later, at some cost.
10099          #
10100          $self->{fh_pers}->close or die "Can't close mail file: $!";
10101          $self->{fh_pers} = undef;
10102          unlink($self->{tempdir_pers}.'/email.txt')
10103            or die "Can't delete file ".$self->{tempdir_pers}."/email.txt: $!";
10104          section_time('delete email.txt');
10105        }
10106        if (defined $self->{tempdir_pers}) {  # prepare for the next one
10107          strip_tempdir($self->{tempdir_pers}); $self->{tempdir_empty} = 1;
10108        }
10109        $sender = undef; @recips = (); $got_rcpt = 0;     # implicit RSET
10110        $max_recip_size_limit = undef; $msginfo = undef;  # forget previous
10111        if ($policy_changed)
10112          { %current_policy_bank = %baseline_policy_bank; $policy_changed = 0 }
10113        $self->preserve_evidence(0);  # reset
10114        # report elapsed times by section for each transaction
10115        # (the time for the QUIT remains unaccounted for)
10116        do_log(2, Amavis::Timing::report());
10117        Amavis::Timing::init(); snmp_counters_init();
10118        last;
10119      };  # DATA
10120      # catchall (EXPN, TURN, unknown):
10121      $self->smtp_resp(1,"502 5.5.1 Error: command ($_) not implemented",1,$cmd);
10122    # $self->smtp_resp(1,"500 5.5.2 Error: command ($_) not recognized", 1,$cmd);
10123    };  # end of 'switch' block
10124    if ($terminating || defined $aborting) {   # exit SMTP-session loop
10125      $voluntary_exit = 1; last;
10126    }
10127    # rfc2920 requires a flush whenever the local TCP input buffer is
10128    # emptied. Since we can't check it (unless we use sysread & select),
10129    # we should do a flush here to be in compliance. We could only break
10130    # the requirement if we _knew_ we talk with a local MTA client which
10131    # uses client-side pipelining.
10132    $self->smtp_resp_flush;
10133    $0 = sprintf("amavisd (ch%d-%s-idle)",
10134                 $Amavis::child_invocation_count, am_id());
10135    Amavis::Timing::go_idle(6);
10136    undef $!;
10137  } # end of while
10138  my($errn,$errs);
10139  if (!$voluntary_exit) {
10140    $eof = 1;
10141    if (!defined($_)) { $errn = 0+$!; $errs = "$!" }
10142  }
10143  $0 = sprintf("amavisd (ch%d)", $Amavis::child_invocation_count);
10144  Amavis::Timing::go_busy(7);
10145  # come here when: QUIT is received, eof or err on socket, or we need to abort
10146  $self->smtp_resp_flush; # just in case, the session might have been disconnected
10147  my($msg) =
10148    defined $aborting && !$eof ? "ABORTING the session: $aborting" :
10149    defined $aborting ? $aborting :
10150    !$terminating ? "client broke the connection without a QUIT ($errs)" : '';
10151  do_log($aborting?-1:3, $self->{proto}.': NOTICE: '.$msg)  if $msg ne '';
10152  if (defined $aborting && !$eof)
10153    { $self->smtp_resp(1,"421 4.3.2 Service shutting down, ".$aborting) }
10154  $self->{session_closed_normally} = 1;
10155  # closes connection after child_finish_hook
10156}
10157
10158# sends a SMTP response consisting of 3-digit code and an optional message;
10159# slow down evil clients by delaying response on permanent errors
10160sub smtp_resp($$$;$$) {
10161  my($self, $flush,$resp, $penalize,$line) = @_;
10162  if ($penalize) {
10163    do_log(-1, $self->{proto} . ": $resp; PENALIZE: $line");
10164    sleep 5;
10165    section_time('SMTP penalty wait');
10166  }
10167  $resp = sanitize_str($resp,1);
10168  local($1,$2,$3,$4);
10169  if ($resp !~ /^ ([1-5]\d\d) (\ |-|\z)
10170                ([245] \. \d{1,3} \. \d{1,3} (?: \ |\z) )?
10171                (.*) \z/xs)
10172    { die "Internal error(2): bad SMTP response code: '$resp'" }
10173  my($resp_code,$continuation,$enhanced,$tail) = ($1,$2,$3, $4);
10174  my($lead_len) = length($resp_code) + 1 + length($enhanced);
10175  while (length($tail) > 512-2-$lead_len || $tail =~ /\n/) {
10176    # rfc2821: The maximum total length of a reply line including the
10177    # reply code and the <CRLF> is 512 characters.  More information
10178    # may be conveyed through multiple-line replies.
10179    my($head) = substr($tail,0,512-2-$lead_len);
10180    if ($head =~ /^([^\n]*\n)/) { $head = $1 }
10181    $tail = substr($tail,length($head)); chomp($head);
10182    push(@{$self->{smtp_outbuf}}, $resp_code.'-'.$enhanced.$head);
10183  }
10184  push(@{$self->{smtp_outbuf}},$resp_code.$continuation.$enhanced.$tail);
10185  $self->smtp_resp_flush   if $flush || !$self->{pipelining} ||
10186                              @{$self->{smtp_outbuf}} > 200;
10187}
10188
10189sub smtp_resp_flush($) {
10190  my($self) = shift;
10191  if (@{$self->{smtp_outbuf}}) {
10192    for my $resp (@{$self->{smtp_outbuf}}) {
10193      do_log(4, $self->{proto} . "> $resp");
10194    };
10195    my($stat) =
10196      $self->{sock}->print(map { $_."\015\012" } @{$self->{smtp_outbuf}} );
10197    @{$self->{smtp_outbuf}} = ();  # prevent printing again even if error
10198    $stat or die "Error writing a SMTP response to the socket: $!";
10199  }
10200}
10201
102021;
10203
10204__DATA__
10205#
10206package Amavis::In::QMQPqq;
10207use strict;
10208# use re 'taint';   # (is this module ready for this yet?)
10209
10210BEGIN {
10211    use Exporter ();
10212    use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
10213    $VERSION = '1.17';
10214    @ISA = qw(Exporter);
10215}
10216use POSIX qw(strftime);
10217use Errno qw(ENOENT);
10218
10219BEGIN {
10220    import Amavis::Conf qw(:platform :confvars :dynamic_confvars c cr ca);
10221    import Amavis::Util qw(do_log am_id prolong_timer debug_oneshot
10222        untaint sanitize_str strip_tempdir rmdir_recursively);
10223    import Amavis::Lookup qw(lookup);
10224    import Amavis::Timing qw(section_time);
10225    import Amavis::rfc2821_2822_Tools;
10226    import Amavis::In::Message;
10227    import Amavis::In::Connection;
10228}
10229
10230sub new($) {
10231    my($class) = @_;
10232    my($self) = bless {}, $class;
10233    $self->{fh_pers} = undef;           # persistent file handle for email.txt
10234    $self->{tempdir_pers} = undef;      # temporary directory for check_mail
10235    $self->{preserve} = undef;          # don't delete tempdir on exit
10236    $self->{tempdir_empty} = 1;         # anything of interest in tempdir?
10237    $self->{bytesleft} = undef;         # bytes left for whole package
10238    $self->{len} = undef;               # set by getlen() method
10239    $self->{sock} = undef;              # connected socket
10240    $self->{proto} = undef;             # protocol
10241    $self->{session_closed_normally} = undef; # closed properly? (waited for K/Z/D)
10242    $self;
10243}
10244
10245sub preserve_evidence  # try to preserve temporary files etc in case of trouble
10246  { my($self)=shift; !@_ ? $self->{preserve} : ($self->{preserve}=shift) }
10247
10248sub DESTROY {
10249    my($self) = shift;
10250#   do_log(0, "Amavis::In::QMQPqq::DESTROY called");
10251    $self->{fh_pers}->close
10252        or die "Can't close temp file: $!"  if $self->{fh_pers};
10253    my($errn) = $self->{tempdir_pers} eq '' ? ENOENT
10254                    : (stat($self->{tempdir_pers}) ? 0 : 0+$!);
10255    if (defined $self->{tempdir_pers} && $errn != ENOENT) {
10256        # this will not be included in the TIMING report,
10257        # but it only occurs infrequently and doesn't take that long
10258        if ($self->preserve_evidence && !$self->{tempdir_empty}) {
10259            do_log(0, "tempdir is to be PRESERVED: ".$self->{tempdir_pers});
10260        } else {
10261            do_log(2, "tempdir being removed: ".$self->{tempdir_pers});
10262            rmdir_recursively($self->{tempdir_pers});
10263        }
10264    }
10265    if (! $self->{session_closed_normally}) {
10266        $self->qmqpqq_resp("Z","Service shutting down, closing channel");
10267    }
10268}
10269
10270sub prepare_tempdir($) {
10271    my($self) = @_;
10272    if (! defined $self->{tempdir_pers} ) {
10273        # invent a name for a temporary directory for this child, and create it
10274        my($now_iso8601) = strftime("%Y%m%dT%H%M%S", localtime);
10275        $self->{tempdir_pers} = sprintf("%s/amavis-%s-%05d",
10276                                        $TEMPBASE, $now_iso8601, $$);
10277    }
10278    my($errn) = stat($self->{tempdir_pers}) ? 0 : 0+$!;
10279    if ($errn == ENOENT || ! -d _) {
10280        mkdir($self->{tempdir_pers}, 0750)
10281            or die "Can't create directory $self->{tempdir_pers}: $!";
10282        $self->{tempdir_empty} = 1;
10283        section_time('mkdir tempdir');
10284    }
10285    # prepare temporary file for writing (and reading later)
10286    my($fname) = $self->{tempdir_pers} . "/email.txt";
10287    my($errn) = stat($fname) ? 0 : 0+$!;
10288    if ($self->{fh_pers} && !$errn && -f _) {
10289        $self->{fh_pers}->seek(0,0) or die "Can't rewind mail file: $!";
10290        $self->{fh_pers}->truncate(0) or die "Can't truncate mail file: $!";
10291    } else {
10292        $self->{fh_pers} = IO::File->new($fname, 'w+', 0640)
10293            or die "Can't create file $fname: $!";
10294        section_time('create email.txt');
10295    }
10296}
10297
10298
10299# get byte, die if no bytes left
10300sub getbyte($) {
10301my($self) = shift;
10302if(!$self->{bytesleft}--) {
10303        die("No bytes left");
10304        }
10305if(defined($_ = $self->{sock}->getc)) {
10306        return($_);
10307        }
10308die("EOF on socket");
10309}
10310
10311sub getlen($) {
10312my($self) = shift;
10313my($ch,$len);
10314
10315for(;;) {
10316        $ch = $self->getbyte;
10317        if($ch eq ':') {
10318                return($self->{len} = $len);
10319                }
10320        if($ch !~ /^\d$/) {
10321                die("Char '$ch' is not a number while determining length");
10322                }
10323        $len .= $ch;
10324        }
10325}
10326
10327sub getcomma($) {
10328my($self) = shift;
10329if($self->getbyte ne ',') {
10330        die("Comma expected, found '$_'");
10331        }
10332}
10333
10334sub getnetstring($$) {
10335my($self) = shift;
10336($self->{sock}->read($_[0],$self->getlen) == $self->{len}) ||
10337        die("EOF on socket");
10338$self->{bytesleft} -= $self->{len};
10339$self->getcomma;
10340}
10341
10342
10343# Accept a QMQPqq connect
10344# and call content checking for the message received
10345#
10346sub process_qmqpqq_request($$$$) {
10347my($self,$sock,$conn,$check_mail) = @_;
10348# $sock:       connected socket from Net::Server
10349# $conn:       information about client connection
10350# $check_mail: subroutine ref to be called with file handle
10351
10352$self->{proto} = "QMQPqq";
10353$self->{sock} = $sock;          # store $sock info for getbyte() method
10354$self->{bytesleft} = 20;        # initial bytesleft value, there should
10355                                # NEVER EVER be longer email than 10^20 (approximately)
10356                                # bytes but increase if needed ;)
10357$self->{len} = undef;
10358
10359my($msginfo);
10360
10361my($sender,@recips);
10362
10363my($len);
10364
10365$conn->smtp_proto("QMQPqq");  # the name of the method is too specific
10366eval {
10367        # get length of whole package
10368        $self->{bytesleft} = $self->getlen;
10369
10370        # get length of 'email'
10371        $len = $self->getlen;
10372        section_time('initial length determination');
10373
10374        am_id(sprintf("%05d-%02d",$$,$Amavis::child_invocation_count));
10375
10376        # prepare tempdir
10377        $self->prepare_tempdir;
10378        $msginfo = Amavis::In::Message->new;
10379        $msginfo->rx_time(time);
10380        $msginfo->delivery_method(c('forward_method'));
10381
10382        # get 'email'
10383        $self->{tempdir_empty} = 0;
10384        my $size = 16384;
10385        while(($len > 0) && ($sock->read($_,($len >= $size ? $size : $size = $len)) == $size)) {
10386                (print {$self->{fh_pers}} $_) ||
10387                        die("Can't write to mail file: $!");
10388                $len -= $size;
10389                }
10390        if($len > 0) {
10391                die("EOF on socket");
10392                }
10393        $self->{fh_pers}->flush || die("Can't flush mail file: $!");
10394        $self->{fh_pers}->seek(0,1) || die("Can't seek on file: $!");
10395        $self->{bytesleft} -= $self->{len};
10396        section_time('email receiving');
10397        # comma has to follow
10398        $self->getcomma;
10399
10400        # get sender
10401        $self->getnetstring($sender);
10402        section_time('sender receiving');
10403
10404        # get recips
10405        my $i = 0;
10406        while($self->{bytesleft}) {
10407                $self->getnetstring($recips[$i++]);
10408                }
10409        section_time('recips receiving');
10410
10411        # final comma has to follow
10412        $self->{bytesleft} = 1;
10413        $self->getcomma;
10414               
10415        $msginfo->sender($sender);
10416        $msginfo->recips(\@recips);
10417               
10418        do_log(1, sprintf("%s:%s:%s %s: <%s> -> %s Received: %s",
10419                $self->{proto},$conn->socket_ip eq $inet_socket_bind ?
10420                        '' : '['.$conn->socket_ip.']',
10421                $conn->socket_port, $self->{tempdir_pers},
10422                $sender, join(',', map{"<$_>"}@recips),
10423                join(' ',
10424                        ($msginfo->msg_size  eq '' ? ()
10425                        : 'SIZE='.$msginfo->msg_size),
10426                        ($msginfo->body_type eq '' ? ()
10427                        : 'BODY='.$msginfo->body_type),
10428                        received_line($conn,$msginfo,am_id(),0) )
10429                ));
10430
10431        $msginfo->mail_tempdir($self->{tempdir_pers});
10432        $msginfo->mail_text_fn($self->{tempdir_pers} . '/email.txt');
10433        $msginfo->mail_text($self->{fh_pers});
10434
10435        my($smtp_resp,$exit_code,$preserve_evidence) =
10436                &$check_mail($conn,$msginfo,0,$self->{tempdir_pers});
10437
10438        if ($preserve_evidence) {
10439                $self->preserve_evidence(1);
10440                }
10441        if ($smtp_resp !~ /^4/ &&
10442                grep { !$_->recip_done } @{$msginfo->per_recip_data}) {
10443                die("TROUBLE/MISCONFIG: not all recipients done, ".
10444                        "\$forward_method is \"$forward_method\"");
10445                }
10446
10447        # all ok
10448        if($smtp_resp =~ /^2/) {
10449                $self->qmqpqq_resp("K",$smtp_resp);
10450                }
10451        # permanent reject
10452        elsif($smtp_resp =~ /^5/) {
10453                $self->qmqpqq_resp("D",$smtp_resp);
10454                }
10455        # temporary reject (or other error if !~ /^4/)
10456        else {
10457                $self->qmqpqq_resp("Z",$smtp_resp);
10458                }
10459        };
10460
10461alarm(0); do_log(5,"timer stopped after QMQPqq eval");
10462
10463if($@ ne '') {
10464        chomp($@);
10465
10466        do_log(0,"QMQPqq: NOTICE: $@");
10467        $self->qmqpqq_resp("Z","Service shutting down, $@");
10468        }
10469
10470if ($self->preserve_evidence && !$self->{tempdir_empty}) {
10471        # keep evidence in case of trouble
10472        do_log(0,"PRESERVING EVIDENCE in ".$self->{tempdir_pers});
10473        $self->{fh_pers}->close or die "Can't close mail file: $!";
10474        $self->{fh_pers} = undef; $self->{tempdir_pers} = undef;
10475        $self->{tempdir_empty} = 1;
10476        }
10477
10478# cleanup, but leave directory (and file handle
10479# if possible) for reuse
10480if ($self->{fh_pers} && !$can_truncate) {
10481        # truncate is not standard across all Unix variants,
10482        # it is not Posix, but is XPG4-UNIX.
10483        # So if we can't truncate a file and leave it open,
10484        # we have to create it anew later, at some cost.
10485        #
10486        $self->{fh_pers}->close or die "Can't close mail file: $!";
10487        $self->{fh_pers} = undef;
10488        unlink($self->{tempdir_pers}."/email.txt")
10489                or die "Can't delete file ".
10490        $self->{tempdir_pers}."/email.txt: $!";
10491        section_time('delete email.txt');
10492        }
10493
10494if (defined $self->{tempdir_pers}) { # prepare for the next one
10495        strip_tempdir($self->{tempdir_pers});
10496        $self->{tempdir_empty} = 1;
10497        }
10498
10499$self->preserve_evidence(0);  # reset
10500# report elapsed times by section for each transaction
10501do_log(2, Amavis::Timing::report());
10502
10503$self->{session_closed_normally} = 1;
10504# closes connection after child_finish_hook
10505}
10506
10507# sends a QMQPqq response consisting of K/D/Z code and an optional message;
10508# slow down evil clients by delaying response on permanent errors
10509sub qmqpqq_resp($$$;$$) {
10510my($self,$code,$resp,$penalize,$line) = @_;
10511if($code !~ /^(K|Z|D)$/) {
10512        die("Internal error(2): bad QMQPqq response code: '$code'");
10513        }
10514if($penalize) {
10515        do_log(0,"QMQPqq: $resp; PENALIZE: $line");
10516        sleep 5;
10517        section_time('QMQPqq penalty wait');
10518        }
10519$resp = sanitize_str($resp,1);
10520do_log(4,"QMQPqq> $resp");
10521print($self->netstring($code . $resp));
10522}
10523
10524sub netstring($$) {
10525my($self,$string) = @_;
10526return(sprintf("%d:%s,",length($string),$string));
10527}
10528
105291;
10530
10531__DATA__
10532#
10533package Amavis::AV;
10534use strict;
10535use re 'taint';
10536
10537BEGIN {
10538  use Exporter ();
10539  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
10540  $VERSION = '2.034';
10541  @ISA = qw(Exporter);
10542  @EXPORT_OK = qw(&sophos_savi_init);
10543}
10544
10545use POSIX qw(WIFEXITED WIFSIGNALED WIFSTOPPED
10546             WEXITSTATUS WTERMSIG WSTOPSIG);
10547use Errno qw(EPIPE ENOTCONN ENOENT EACCES);
10548use Socket;
10549use IO::Socket;
10550use IO::Socket::UNIX;
10551
10552use subs @EXPORT_OK;
10553use vars @EXPORT;
10554
10555BEGIN {
10556  import Amavis::Conf qw(:platform :confvars c cr ca);
10557  import Amavis::Util qw(ll untaint min max do_log am_id rmdir_recursively
10558                         exit_status_str run_command);
10559  import Amavis::Timing qw(section_time);
10560}
10561
10562use vars qw(%st_socket_created %st_sock); # keep persistent state (per-socket)
10563
10564# subroutine available for calling from @av_scanners list entries;
10565# it has the same args and returns as run_av() below
10566sub ask_daemon { ask_av(\&ask_daemon_internal, @_) }
10567
10568sub clamav_module_init($) {
10569  my($av_name) = @_;
10570  # each child should reinitialize clamav module to reload databases.
10571  my($clamav_version) = Mail::ClamAV->VERSION;
10572  my($dbdir) = Mail::ClamAV::retdbdir();
10573  my($clamav_obj) = Mail::ClamAV->new($dbdir);
10574  ref $clamav_obj
10575    or die "$av_name: Can't load db from $dbdir: $Mail::ClamAV::Error";
10576  $clamav_obj->buildtrie;
10577  $clamav_obj->maxreclevel($MAXLEVELS)  if $MAXLEVELS;
10578  $clamav_obj->maxfiles($MAXFILES);
10579  $clamav_obj->maxfilesize($MAX_EXPANSION_QUOTA || 30*1024*1024);
10580  if ($clamav_version >= 0.12) {
10581    $clamav_obj->maxratio($MAX_EXPANSION_FACTOR);
10582#   $clamav_obj->archivememlim(0);  # limit memory usage for bzip2 (0/1)
10583  }
10584  do_log(2,"$av_name init");
10585  section_time('clamav_module_init');
10586  ($clamav_obj,$clamav_version);
10587}
10588
10589# to be called from sub ask_clamav
10590use vars qw($clamav_obj $clamav_version);
10591sub clamav_module_internal($@) {
10592  my($query, $bare_fnames,$names_to_parts,$tempdir, $av_name) = @_;
10593  if (!defined $clamav_obj) {
10594    ($clamav_obj,$clamav_version) = clamav_module_init($av_name);  # first time
10595  } elsif ($clamav_obj->statchkdir) {            # db reload needed?
10596    do_log(2, "$av_name: reloading virus database");
10597    ($clamav_obj,$clamav_version) = clamav_module_init($av_name);
10598  }
10599  my($fname) = "$tempdir/parts/$query";   # file to be checked
10600  my($part) = $names_to_parts->{$query};  # get corresponding parts object
10601  my($options) = 0;  # bitfield of options to Mail::ClamAV::scan
10602  my($opt_archive,$opt_mail);
10603  if ($clamav_version < 0.12) {
10604    $opt_archive = &Mail::ClamAV::CL_ARCHIVE;
10605    $opt_mail    = &Mail::ClamAV::CL_MAIL;
10606  } else {         # >= 0.12, reflects renamed flags in libclamav 0.80
10607    $opt_archive = &Mail::ClamAV::CL_SCAN_ARCHIVE;
10608    $opt_mail    = &Mail::ClamAV::CL_SCAN_MAIL;
10609  }
10610  $options |= &Mail::ClamAV::CL_SCAN_STDOPT  if $clamav_version >= 0.13;
10611  $options |= $opt_archive;  # turn on ARCHIVE
10612  $options &= ~$opt_mail;    # turn off MAIL
10613  if (ref($part) && (lc($part->type_short) eq 'mail' ||
10614                     lc($part->type_declared) eq 'message/rfc822')) {
10615    do_log(2, "$av_name: $query - enabling option CL_MAIL");
10616    $options |= $opt_mail;   # turn on MAIL
10617  }
10618  my($ret) = $clamav_obj->scan(untaint($fname), $options);
10619  my($output,$status);
10620  if    ($ret->virus) { $status = 1; $output = "INFECTED: $ret" }
10621  elsif ($ret->clean) { $status = 0; $output = "CLEAN" }
10622  else { $status = 2; $output = $ret->error.", errno=".$ret->errno }
10623  ($status,$output);  # return synthesised status and a result string
10624}
10625
10626# subroutine available for calling from @av_scanners list entries;
10627# it has the same args and returns as run_av() below
10628sub ask_clamav { ask_av(\&clamav_module_internal, @_) }
10629
10630
10631use vars qw($savi_obj);
10632sub sophos_savi_init {
10633  my($av_name, $command) = @_;
10634  my(@savi_bool_options) = qw(
10635    FullSweep DynamicDecompression FullMacroSweep OLE2Handling
10636    IgnoreTemplateBit VBA3Handling VBA5Handling OF95DecryptHandling
10637    HelpHandling DecompressVBA5 Emulation PEHandling ExcelFormulaHandling
10638    PowerPointMacroHandling PowerPointEmbeddedHandling ProjectHandling
10639    ZipDecompression ArjDecompression RarDecompression UueDecompression
10640    GZipDecompression TarDecompression CmzDecompression HqxDecompression
10641    MbinDecompression !LoopBackEnabled
10642    Lha SfxArchives MSCabinet TnefAttachmentHandling MSCompress
10643    !DeleteAllMacros Vbe !ExecFileDisinfection VisioFileHandling
10644    ActiveMimeHandling !DelVBA5Project
10645    ScrapObjectHandling SrpStreamHandling Office2001Handling
10646    Upx PalmPilotHandling HqxDecompression
10647    Pdf Rtf Html Elf WordB OutlookExpress
10648  );
10649  # starting with SAVI V3: Mac and SafeMacDfHandling options were removed;
10650  # new option GrpArchiveUnpack makes individual settings unnecessary;
10651  # option 'Mime' may cause a CPU loop when checking broken mail with some
10652  # versions of Sophos library (even with more recent ones!)
10653  my($savi_obj) = SAVI->new;
10654  ref $savi_obj or die "$av_name: Can't create SAVI object, err=$savi_obj";
10655  my($version) = $savi_obj->version;
10656  ref $version or die "$av_name: Can't get SAVI version, err=$version";
10657  do_log(2,sprintf("$av_name init: Version %s (engine %d.%d) recognizing %d viruses\n",
10658         $version->string, $version->major, $version->minor, $version->count));
10659# for ($version->ide_list)
10660#   { do_log(2, sprintf("$av_name: IDE %s released %s", $_->name, $_->date)) }
10661  my($error) = $savi_obj->set('MaxRecursionDepth', $MAXLEVELS, 1);
10662  !defined $error or die "$av_name: error setting MaxRecursionDepth: err=$error";
10663  $error     = $savi_obj->set('NamespaceSupport', 3);  # new with Sophos 3.67
10664  !defined $error
10665    or do_log(-1,"$av_name: error setting NamespaceSupport: err=$error");
10666  for (@savi_bool_options) {
10667    my($value) = /^!/ ? 0 : 1;  s/^!+//;
10668    $error = $savi_obj->set($_, $value);
10669    !defined $error or die "$av_name: Error setting $_: err=$error";
10670  }
10671  section_time('sophos_savi_init');
10672  $savi_obj;
10673}
10674
10675# to be called from sub sophos_savi
10676sub sophos_savi_internal {
10677  my($query,
10678     $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args) = @_;
10679# if (defined $args) { $savi_obj = $args }
10680# else {
10681    $savi_obj = sophos_savi_init($av_name,$command)  if !defined $savi_obj;
10682# }
10683  my($fname) = "$tempdir/parts/$query";   # file to be checked
10684  my($part) = $names_to_parts->{$query};  # get corresponding parts object
10685  my($mime_option_value) = 0;
10686  if (ref($part) && (lc($part->type_short) eq 'mail' ||
10687                     lc($part->type_declared) eq 'message/rfc822')) {
10688    do_log(2, "$av_name: $query - enabling option MIME");
10689    $mime_option_value = 1;
10690  }
10691  my($error) = $savi_obj->set('MIME', $mime_option_value);
10692  !defined $error or die sprintf("%s: Error %s option MIME: err=%s",
10693                $av_name, $mime_option_value ? 'setting' : 'clearing', $error);
10694  my($output,$status); my($result) = $savi_obj->scan($fname);
10695  if (!ref($result)) {  # error
10696    my($msg) = "$av_name: error scanning file $fname, " .
10697               $savi_obj->error_string($result) . " ($result) $!";
10698    if (! grep {$result == $_} (514,527,530,538,549) ) {
10699      $status = 2; $output = "ERROR: $msg\n";
10700    } else { # don't panic on non-fatal (encrypted, corrupted, partial)
10701      $status = 0; $output = "CLEAN: $msg\n";
10702    }
10703    do_log(-1,$output);
10704  } elsif ($result->infected) {
10705    $status = 1; $output = "INFECTED $query\n";
10706    for my $virus_name ($result->viruses) { $output .= "$virus_name FOUND\n" }
10707  } else {
10708    $status = 0; $output = "CLEAN $query\n";
10709  }
10710  ($status,$output);  # return synthesised status and a result string
10711}
10712
10713# subroutine available for calling from @av_scanners list entries;
10714# it has the same args and returns as run_av() below
10715sub ask_sophos_savi {
10716  my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
10717     $sts_clean,$sts_infected,$how_to_get_names) = @_;
10718  if (@_ < 3+6) {  # supply default arguments for backwards compatibility
10719    $args = ["*"]; $sts_clean = [0]; $sts_infected = [1];
10720    $how_to_get_names = qr/^(.*) FOUND$/;
10721  }
10722  ask_av(\&sophos_savi_internal,
10723         $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
10724         $sts_clean, $sts_infected, $how_to_get_names);
10725}
10726
10727
10728# same args and returns as run_av() below,
10729# but prepended by a $query, which is the string to be sent to the daemon.
10730# Handles both UNIX and INET domain sockets.
10731# More than one socket may be specified for redundancy, they will be tried
10732# one after the other until one succeeds.
10733#
10734sub ask_daemon_internal {
10735  my($query,  # expanded query template, often a command and a file or dir name
10736     $bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
10737     $sts_clean,$sts_infected,$how_to_get_names,  # regexps
10738  ) = @_;
10739  my($query_template_orig,$sockets) = @$args;
10740  my($output); my($socketname,$is_inet);
10741  if (!ref($sockets)) { $sockets = [ $sockets ] }
10742  my($max_retries) = 2 * @$sockets;  my($retries) = 0;
10743  $SIG{PIPE} = 'IGNORE';  # 'send' to broken pipe would throw a signal
10744  for (;;) {  # gracefully handle cases when av child times out or restarts
10745    @$sockets >= 1 or die "no sockets specified!?";  # sanity
10746    $socketname = $sockets->[0];  # try the first one in the current list
10747    $is_inet = $socketname =~ m{^/} ? 0 : 1; # simpleminded: unix vs. inet sock
10748    eval {
10749      if (!$st_socket_created{$socketname}) {
10750        ll(3) && do_log(3, "$av_name: Connecting to socket " .
10751                  join(' ',$daemon_chroot_dir,$socketname).
10752                  (!$retries ? '' : ", retry #$retries") );
10753        if ($is_inet) {   # inet socket
10754          $st_sock{$socketname} = IO::Socket::INET->new($socketname)
10755            or die "Can't connect to INET socket $socketname: $!\n";
10756          $st_socket_created{$socketname} = 1;
10757        } else {          # unix socket
10758          $st_sock{$socketname} = IO::Socket::UNIX->new(Type => SOCK_STREAM)
10759            or die "Can't create UNIX socket: $!\n";
10760          $st_socket_created{$socketname} = 1;
10761          $st_sock{$socketname}->connect( pack_sockaddr_un($socketname) )
10762            or die "Can't connect to UNIX socket $socketname: $!\n";
10763        }
10764      }
10765      ll(3) && do_log(3,sprintf("$av_name: Sending %s to %s socket %s",
10766                                $query, $is_inet?"INET":"UNIX", $socketname));
10767      # UGLY: bypass send method in IO::Socket to be able to retrieve
10768      # status/errno directly from 'send', not from 'getpeername':
10769      defined send($st_sock{$socketname}, $query, 0)
10770        or die "Can't send to socket $socketname: $!\n";
10771      if ($av_name =~ /^(Sophie|Trophie)/i) {
10772        # Sophie and Trophie can accept multiple requests per session
10773        # and return a single line response each time
10774        defined $st_sock{$socketname}->recv($output, 1024)
10775          or die "Can't receive from socket $socketname: $!\n";
10776      } else {
10777        $output = join('', $st_sock{$socketname}->getlines);
10778        $st_sock{$socketname}->close
10779          or die "Can't close socket $socketname: $!\n";
10780        $st_sock{$socketname}=undef; $st_socket_created{$socketname}=0;
10781      }
10782      $! = undef;
10783      $output ne '' or die "Empty result from $socketname\n";
10784    };
10785    last  if $@ eq '';
10786    # error handling (most interesting error codes are EPIPE and ENOTCONN)
10787    chomp($@); my($err) = "$!"; my($errn) = 0+$!;
10788    ++$retries <= $max_retries
10789      or die "Too many retries to talk to $socketname ($@)";
10790    # is ECONNREFUSED for INET sockets common enough too?
10791    if ($retries <= 1 && $errn == EPIPE) {  # common, don't cause concern
10792      do_log(2,"$av_name broken pipe (don't worry), retrying ($retries)");
10793    } else {
10794      do_log( ($retries>1?-1:1), "$av_name: $@, retrying ($retries)");
10795      if ($retries % @$sockets == 0) {  # every time the list is exhausted
10796        my($dly) = min(20, 1 + 5 * ($retries/@$sockets - 1));
10797        do_log(3,"$av_name: sleeping for $dly s");
10798        sleep($dly);   # slow down a possible runaway
10799      }
10800    }
10801    if ($st_socket_created{$socketname}) {
10802      # prepare for a retry, ignore 'close' status
10803      $st_sock{$socketname}->close;
10804      $st_sock{$socketname} = undef; $st_socket_created{$socketname} = 0;
10805    }
10806    # leave good socket as the first entry in the list
10807    # so that it will be tried first when needed again
10808    push(@$sockets, shift @$sockets)  if @$sockets>1; # circular shift left
10809  }
10810  (0,$output);  # return synthesised status and result string
10811}
10812
10813# ask_av is a common subroutine available to be used by ask_daemon, ask_clamav,
10814# ask_sophos_savi and similar front-end routines used in @av_scanners entries.
10815# It traverses supplied files or directory ($bare_fnames) and calls a supplied
10816# subroutine for each file to be scanned, summarizing the final av scan result.
10817# It has the same args and returns as run_av() below, prepended by a checking
10818# subroutine argument.
10819sub ask_av {
10820  my($code) = shift; # strip away the first argument, a subroutine ref
10821  my($bare_fnames,$names_to_parts,$tempdir, $av_name,$command,$args,
10822     $sts_clean,$sts_infected,$how_to_get_names) = @_;
10823  my($query_template) = ref $args eq 'ARRAY' ? $args->[0] : $args;
10824  do_log(5, "ask_av ($av_name): query template1: $query_template");
10825  my($checking_each_file) = $query_template =~ /\*/;
10826  my($scan_status,@virusname); my($output) = '';
10827  for my $f ($checking_each_file ? @$bare_fnames : ("$tempdir/parts")) {
10828    my($query) = $query_template;
10829    if (!$checking_each_file) {  # scanner can be given a directory name
10830      $query =~ s[{}][$tempdir/parts]g;  # replace {} with directory name
10831      do_log(3,"Using ($av_name) on dir: $query");
10832    } else {                     # must check each file individually
10833      # replace {}/* with directory name and file, and * with current file name
10834      $query =~ s[ ({}/)? \* ][ $1 eq '' ? $f : "$tempdir/parts/$f" ]gesx;
10835      do_log(3,"Using ($av_name) on file: $query");
10836    }
10837    my($t_status,$t_output) = &$code($query, @_);
10838    do_log(4,"ask_av ($av_name) result: $t_output");
10839    # braindead Perl: ""=~/x{0}/ serves as explicit default for an empty regexp
10840    if (defined $sts_infected && (
10841        ref($sts_infected) eq 'ARRAY' ? (grep {$_==$t_status} @$sts_infected)
10842                 : ""=~/x{0}/ && $t_output=~/$sts_infected/m)) {  # is infected
10843      # test for infected first, in case both expressions match
10844      $scan_status = 1;  # 'true' indicates virus found, no errors
10845      my(@t_virusnames) = ref($how_to_get_names) eq 'CODE'
10846                            ? &$how_to_get_names($t_output)
10847                            : ""=~/x{0}/ && $t_output=~/$how_to_get_names/gm;
10848      @t_virusnames = map { defined $_ ? $_ : () } @t_virusnames;
10849      push(@virusname, @t_virusnames);
10850      $output .= $t_output . $eol;
10851      do_log(2,"ask_av ($av_name): $f INFECTED: ".join(", ",@t_virusnames));
10852    } elsif (!defined($sts_clean)) {  # clean, but inconclusive
10853      # by convention: undef $sts_clean means result is inconclusive,
10854      # file appears clean, but continue scanning with other av scanners,
10855      # the current scanner does not want to vouch for it; useful for a
10856      # scanner like jpeg checker which tests for one vulnerability only
10857      do_log(3,"ask_av ($av_name): $f CLEAN, but inconclusive");
10858    } elsif (ref($sts_clean) eq 'ARRAY'
10859                  ? (grep {$_==$t_status} @$sts_clean)
10860                  : ""=~/x{0}/ && $t_output=~/$sts_clean/m) {  # is clean
10861      $scan_status = 0  if !$scan_status;   # no viruses, no errors
10862      do_log(3,"ask_av ($av_name): $f CLEAN");
10863    } else {
10864      do_log(-2,"ask_av ($av_name) FAILED - unexpected result: $t_output");
10865      last;  # error, bail out
10866    }
10867  }
10868  if (!@$bare_fnames) { $scan_status = 0 }  # no errors, no viruses
10869  do_log(3,"$av_name result: clean") if defined($scan_status) && !$scan_status;
10870  ($scan_status,$output,\@virusname);
10871}
10872
10873# Call a virus scanner and parse its output.
10874# Returns a triplet (or die in case of failure).
10875# The first element of the triplet is interpreted as follows:
10876# - true if virus found,
10877# - 0 if no viruses found,
10878# - undef if it did not complete its job;
10879# the second element is a string, the text as provided by the virus scanner;
10880# the third element is ref to a list of virus names found (if any).
10881#   (it is guaranteed the list will be nonempty if virus was found)
10882#
10883sub run_av {
10884  # first three args are prepended, not part of n-tuple
10885  my($bare_fnames,  # a ref to a list of filenames to scan (basenames)
10886     $names_to_parts, # ref to a hash that maps base file names to parts object
10887     $tempdir,      # temporary directory
10888     $av_name, $command, $args,
10889     $sts_clean,    # a ref to a list of status values, or a regexp
10890     $sts_infected, # a ref to a list of status values, or a regexp
10891     $how_to_get_names, # ref to sub, or a regexp to get list of virus names
10892     $pre_code, $post_code,  # routines to be invoked before and after av
10893  ) = @_;
10894  my($scan_status,$virusnames,$error_str); my($output) = '';
10895  &$pre_code(@_)  if defined $pre_code;
10896  if (ref($command) eq 'CODE') {
10897    do_log(3,"Using $av_name: (built-in interface)");
10898    ($scan_status,$output,$virusnames) = &$command(@_);
10899  } else {
10900    my(@args) = split(' ',$args);
10901    if (grep { m{^({}/)?\*\z} } @args) {    #  {}/* or *, list each file
10902      # replace asterisks with bare file names (basenames) if alone or in {}/*
10903      local($1);
10904      @args = map { !m{^({}/)?\*\z} ? $_
10905                                  : map {$1.untaint($_)} @$bare_fnames } @args;
10906    }
10907    for (@args) { s[{}][$tempdir/parts]g }  # replace {} with directory name
10908    # NOTE: RAV does not like '</dev/null' in its command!
10909    ll(3) && do_log(3, "Using ($av_name): " . join(' ',$command,@args));
10910    my($proc_fh,$pid) = run_command(undef, "&1", $command, @args);
10911    while( defined($_ = $proc_fh->getline) ) { $output .= $_ }
10912    my($err); $proc_fh->close or $err=$!; my($child_stat) = $?;
10913    $error_str = exit_status_str($child_stat,$err);
10914    my($retval) = WEXITSTATUS($child_stat);
10915    local($1); chomp($output); my($output_trimmed) = $output;
10916    $output_trimmed =~ s/\r\n/\n/gs;
10917    $output_trimmed =~ s/([ \t\n\r])[ \t\n\r]{4,}/$1.../gs;
10918    $output_trimmed = "..." . substr($output_trimmed,-800)
10919      if length($output_trimmed) > 800;
10920    do_log(3, "run_av: $command $error_str, $output_trimmed");
10921    # braindead Perl: ""=~/x{0}/ serves as explicit default for an empty regexp
10922    if (!WIFEXITED($child_stat)) {
10923    } elsif (defined $sts_infected && (
10924             ref($sts_infected) eq 'ARRAY'
10925                  ? (grep {$_==$retval} @$sts_infected)
10926                  : ""=~/x{0}/ && $output=~/$sts_infected/m)) {  # is infected
10927      # test for infected first, in case both expressions match
10928      $virusnames = [];  # get a list of virus names by parsing output
10929      @$virusnames = ref($how_to_get_names) eq 'CODE'
10930                          ? &$how_to_get_names($output)
10931                          : ""=~/x{0}/ && $output=~/$how_to_get_names/gm;
10932      @$virusnames = map { defined $_ ? $_ : () } @$virusnames;
10933      $scan_status = 1;  # 'true' indicates virus found
10934      do_log(2,"run_av ($av_name): INFECTED: ".join(", ",@$virusnames));
10935    } elsif (!defined($sts_clean)) {  # clean, but inconclusive
10936      # by convention: undef $sts_clean means result is inconclusive,
10937      # file appears clean, but continue scanning with other av scanners,
10938      # the current scanner does not want to vouch for it; useful for a
10939      # scanner like jpeg checker which tests for one vulnerability only
10940      do_log(3,"run_av ($av_name): clean, but inconclusive");
10941    } elsif (ref($sts_clean) eq 'ARRAY' ? (grep {$_==$retval} @$sts_clean)
10942                          : ""=~/x{0}/ && $output=~/$sts_clean/m) {  # is clean
10943      $scan_status = 0;  # 'false' (but defined) indicates no viruses
10944      do_log(5,"run_av ($av_name): clean");
10945    } else {
10946      $error_str = "unexpected $error_str, output=\"$output_trimmed\"";
10947    }
10948    $output = $output_trimmed  if length($output) > 900;
10949  }
10950  &$post_code(@_)  if defined $post_code;
10951  $virusnames = []        if !defined $virusnames;
10952  @$virusnames = (undef)  if $scan_status && !@$virusnames;  # nonnil
10953  if (!defined($scan_status) && defined($error_str)) {
10954    die "$command $error_str";      # die is more informative than return value
10955  }
10956  ($scan_status, $output, $virusnames);
10957}
10958
10959sub virus_scan($$$) {
10960  my($tempdir,$firsttime,$parts_root) = @_;
10961  my($scan_status,$output,@virusname,@detecting_scanners);
10962  my($anyone_done); my($anyone_tried);
10963  my($bare_fnames_ref,$names_to_parts);
10964  my(@errors); my($j); my($tier) = 'primary';
10965  for my $av (@{ca('av_scanners')}, "\000", @{ca('av_scanners_backup')}) {
10966    next  if !defined $av;
10967    if ($av eq "\000") {  # 'magic' separator between lists
10968      last  if $anyone_done;
10969      do_log(-2,"WARN: all $tier virus scanners failed, considering backups");
10970      $tier = 'secondary';  next;
10971    }
10972    next  if !ref $av || !defined $av->[1];
10973    if (!defined $bare_fnames_ref) {  # first time: collect file names to scan
10974      ($bare_fnames_ref,$names_to_parts) =
10975        files_to_scan("$tempdir/parts",$parts_root);
10976      do_log(2, "Not calling virus scanners, ".
10977                "no files to scan in $tempdir/parts")  if !@$bare_fnames_ref;
10978    }
10979    $anyone_tried++; my($this_status,$this_output,$this_vn);
10980    if (!@$bare_fnames_ref) {  # no files to scan?
10981      ($this_status,$this_output,$this_vn) = (0, '', []);  # declare clean
10982    } else {  # call virus scanner
10983      eval {
10984        ($this_status,$this_output,$this_vn) =
10985          run_av($bare_fnames_ref,$names_to_parts,$tempdir, @$av);
10986      };
10987      if ($@ ne '') {
10988        my($err) = $@; chomp($err);
10989        $err = "$av->[0] av-scanner FAILED: $err";
10990        do_log(-2,$err); push(@errors,$err);
10991        $this_status = undef;
10992      };
10993    }
10994    $anyone_done++  if defined $this_status;
10995    $j++; section_time("AV-scan-$j");
10996    if ($this_status) {  # virus detected by this scanner
10997      push(@detecting_scanners, $av->[0]);
10998      if (!@virusname) { # store results of the first scanner detecting
10999        @virusname = @$this_vn;
11000        $scan_status = $this_status; $output = $this_output;
11001      }
11002      last  if c('first_infected_stops_scan');  # stop now if we found a virus?
11003    } elsif (!defined($scan_status)) {  # tentatively keep regardless of status
11004      $scan_status = $this_status; $output = $this_output;
11005    }
11006  }
11007  if (@virusname && @detecting_scanners) {
11008    my(@ds) = @detecting_scanners;  for (@ds) { s/,/;/ }  # facilitates parsing
11009    ll(2) && do_log(2, sprintf("virus_scan: (%s), detected by %d scanners: %s",
11010                      join(', ',@virusname), scalar(@ds), join(', ',@ds)));
11011  }
11012  $output =~ s{\Q$tempdir\E/parts/?}{}gs  if defined $output;  # hide path info
11013  if (!$anyone_tried) { die "NO VIRUS SCANNERS AVAILABLE\n" }
11014  elsif (!$anyone_done)
11015    { die ("ALL VIRUS SCANNERS FAILED: ".join("; ",@errors)."\n") }
11016  ($scan_status, $output, \@virusname, \@detecting_scanners);  # return a quad
11017}
11018
11019# return a ref to a list of files to be scanned in a given directory
11020sub files_to_scan($$) {
11021  my($dir,$parts_root) = @_;
11022  my($names_to_parts) = {};  # a hash that maps base file names
11023                             # to Amavis::Unpackers::Part object
11024  for (my($part), my(@unvisited)=($parts_root);
11025       @unvisited and $part=shift(@unvisited);
11026       push(@unvisited,@{$part->children}))
11027  { # traverse decomposed parts tree breadth-first, match it to actual files
11028    $names_to_parts->{$part->base_name} = $part  if $part ne $parts_root;
11029  }
11030  local(*DIR); my($f); my($bare_fnames_ref) = []; my(%bare_fnames);
11031  opendir(DIR, $dir) or die "Can't open directory $dir: $!";
11032  # traverse parts directory and check for actual files
11033  while (defined($f = readdir(DIR))) {
11034    my($fname) = "$dir/$f";
11035    my($errn) = lstat($fname) ? 0 : 0+$!;
11036    next  if $errn == ENOENT;
11037    if ($errn) { die "files_to_scan: file $fname inaccessible: $!" }
11038    if (!-r _) {  # attempting to gain read access to the file
11039      do_log(3,"files_to_scan: attempting to gain read access to $fname");
11040      chmod(0750,untaint($fname))
11041        or die "files_to_scan: Can't change protection on $fname: $!";
11042      $errn = lstat($fname) ? 0 : 0+$!;
11043      if ($errn) { die "files_to_scan: file $fname inaccessible: $!" }
11044      if (!-r _) { die "files_to_scan: file $fname not readable" }
11045    }
11046    next  if ($f eq '.' || $f eq '..') && -d _;  # this or the parent directory
11047    if (!-f _ || !exists $names_to_parts->{$f}) { # nonregular f. or unexpected
11048      my($what) = -l _ ? 'symlink' : -d _ ? 'directory' : -f _ ? 'file'
11049                 : 'non-regular file';
11050      my($msg) = "removing unexpected $what $fname";
11051      $msg .= ", it has no corresponding parts object"
11052        if !exists $names_to_parts->{$f};
11053      do_log(-1, "WARN: files_to_scan: ".$msg);
11054      if (-d _) { rmdir_recursively(untaint($fname)) }
11055      else { unlink(untaint($fname)) or die "Can't delete $what $fname: $!" }
11056    } elsif (-z _) {
11057      # empty file
11058    } else {
11059      if ($f !~ /^[A-Za-z0-9_.-]+\z/s)
11060        {do_log(-1,"WARN: files_to_scan: unexpected/suspicious file name: $f")}
11061      push(@$bare_fnames_ref,$f); $bare_fnames{$f} = 1;
11062    }
11063  }
11064  closedir(DIR) or die "Can't close directory $dir: $!";
11065  # remove entries from %$names_to_parts that have no corresponding files
11066  my($fname,$part);
11067  while ( ($fname,$part) = each %$names_to_parts ) {
11068    next  if exists $bare_fnames{$fname};
11069    if ($part->exists) {
11070      my($type_short) = $part->type_short;
11071      ll(4) && do_log(4,sprintf(
11072                         "files_to_scan: info: part %s (%s) no longer present",
11073         $fname, (!ref $type_short ? $type_short : join(', ',@$type_short)) ));
11074    }
11075    delete $names_to_parts->{$fname}; # delete is allowed for the current elem.
11076  }
11077  ($bare_fnames_ref, $names_to_parts);
11078}
11079
110801;
11081
11082__DATA__
11083#
11084package Amavis::SpamControl;
11085use strict;
11086use re 'taint';
11087
11088BEGIN {
11089  use Exporter ();
11090  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
11091  $VERSION = '2.034';
11092  @ISA = qw(Exporter);
11093}
11094use FileHandle;
11095use Mail::SpamAssassin;
11096
11097BEGIN {
11098  import Amavis::Conf qw(:platform :sa $daemon_user c cr ca);
11099  import Amavis::Util qw(ll do_log retcode exit_status_str run_command
11100                         prolong_timer);
11101  import Amavis::rfc2821_2822_Tools;
11102  import Amavis::Timing qw(section_time);
11103  import Amavis::Lookup qw(lookup);
11104}
11105
11106use subs @EXPORT_OK;
11107
11108use vars qw($spamassassin_obj);
11109
11110# called at startup, before the main fork
11111sub init() {
11112  do_log(1, "SpamControl: initializing Mail::SpamAssassin");
11113  my($saved_umask) = umask;
11114  $spamassassin_obj = Mail::SpamAssassin->new({
11115    debug => $sa_debug,
11116    save_pattern_hits => $sa_debug,
11117    dont_copy_prefs   => 1,
11118    local_tests_only  => $sa_local_tests_only,
11119    home_dir_for_helpers => $helpers_home,
11120    stop_at_threshold => 0,
11121#   DEF_RULES_DIR     => '/usr/local/share/spamassassin',
11122#   LOCAL_RULES_DIR   => '/etc/mail/spamassassin',
11123#see man Mail::SpamAssassin for other options
11124  });
11125# $Mail::SpamAssassin::DEBUG->{rbl}=-3;
11126# $Mail::SpamAssassin::DEBUG->{dcc}=-3;
11127# $Mail::SpamAssassin::DEBUG->{pyzor}=-3;
11128# $Mail::SpamAssassin::DEBUG->{bayes}=-3;
11129# $Mail::SpamAssassin::DEBUG->{rulesrun}=4+64;
11130  if ($sa_auto_whitelist && Mail::SpamAssassin::Version() < 3) {
11131    do_log(1, "SpamControl: turning on SA auto-whitelisting (AWL)");
11132    # create a factory for the persistent address list
11133    my($addrlstfactory) = Mail::SpamAssassin::DBBasedAddrList->new;
11134    $spamassassin_obj->set_persistent_address_list_factory($addrlstfactory);
11135  }
11136  $spamassassin_obj->compile_now;     # ensure all modules etc. are preloaded
11137  alarm(0);              # seems like SA forgets to clear alarm in some cases
11138  umask($saved_umask);   # restore our umask, SA clobbered it
11139  do_log(1, "SpamControl: done");
11140}
11141
11142# check envelope sender if white or blacklisted by each recipient;
11143# Saves the result in recip_blacklisted_sender and recip_whitelisted_sender
11144# properties of each recipient object.
11145#
11146sub white_black_list($$$$$) {
11147  my($conn,$msginfo,$sql_wblist,$user_id_sql,$ldap_policy) = @_;
11148  my($any_w)=0; my($any_b)=0; my($all)=1; my($wr,$br);
11149  my($sender) = $msginfo->sender;
11150  do_log(4,"wbl: checking sender <$sender>");
11151  for my $r (@{$msginfo->per_recip_data}) {
11152    next  if $r->recip_done;  # already dealt with
11153    my($found,$wb,$boost); my($recip) = $r->recip_addr;
11154    my($user_id_ref,$mk_ref) = !defined $sql_wblist ? ([],[])
11155                                 : lookup(1,$recip,$user_id_sql);
11156    do_log(5,"wbl: (SQL) recip <$recip>, ".scalar(@$user_id_ref)." matches")
11157      if defined $sql_wblist && ll(5);
11158    for my $ind (0..$#{$user_id_ref}) {  # for ALL SQL sets matching the recip
11159      my($user_id) = $user_id_ref->[$ind];  my($mkey);
11160      ($wb,$mkey) = lookup(0,$sender,
11161                Amavis::Lookup::SQLfield->new($sql_wblist,'wb','S',$user_id) );
11162      do_log(4,"wbl: (SQL) recip <$recip>, rid=$user_id, got: \"$wb\"");
11163      if (!defined($wb)) {  # NULL field or no match: remains undefined
11164      } elsif ($wb =~ /^ *([+-]?\d+(?:\.\d*)?) *\z/) {  # numeric
11165        my($val) = 0+$1;    # penalty points to be added to the score
11166        $boost += $val;
11167        ll(2) && do_log(2,sprintf(
11168                  "wbl: (SQL) soft-%slisted (%s) sender <%s> => <%s> (rid=%s)",
11169                  ($val<0?'white':'black'), $val, $sender, $recip, $user_id));
11170        $wb = undef;  # not hard- white or blacklisting
11171      } elsif ($wb =~ /^[ \000]*\z/) {        # neutral, stops the search
11172        $found++; $wb = 0;
11173        do_log(5,"wbl: (SQL) recip <$recip> is neutral to sender <$sender>");
11174      } elsif ($wb =~ /^([BbNnFf])[ ]*\z/) {  # blacklisted (B, N, F)
11175        $found++; $wb = -1; $any_b++; $br = $recip;
11176        $r->recip_blacklisted_sender(1);
11177        do_log(5,"wbl: (SQL) recip <$recip> blacklisted sender <$sender>");
11178      } else {                         # whitelisted (W, Y, T) or anything else
11179        if ($wb =~ /^([WwYyTt])[ ]*\z/) {
11180          do_log(5, "wbl: (SQL) recip <$recip> whitelisted sender <$sender>");
11181        } else {
11182          do_log(-1,"wbl: (SQL) recip <$recip> whitelisted sender <$sender>, ".
11183                    "unexpected wb field value: \"$wb\"");
11184        }
11185        $found++; $wb = +1; $any_w++; $wr = $recip;
11186        $r->recip_whitelisted_sender(1);
11187      }
11188      last  if $found;
11189    }
11190    if (!$found && defined($ldap_policy)) {
11191      my($wblist);
11192      my($keys_ref,$rhs_ref) = make_query_keys($sender,0,0);
11193      my(@keys) = @$keys_ref;
11194      do_log(5,sprintf("wbl: (LDAP) query keys: %s",
11195                       join(', ',map{"\"$_\""}@keys)));
11196      $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new($ldap_policy,'amavisBlacklistSender','L-'));
11197      for my $key (@keys) {
11198        if (grep {/^\Q$key\E\z/i} @$wblist) {
11199          $found++; $wb = -1; $br = $recip; $any_b++;
11200          $r->recip_blacklisted_sender(1);
11201          do_log(5,"wbl: (LDAP) recip <$recip> blacklisted sender <$sender>");
11202        }
11203      }
11204      $wblist = lookup(0,$recip,Amavis::Lookup::LDAPattr->new($ldap_policy,'amavisWhitelistSender','L-'));
11205      for my $key (@keys) {
11206        if (grep {/^\Q$key\E\z/i} @$wblist) {
11207          $found++; $wb = +1; $wr = $recip; $any_w++;
11208          $r->recip_whitelisted_sender(1);
11209          do_log(5,"wbl: (LDAP) recip <$recip> whitelisted sender <$sender>");
11210        }
11211      }
11212    }
11213    if (!$found) {  # fall back to static lookups if no match
11214      # sender can be both white- and blacklisted at the same time
11215      my($val); my($r_ref,$mk_ref,@t);
11216
11217      # NOTE on the specifics of $per_recip_blacklist_sender_lookup_tables :
11218      # the $r_ref below is supposed to be a ref to a single lookup table
11219      # for compatibility with pre-2.0 versions of amavisd-new;
11220      # Note that this is different from @score_sender_maps, which is
11221      # supposed to contain a ref to a _list_ of lookup tables as a result
11222      # of the first-level lookup (on the recipient address as a key).
11223      #
11224      ($r_ref,$mk_ref) = lookup(0,$recip,
11225                         Amavis::Lookup::Label->new("blacklist_recip<$recip>"),
11226                         cr('per_recip_blacklist_sender_lookup_tables'));
11227      @t = ( (defined $r_ref ? $r_ref : ()), @{ca('blacklist_sender_maps')} );
11228      $val = lookup(0,$sender,
11229                    Amavis::Lookup::Label->new("blacklist_sender<$sender>"),
11230                    @t)  if @t;
11231      if ($val) {
11232        $found++; $wb = -1; $br = $recip; $any_b++;
11233        $r->recip_blacklisted_sender(1);
11234        do_log(5,"wbl: recip <$recip> blacklisted sender <$sender>");
11235      }
11236      # similar for whitelists:
11237      ($r_ref,$mk_ref) = lookup(0,$recip,
11238                         Amavis::Lookup::Label->new("whitelist_recip<$recip>"),
11239                         cr('per_recip_whitelist_sender_lookup_tables'));
11240      @t = ( (defined $r_ref ? $r_ref : ()), @{ca('whitelist_sender_maps')} );
11241      $val = lookup(0,$sender,
11242                    Amavis::Lookup::Label->new("whitelist_sender<$sender>"),
11243                    @t)  if @t;
11244      if ($val) {
11245        $found++; $wb = +1; $wr = $recip; $any_w++;
11246        $r->recip_whitelisted_sender(1);
11247        do_log(5,"wbl: recip <$recip> whitelisted sender <$sender>");
11248      }
11249    }
11250    if (!defined($boost)) {        # static lookups if no match
11251      # note the first argument of lookup() is true, requesting ALL matches
11252      my($r_ref,$mk_ref) = lookup(1,$recip,
11253                             Amavis::Lookup::Label->new("score_recip<$recip>"),
11254                             @{ca('score_sender_maps')});
11255      for my $j (0..$#{$r_ref}) {  # for ALL tables matching the recipient
11256        my($val,$key) = lookup(0,$sender,
11257                           Amavis::Lookup::Label->new("score_sender<$sender>"),
11258                           @{$r_ref->[$j]} );
11259        if ($val != 0) {
11260          $boost += $val;
11261          ll(2) && do_log(2,
11262                       sprintf("wbl: soft-%slisted (%s) sender <%s> => <%s>, ".
11263                               "recip_key=\"%s\"", ($val<0?'white':'black'),
11264                               $val, $sender, $recip, $mk_ref->[$j]));
11265        }
11266      }
11267    }
11268    $r->recip_score_boost($boost)  if defined $boost;
11269    $all = 0  if !$wb;
11270  }
11271  if (!ll(2)) {
11272    # don't bother preparing log report which will not be printed
11273  } else {
11274    my($msg) = '';
11275    if    ($all && $any_w && !$any_b) { $msg = "whitelisted" }
11276    elsif ($all && $any_b && !$any_w) { $msg = "blacklisted" }
11277    elsif ($all) { $msg = "black or whitelisted by all recips" }
11278    elsif ($any_b || $any_w) {
11279      $msg .= "whitelisted by ".($any_w>1?"$any_w recips, ":"$wr, ") if $any_w;
11280      $msg .= "blacklisted by ".($any_b>1?"$any_b recips, ":"$br, ") if $any_b;
11281      $msg .= "but not by all,";
11282    }
11283    do_log(2,"wbl: $msg sender <$sender>")  if $msg ne '';
11284  }
11285  ($any_w+$any_b, $all);
11286}
11287
11288# - returns true if spam detected,
11289# - returns 0 if no spam found,
11290# - throws exception (die) in case of errors,
11291#   or just returns undef if it did not complete its jobs
11292#
11293sub spam_scan($$) {
11294  my($conn,$msginfo) = @_;
11295  my($spam_level, $spam_status, $spam_report); my(@lines);
11296  my($hdr_edits) = $msginfo->header_edits;
11297  if (!$hdr_edits) {
11298    $hdr_edits = Amavis::Out::EditHeader->new;
11299    $msginfo->header_edits($hdr_edits);
11300  }
11301  my($dspam_signature,$dspam_result,$dspam_fname);
11302  push(@lines, sprintf("Return-Path: %s\n",      # fake a local delivery agent
11303    qquote_rfc2821_local($msginfo->sender)));
11304  push(@lines, sprintf("X-Envelope-To: %s\n",
11305                      join(",\n ",qquote_rfc2821_local(@{$msginfo->recips}))));
11306  my($fh) = $msginfo->mail_text;
11307  my($mbsl) = c('sa_mail_body_size_limit');
11308  if ( defined $mbsl &&
11309       ($msginfo->orig_body_size > $mbsl ||
11310        $msginfo->orig_header_size + 1 + $msginfo->orig_body_size
11311          > 5*1024 + $mbsl)
11312     ) {
11313    do_log(1,"spam_scan: not wasting time on SA, message ".
11314             "longer than $mbsl bytes: ".
11315             $msginfo->orig_header_size .'+'. $msginfo->orig_body_size);
11316  } else {
11317    if ($dspam eq '') {
11318      do_log(5,"spam_scan: DSPAM not available, skipping it");
11319    } else {
11320      # pass the mail to DSPAM, extract its result headers and feed them to SA
11321      $dspam_fname = $msginfo->mail_tempdir . '/dspam.msg';
11322      my($dspam_fh) = IO::File->new;  # will receive output from DSPAM
11323      $dspam_fh->open($dspam_fname,'>',0640)
11324        or die "Can't create file $dspam_fname: $!";
11325      $fh->seek(0,0) or die "Can't rewind mail file: $!";
11326      my($proc_fh,$pid) = run_command('&'.fileno($fh), "&1", $dspam,
11327              qw(--stdout --deliver=spam,innocent
11328                 --mode=tum --feature=chained,noise
11329                 --enable-signature-headers
11330                 --user), $daemon_user,
11331            );  # --mode=teft
11332            # qw(--stdout --deliver-spam)  # dspam < 3.0
11333      # keep X-DSPAM-*, ignore other changes e.g. Content-Transfer-Encoding
11334      my($all_local) = !grep { !lookup(0,$_,@{ca('local_domains_maps')}) }
11335                             @{$msginfo->recips};
11336      my($first_line);
11337      while (defined($_ = $proc_fh->getline)) {  # scan mail header from DSPAM
11338        $dspam_fh->print($_) or die "Can't write to $dspam_fname: $!";
11339        if (!defined($first_line))
11340          { $first_line = $_; do_log(5,"spam_scan: from DSPAM: $first_line") }
11341        last  if $_ eq $eol;
11342        local($1,$2);
11343        if (/^(X-DSPAM[^:]*):[ \t]*(.*)$/) {  # note: does not handle folding
11344          my($hh,$hb) = ($1,$2);
11345          $dspam_signature = $hb  if /^X-DSPAM-Signature:/i;
11346          $dspam_result    = $hb  if /^X-DSPAM-Result:/i;
11347          do_log(3,$_); push(@lines,$_);   # store header in array passed to SA
11348          # add DSPAM header fields to passed mail
11349          $hdr_edits->append_header($hh,$hb)  if $all_local;
11350        }
11351      }
11352      while ($proc_fh->read($_,16384) > 0) {     # copy mail body from DSPAM
11353        $dspam_fh->print($_) or die "Can't write to $dspam_fname: $!";
11354      }
11355      my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
11356      $dspam_fh->close or die "Can't close $dspam_fname: $!";
11357      do_log(-1,sprintf("WARN: DSPAM problem, %s, result=%s",
11358                        exit_status_str($?,$err), $first_line)
11359            )  if $retval || !defined $first_line;
11360      do_log(4,"spam_scan: DSPAM gave: $dspam_signature, $dspam_result");
11361      section_time('DSPAM');
11362    }
11363    # read mail into memory in preparation for SpamAssasin
11364    my($body_lines) = 0;
11365    $fh->seek(0,0) or die "Can't rewind mail file: $!";
11366    while (<$fh>) { push(@lines,$_); last if $_ eq $eol }  # header
11367    while (<$fh>) { push(@lines,$_); $body_lines++ }       # body
11368    section_time('SA msg read');
11369
11370    my($sa_required, $sa_tests);
11371    my($saved_umask) = umask;
11372    my($remaining_time) = alarm(0);  # check how much time is left
11373    eval {
11374      # NOTE ON TIMEOUTS: SpamAssassin may use timer for its own purpose,
11375      # disabling it before returning. It seems it only uses timer when
11376      # external tests are enabled, so in order for our timeout to be
11377      # useful, $sa_local_tests_only needs to be true (e.g. 1).
11378      local $SIG{ALRM} = sub {
11379        my($s) = Carp::longmess("SA TIMED OUT, backtrace:");
11380        # crop at some rather arbitrary limit
11381        if (length($s) > 900) { $s = substr($s,0,900-3) . "..." }
11382        do_log(-1,$s);
11383      };
11384      # prepared to wait no more than n seconds
11385      alarm($sa_timeout)  if $sa_timeout > 0;
11386      my($mail_obj); my($sa_version) = Mail::SpamAssassin::Version();
11387      do_log(5,"calling SA parse, SA version $sa_version");
11388      # *** note that $sa_version could be 3.0.1, which is not really numeric!
11389      if ($sa_version >= 3) {
11390        $mail_obj = $spamassassin_obj->parse(\@lines);
11391      } else {  # 2.63 or earlier
11392        $mail_obj = Mail::SpamAssassin::NoMailAudit->new(data => \@lines,
11393                                                         add_From_line => 0);
11394      }
11395      section_time('SA parse');
11396      do_log(4,"CALLING SA check");
11397      my($per_msg_status);
11398      { local($1,$2,$3,$4,$5,$6);  # avoid Perl 5.8.0 bug, $1 gets tainted
11399        $per_msg_status = $spamassassin_obj->check($mail_obj);
11400      }
11401      my($rem_t) = alarm(0);
11402      do_log(4,"RETURNED FROM SA check, time left: $rem_t s");
11403
11404      { local($1,$2,$3,$4);  # avoid Perl 5.8.0..5.8.3...? taint bug
11405        $spam_level  = $per_msg_status->get_hits;
11406        $sa_required = $per_msg_status->get_required_hits; # not used
11407        $sa_tests    = $per_msg_status->get_names_of_tests_hit;
11408        $spam_report = $per_msg_status->get_report;  # taints $1 and $2 !
11409
11410      # example of how to gather aditional information from SA:
11411      # my($trusted) = $per_msg_status->_get_tag('RELAYSTRUSTED');
11412      # $hdr_edits->append_header('X-TESTING',$trusted);
11413
11414      #Experimental, unfinished:
11415      # $per_msg_status->rewrite_mail;
11416      # my($entity) = nomailaudit_to_mime_entity($mail_obj);
11417
11418        $per_msg_status->finish();
11419      }
11420    };
11421    section_time('SA check');
11422    umask($saved_umask);  # SA changes umask to 0077
11423    prolong_timer('spam_scan_SA', $remaining_time); # restart the timer
11424    if ($@ ne '') {  # SA timed out?
11425      chomp($@);
11426      die "$@\n"  if $@ ne "timed out";
11427    }
11428    $sa_tests =~ s/,\s*/,/g;  $spam_status = "tests=".$sa_tests;
11429
11430    if ($dspam ne '' && defined $spam_level) {  # DSPAM auto-learn
11431      my($eat,@options);
11432      @options = (qw(--stdout --mode=tum --user), $daemon_user);  # --mode=teft
11433      if (   $spam_level >  5.0 && $dspam_result eq 'Innocent') {
11434        $eat = 'SPAM'; push(@options, qw(--class=spam --source=error));
11435#       @options = qw(--stdout --addspam);  # dspam < 3.0
11436      }
11437      elsif ($spam_level <  1.0 && $dspam_result eq 'Spam') {
11438        $eat = 'HAM'; push(@options, qw(--class=innocent --source=error));
11439#       @options = qw(--stdout --falsepositive);  # dspam < 3.0
11440      }
11441      if (defined $eat && $dspam_signature ne '') {
11442        do_log(2,"DSPAM learn $eat ($spam_level), $dspam_signature");
11443        my($proc_fh,$pid) = run_command($dspam_fname, "&1", $dspam, @options);
11444        while (defined($_ = $proc_fh->getline)) {}  # consume possible output
11445#       my($output) = join('', $proc_fh->getlines); # consume possible output
11446        my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
11447#       do_log(-1,"DSPAM learn $eat response:".$output)  if $output ne '';
11448        $retval==0 or die ("DSPAM learn $eat FAILED: ".exit_status_str($?,$err));
11449        section_time('DSPAM learn');
11450      }
11451    }
11452  }
11453  if (defined $dspam_fname) {
11454    if (($spam_level > 5.0 ? 1 : 0) != ($dspam_result eq 'Spam' ? 1 : 0))
11455      { do_log(2,"DSPAM: different opinions: $dspam_result, $spam_level") }
11456    unlink($dspam_fname) or die "Can't delete file $dspam_fname: $!";
11457  }
11458  do_log(3,"spam_scan: hits=$spam_level $spam_status");
11459  ($spam_level, $spam_status, $spam_report);
11460}
11461
11462#sub nomailaudit_to_mime_entity($) {
11463# my($mail_obj) = @_;  # expect a Mail::SpamAssassin::MsgContainer object
11464# my(@m_hdr) = $mail_obj->header;  # in array context returns array of lines
11465# my($m_body) = $mail_obj->body;   # returns array ref
11466# my($entity);
11467# # make sure _our_ source line number is reported in case of failure
11468# eval {$entity = MIME::Entity->build(
11469#                              Type => 'text/plain', Encoding => '-SUGGEST',
11470#                              Data => $m_body); 1}  or do {chomp($@); die $@};
11471# my($head) = $entity->head;
11472# # insert header fields from template into MIME::Head entity
11473# for my $hdr_line (@m_hdr) {
11474#   # make sure _our_ source line number is reported in case of failure
11475#   eval {$head->replace($fhead,$fbody); 1} or do {chomp($@); die $@};
11476# }
11477# $entity;  # return the built MIME::Entity
11478#}
11479
114801;
11481
11482__DATA__
11483#
11484package Amavis::Unpackers;
11485use strict;
11486use re 'taint';
11487
11488BEGIN {
11489  use Exporter ();
11490  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
11491  $VERSION = '2.034';
11492  @ISA = qw(Exporter);
11493  %EXPORT_TAGS = ();
11494  @EXPORT = ();
11495  @EXPORT_OK = qw(&init &decompose_part &determine_file_types);
11496}
11497use Errno qw(ENOENT EACCES);
11498use File::Basename qw(basename);
11499use Convert::TNEF;
11500use Convert::UUlib qw(:constants);
11501use Compress::Zlib;
11502use Archive::Tar;
11503use Archive::Zip qw(:CONSTANTS :ERROR_CODES);
11504use File::Copy;
11505
11506BEGIN {
11507  import Amavis::Util qw(untaint min max ll do_log retcode exit_status_str
11508                         snmp_count prolong_timer sanitize_str run_command
11509                         rmdir_recursively);
11510  import Amavis::Conf qw(:platform :confvars :unpack c cr ca);
11511  import Amavis::Timing qw(section_time);
11512  import Amavis::Lookup qw(lookup);
11513  import Amavis::Unpackers::MIME qw(mime_decode);
11514  import Amavis::Unpackers::NewFilename qw(consumed_bytes);
11515}
11516
11517use subs @EXPORT_OK;
11518
11519# recursively descend into a directory $dir containing potentially unsafe
11520# files with unpredictable names, soft links, etc., rename each regular
11521# nonempty file to directory $outdir giving it a generated name,
11522# and discard all the rest, including the directory $dir.
11523# Return a pair: number of bytes that 'sanitized' files now occupy,
11524# and a number of parts objects created.
11525#
11526sub flatten_and_tidy_dir($$$;$$);  # prototype
11527sub flatten_and_tidy_dir($$$;$$) {
11528  my($dir, $outdir, $parent_obj, $item_num_offset, $orig_names) = @_;
11529  do_log(4, "flatten_and_tidy_dir: processing directory \"$dir\"");
11530  my($cnt_r,$cnt_u) = (0,0); my($consumed_bytes) = 0;
11531  local(*DIR); my($f);
11532  chmod(0750, $dir) or die "Can't change protection of \"$dir\": $!";
11533  opendir(DIR, $dir) or die "Can't open directory \"$dir\": $!";
11534  my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement;
11535  while (defined($f = readdir(DIR))) {
11536    my($msg);  my($fname) = "$dir/$f";
11537    my($errn) = lstat($fname) ? 0 : 0+$!;
11538    if    ($errn == ENOENT) { $msg = "does not exist" }
11539    elsif ($errn)           { $msg = "inaccessible: $!" }
11540    if (defined $msg) { die "flatten_and_tidy_dir: \"$fname\" $msg," }
11541    next  if ($f eq '.' || $f eq '..') && -d _;
11542    my($newpart_obj) = Amavis::Unpackers::Part->new($outdir,$parent_obj);
11543    $item_num++;
11544    $newpart_obj->mime_placement(sprintf("%s/%d",$parent_placement,
11545                                                 $item_num+$item_num_offset) );
11546    # save tainted original member name if available, or a tainted file name
11547    my($original_name) = !ref($orig_names) ? undef : $orig_names->{$f};
11548    $newpart_obj->name_declared(defined $original_name ? $original_name : $f);
11549    # untaint, but if $dir happens to still be tainted, we want to know and die
11550    $fname = $dir.'/'.untaint($f);
11551    if (-d _) {
11552      $newpart_obj->attributes_add('D');
11553      my($bytes,$cnt) = flatten_and_tidy_dir($fname, $outdir, $parent_obj,
11554                                      $item_num+$item_num_offset, $orig_names);
11555      $consumed_bytes += $bytes; $item_num += $cnt;
11556    } elsif (-l _) {
11557      $cnt_u++; $newpart_obj->attributes_add('L');
11558      unlink($fname) or die "Can't remove soft link \"$fname\": $!";
11559    } elsif (!-f _) {
11560      do_log(4, "flatten_and_tidy_dir: NONREGULAR FILE \"$fname\"");
11561      $cnt_u++; $newpart_obj->attributes_add('S');
11562      unlink($fname) or die "Can't remove nonregular file \"$fname\": $!";
11563    } elsif (-z _) {
11564      $cnt_u++;
11565      unlink($fname) or die "Can't remove empty file \"$fname\": $!";
11566    } else {
11567      chmod(0750, $fname)
11568        or die "Can't change protection of file \"$fname\": $!";
11569      my($size) = 0 + (-s _);
11570      $newpart_obj->size($size);
11571      $consumed_bytes += $size;
11572      my($newpart) = $newpart_obj->full_name;
11573      ll(5) && do_log(5,
11574        sprintf("flatten_and_tidy_dir: renaming \"%s\"%s to %s", $fname,
11575                !defined $original_name ? '' : " ($original_name)", $newpart));
11576      $cnt_r++;
11577      rename($fname, $newpart)
11578        or die "Can't rename \"$fname\" to $newpart: $!";
11579    }
11580  }
11581  closedir(DIR) or die "Can't close directory \"$dir\": $!";
11582  rmdir($dir)   or die "Can't remove directory \"$dir\": $!";
11583  section_time("ren$cnt_r-unl$cnt_u-files$item_num");
11584  ($consumed_bytes, $item_num);
11585}
11586
11587# call 'file(1)' utility for each part,
11588# and associate (save) full and short types with each part
11589#
11590sub determine_file_types($$) {
11591  my($tempdir, $partslist_ref) = @_;
11592  $file ne '' or die "Unix utility file(1) not available, but is needed";
11593  my($cwd) = "$tempdir/parts";
11594  my(@part_list) = grep { $_->exists } @$partslist_ref;
11595  if (!@part_list) { do_log(5, "no parts, file(1) not called") }
11596  else {
11597    local($1,$2); # avoid Perl taint bug (5.8.3), $cwd and $arg are not tainted
11598                  # but $arg becomes tainted because $1 is tainted from before
11599    my(@file_list);
11600    for my $part (@part_list) {
11601      my($arg) = $part->full_name;
11602      $arg =~ s{^\Q$cwd\E/(.*)\z}{$1}s;    # remove cwd if possible
11603      push(@file_list, $arg);
11604    }
11605    chdir($cwd) or die "Can't chdir to $cwd: $!";
11606    my($proc_fh,$pid) = run_command(undef, "&1", $file, @file_list);
11607    chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
11608    local($_); my($index) = 0;
11609    while (defined($_ = $proc_fh->getline)) {
11610      chomp;
11611      do_log(5, "result line from file(1): ".$_);
11612      if ($index > $#file_list) {
11613        do_log(-1, "NOTICE: Skipping extra output from file(1): $_");
11614      } else {
11615        my($part)   = $part_list[$index];  # walk through @part_list in sync
11616        my($expect) = $file_list[$index];  # walk through @file_list in sync
11617        if (!/^(\Q$expect\E):[ \t]*(.*)\z/s) {    # split file name from type
11618          do_log(-1,"NOTICE: Skipping bad output from file(1) ".
11619                    "at [$index, $expect], got: $_");
11620        } else {
11621          my($type_short); my($actual_name) = $1; my($type_long) = $2;
11622          $type_short = lookup(0,$type_long,@map_full_type_to_short_type_maps);
11623          do_log(4, sprintf("File-type of %s: %s%s",
11624                            $part->base_name, $type_long,
11625                            (!defined $type_short ? ''
11626                               : !ref $type_short ? "; ($type_short)"
11627                               : '; (' . join(', ',@$type_short) . ')'
11628                            ) ));
11629          $part->type_long($type_long); $part->type_short($type_short);
11630          $part->attributes_add('C')
11631            if !ref($type_short) ? $type_short eq 'pgp'  # encrypted?
11632                                 : grep {$_ eq 'pgp'} @$type_short;
11633          $index++;
11634        }
11635      }
11636    }
11637    if ($index < @part_list) {
11638      die sprintf("parsing file(1) results - missing last %d results",
11639                  @part_list - $index);
11640    }
11641    my($err); $proc_fh->close or $err = $!;
11642    $?==0 or die ("'file' utility ($file) failed, ".exit_status_str($?,$err));
11643    section_time(sprintf('get-file-type%d', scalar(@part_list)));
11644  }
11645}
11646
11647sub decompose_mail($$) {
11648  my($tempdir,$file_generator_object) = @_;
11649
11650  my($hold); my(@parts); my($depth) = 1; my($any_undecipherable) = 0;
11651  my($which_section) = "parts_decode";
11652  # fetch all not-yet-visited part names, and start a new cycle
11653TIER:
11654  while (@parts = @{$file_generator_object->parts_list}) {
11655    if ($MAXLEVELS && $depth > $MAXLEVELS) {
11656      $hold = "Maximum decoding depth ($MAXLEVELS) exceeded";
11657      last;
11658    }
11659    $file_generator_object->parts_list_reset;  # new names cycle
11660    # clip to avoid very long log entries
11661    my(@chopped_parts) = @parts > 5 ? @parts[0..4] : @parts;
11662    ll(4) && do_log(4,sprintf("decode_parts: level=%d, #parts=%d : %s",
11663                     $depth, scalar(@parts),
11664                     join(', ', (map { $_->base_name } @chopped_parts),
11665                     (@chopped_parts >= @parts ? () : "...")) ));
11666    for my $part (@parts) {  # test for existence of all expected files
11667      my($fname) = $part->full_name;
11668      my($errn) = lstat($fname) ? 0 : 0+$!;
11669      if ($errn == ENOENT) {
11670        $part->exists(0);
11671#       $part->type_short('no-file')  if !defined $part->type_short;
11672      } elsif ($errn) {
11673        die "decompose_mail: inaccessible file $fname: $!";
11674      } elsif (!-f _) {  # not a regular file
11675        my($what) = -l _ ? 'symlink' : -d _ ? 'directory' : 'non-regular file';
11676        do_log(-1, "WARN: decompose_mail: removing unexpected $what $fname");
11677        if (-d _) { rmdir_recursively($fname) }
11678        else { unlink($fname) or die "Can't delete $what $fname: $!" }
11679        $part->exists(0);
11680        $part->type_short(-l _ ? 'symlink' : -d _ ? 'dir' : 'special')
11681          if !defined $part->type_short;
11682      } elsif (-z _) {   # empty file
11683        unlink($fname) or die "Can't remove \"$fname\": $!";
11684        $part->exists(0);
11685        $part->type_short('empty')  if !defined $part->type_short;
11686        $part->type_long('empty')   if !defined $part->type_long;
11687      } else {
11688        $part->exists(1);
11689      }
11690    }
11691    determine_file_types($tempdir, \@parts);
11692    for my $part (@parts) {
11693      if ($part->exists && !defined($hold))
11694        { $hold = decompose_part($part, $tempdir) }
11695      $any_undecipherable++  if grep {$_ eq 'U'} @{ $part->attributes || [] };
11696    }
11697    last TIER  if defined $hold;
11698    $depth++;
11699  }
11700  section_time($which_section); prolong_timer($which_section);
11701  ($hold, $any_undecipherable);
11702}
11703
11704# Decompose the part
11705sub decompose_part($$) {
11706  my($part, $tempdir) = @_;
11707  my($hold);
11708  my($none_called);
11709  # possible return values from eval:
11710  # 0 - truly atomic, or unknown or archiver failure; consider atomic
11711  # 1 - some archiver format, successfully unpacked, result replaces original
11712  # 2 - probably unpacked, but keep the original (eg self-extracting archive)
11713  my($sts) = eval {
11714    my($type_short) = $part->type_short;
11715    my(@ts) = !defined $type_short ? ()
11716                : !ref $type_short ? ($type_short) : @$type_short;
11717    return 0  if !@ts;  # consider atomic if unknown (returns from eval)
11718    snmp_count("OpsDecType-".join('.',@ts));
11719
11720    grep(/^mail\z/,@ts) && return do {mime_decode($part,$tempdir,$part); 2};
11721    grep(/^(asc|uue|hqx|ync)\z/,@ts) && return do_ascii($part,$tempdir);
11722    grep(/^F\z/,@ts)    && defined $unfreeze
11723              && return do_uncompress($part,$tempdir,$unfreeze);
11724    grep(/^Z\z/,@ts)    && defined $uncompress
11725              && return do_uncompress($part,$tempdir,$uncompress);
11726    grep(/^bz2?\z/,@ts) && defined $bzip2
11727              && return do_uncompress($part,$tempdir,"$bzip2 -d");
11728    grep(/^gz\z/,@ts)   && defined $gzip
11729              && return do_uncompress($part,$tempdir,"$gzip -d");
11730    grep(/^gz\z/,@ts)   && return do_gunzip($part,$tempdir);  # fallback
11731    grep(/^lzo\z/,@ts)  && defined $lzop
11732              && return do_uncompress($part,$tempdir,"$lzop -d");
11733    grep(/^rpm\z/,@ts)  && defined $rpm2cpio && (defined $pax || defined $cpio)
11734              && return do_uncompress($part,$tempdir,$rpm2cpio);
11735    grep(/^(cpio|tar)\z/,@ts) && (defined $pax || defined $cpio)
11736              && return do_pax_cpio($part,$tempdir, $pax || $cpio);
11737    grep(/^tar\z/,@ts)  && return do_tar($part,$tempdir);  # fallback
11738    grep(/^deb\z/,@ts)  && defined $ar
11739              && return do_ar($part,$tempdir);
11740#   grep(/^a\z/,@ts)    && defined $ar   # unpacking .a seems like overkill
11741#             && return do_ar($part,$tempdir);
11742    grep(/^zip\z/,@ts)  && return do_unzip($part,$tempdir);
11743    grep(/^rar\z/,@ts)  && defined $unrar
11744              && return do_unrar($part,$tempdir);
11745    grep(/^(lha|lzh)\z/,@ts) && defined $lha
11746              && return do_lha($part,$tempdir);
11747    grep(/^arc\z/,@ts)  && defined $arc
11748              && return do_arc($part,$tempdir);
11749    grep(/^arj\z/,@ts)  && defined $unarj
11750              && return do_unarj($part,$tempdir);
11751    grep(/^zoo\z/,@ts)  && defined $zoo
11752              && return do_zoo($part,$tempdir);
11753    grep(/^cab\z/,@ts)  && defined $cabextract
11754              && return do_cabextract($part,$tempdir);
11755    grep(/^doc\z/,@ts)  && defined $ripole
11756              && return do_ole($part,$tempdir);
11757    grep(/^tnef\z/,@ts) && return do_tnef($part,$tempdir);
11758    grep(/^exe\z/,@ts)  && return do_executable($part,$tempdir);
11759
11760    # Falling through (e.g. HTML) - no match, consider atomic
11761    $none_called = 1;
11762    return 0;  # returns from eval
11763  };
11764  if ($@ ne '') {
11765    chomp($@);
11766    if ($@ =~ /^Exceeded storage quota/ ||
11767        $@ =~ /^Maximum number of files.*exceeded/) { $hold = $@ }
11768    else {
11769      do_log(-1,sprintf("Decoding of %s (%s) failed, leaving it unpacked: %s",
11770                        $part->base_name, $part->type_long, $@));
11771    }
11772    $sts = 2;
11773    chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";  # just in case
11774  }
11775  if ($sts == 1 && lookup(0,$part->type_long, @keep_decoded_original_maps)) {
11776    # don't trust this file type or unpacker,
11777    # keep both the original and the unpacked file
11778    ll(4) && do_log(4,sprintf("file type is %s, retain original %s",
11779                              $part->type_long, $part->base_name));
11780    $sts = 2;
11781  }
11782  if ($sts == 1) {
11783    ll(5) && do_log(5, "decompose_part: deleting ".$part->full_name);
11784    unlink($part->full_name)
11785      or die sprintf("Can't unlink %s: %s", $part->full_name, $!);
11786  }
11787  ll(4) && do_log(4,sprintf("decompose_part: %s - %s", $part->base_name,
11788                   ['atomic','archive, unpacked','source retained']->[$sts]));
11789  section_time('decompose_part')  unless $none_called;
11790  $hold;
11791}
11792
11793#
11794# Uncompression/unarchiving routines
11795# Possible return codes:
11796# 0 - truly atomic, or unknown or archiver failure; consider atomic
11797# 1 - some archiver format, successfully unpacked, result replaces original
11798# 2 - probably unpacked, but keep the original (eg self-extracting archive)
11799
11800# if ASCII text, try multiple decoding methods as provided by UUlib
11801# (uuencoded, xxencoded, BinHex, yEnc, Base64, Quoted-Printable)
11802sub do_ascii($$) {
11803  my($part, $tempdir) = @_;
11804
11805  snmp_count('OpsDecByUUlibAttempt');
11806  my($envtmpdir_changed);
11807  # prevent uunconc.c/UUDecode() from trying to create temp file in '/'
11808  if ($ENV{TMPDIR} eq '') { $ENV{TMPDIR} = $TEMPBASE; $envtmpdir_changed = 1 }
11809
11810  my($any_errors,$any_decoded);
11811  eval {  # must not go away without calling Convert::UUlib::CleanUp!
11812    my($sts,$count);
11813    $sts = Convert::UUlib::Initialize();
11814    $sts==RET_OK or die "Convert::UUlib::Initialize failed: "
11815                          . Convert::UUlib::strerror($sts);
11816    my($uulib_version) = Convert::UUlib::GetOption(OPT_VERSION);
11817    !Convert::UUlib::SetOption(OPT_IGNMODE,1)   or die "bad uulib OPT_IGNMODE";
11818  # !Convert::UUlib::SetOption(OPT_DESPERATE,1) or die "bad uulib OPT_DESPERATE";
11819    ($sts, $count) = Convert::UUlib::LoadFile($part->full_name);
11820    if ($sts != RET_OK) {
11821      my($errmsg) = Convert::UUlib::strerror($sts) . ": $!";
11822      $errmsg .= ", (???"
11823        . Convert::UUlib::strerror(Convert::UUlib::GetOption(OPT_ERRNO))."???)"
11824        if $sts == RET_IOERR;
11825      die "Convert::UUlib::LoadFile (uulib V$uulib_version) failed: $errmsg";
11826    }
11827    ll(4) && do_log(4,sprintf(
11828                            "do_ascii: Decoding part %s (%d items), uulib V%s",
11829                            $part->base_name, $count, $uulib_version));
11830    my($uu);
11831    my($item_num) = 0; my($parent_placement) = $part->mime_placement;
11832    for (my($j) = 0; $uu = Convert::UUlib::GetFileListItem($j); $j++) {
11833      $item_num++;
11834      ll(4) && do_log(4,sprintf(
11835                 "do_ascii(%d): state=0x%02x, enc=%s%s, est.size=%s, name=%s",
11836                  $j, $uu->state, Convert::UUlib::strencoding($uu->uudet),
11837                  ($uu->mimetype ne '' ? ", mimetype=" . $uu->mimetype : ''),
11838                  $uu->size, $uu->filename));
11839      if (!($uu->state & FILE_OK)) {
11840        $any_errors++;
11841        do_log(1,"do_ascii: Convert::UUlib info: $j not decodable, ".$uu->state);
11842      } else {
11843        my($newpart_obj)=Amavis::Unpackers::Part->new("$tempdir/parts",$part);
11844        $newpart_obj->mime_placement("$parent_placement/$item_num");
11845        $newpart_obj->name_declared($uu->filename);
11846        my($newpart) = $newpart_obj->full_name;
11847        $! = undef;
11848        $sts = $uu->decode($newpart);  # decode to file $newpart
11849        my($err_decode) = "$!";
11850        chmod(0750, $newpart) or $! == ENOENT  # chmod, don't panic if no file
11851          or die "Can't change protection of \"$newpart\": $!";
11852        my($statmsg);
11853        my($errn) = lstat($newpart) ? 0 : 0+$!;
11854        if    ($errn == ENOENT) { $statmsg = "does not exist"   }
11855        elsif ($errn) { $statmsg = "inaccessible: $!" }
11856        elsif ( -l _) { $statmsg = "is a symlink"     }
11857        elsif ( -d _) { $statmsg = "is a directory"   }
11858        elsif (!-f _) { $statmsg = "not a regular file" }
11859        if (defined $statmsg) { $statmsg = "; file status: $newpart $statmsg" }
11860        my($size) = 0 + (-s _);
11861        $newpart_obj->size($size);
11862        consumed_bytes($size, 'do_ascii');
11863        if ($sts == RET_OK && $errn==0) {
11864          $any_decoded++;
11865          do_log(4,"do_ascii: RET_OK" . $statmsg)  if defined $statmsg;
11866        } elsif ($sts == RET_NODATA || $sts == RET_NOEND) {
11867          $any_errors++;
11868          do_log(-1,"do_ascii: Convert::UUlib error: "
11869                    . Convert::UUlib::strerror($sts) . $statmsg);
11870        } else {
11871          $any_errors++;
11872          my($errmsg) = Convert::UUlib::strerror($sts) . ":: $err_decode";
11873          $errmsg .= ", " . Convert::UUlib::strerror(
11874                  Convert::UUlib::GetOption(OPT_ERRNO) )  if $sts == RET_IOERR;
11875          die ("Convert::UUlib failed: " . $errmsg . $statmsg);
11876        }
11877      }
11878    }
11879  };
11880  my($eval_stat) = $@;
11881  Convert::UUlib::CleanUp();
11882  snmp_count('OpsDecByUUlib')  if $any_decoded;
11883  delete $ENV{TMPDIR}  if $envtmpdir_changed;  # restore
11884  if ($eval_stat ne '') { chomp($eval_stat); die "do_ascii: $eval_stat\n" }
11885  ($any_decoded && !$any_errors) ? 1 : $any_errors ? 2 : 0;
11886}
11887
11888# use Archive-Zip
11889sub do_unzip($$) {
11890  my($part, $tempdir) = @_;
11891
11892  ll(4) && do_log(4, "Unzipping " . $part->base_name);
11893  snmp_count('OpsDecByArZipAttempt');
11894  my($zip) = Archive::Zip->new;
11895  my(@err_nm) = qw(AZ_OK AZ_STREAM_END AZ_ERROR AZ_FORMAT_ERROR AZ_IO_ERROR);
11896
11897  # Need to set up a temporary minimal error handler
11898  # because we now test inside do_unzip whether the $part
11899  # in question is a zip archive
11900  Archive::Zip::setErrorHandler(sub { return 5 });
11901  my($sts) = $zip->read($part->full_name);
11902  Archive::Zip::setErrorHandler(sub { die @_ });
11903  if ($sts != AZ_OK) {
11904    do_log(4, "do_unzip: not a zip: $err_nm[$sts] ($sts)");
11905    return 0;
11906  }
11907  my($any_unsupp_compmeth,$any_zero_length);
11908  my($encryptedcount,$extractedcount) = (0,0);
11909  my($item_num) = 0; my($parent_placement) = $part->mime_placement;
11910  for my $mem ($zip->members()) {
11911    my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
11912    $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
11913    $newpart_obj->name_declared($mem->fileName);
11914    my($compmeth) = $mem->compressionMethod;
11915    if ($compmeth != COMPRESSION_DEFLATED && $compmeth != COMPRESSION_STORED) {
11916      $any_unsupp_compmeth = $compmeth;
11917      $newpart_obj->attributes_add('U');
11918    } elsif ($mem->isEncrypted) {
11919      $encryptedcount++;
11920      $newpart_obj->attributes_add('U','C');
11921    } elsif ($mem->isDirectory) {
11922      $newpart_obj->attributes_add('D');
11923    } else {
11924      # want to read uncompressed - set to COMPRESSION_STORED
11925      my($oldc) = $mem->desiredCompressionMethod(COMPRESSION_STORED);
11926      $sts = $mem->rewindData();
11927      $sts == AZ_OK or die sprintf("%s: error rew. member data: %s (%s)",
11928                                   $part->base_name, $err_nm[$sts], $sts);
11929      my($newpart) = $newpart_obj->full_name;
11930      my($outpart) = IO::File->new;
11931      $outpart->open($newpart,'>') or die "Can't create file $newpart: $!";
11932      binmode($outpart) or die "Can't set file $newpart to binmode: $!";
11933      my($size) = 0;
11934      while ($sts == AZ_OK) {
11935        my($buf_ref);
11936        ($buf_ref, $sts) = $mem->readChunk();
11937        $sts == AZ_OK || $sts == AZ_STREAM_END
11938          or die sprintf("%s: error reading member: %s (%s)",
11939                         $part->base_name, $err_nm[$sts], $sts);
11940        my($buf_len) = length($$buf_ref);
11941        if ($buf_len > 0) {
11942          $size += $buf_len;
11943          $outpart->print($$buf_ref) or die "Can't write to $newpart: $!";
11944          consumed_bytes($buf_len, 'do_unzip');
11945        }
11946      }
11947      $any_zero_length = 1  if $size == 0;
11948      $newpart_obj->size($size);
11949      $outpart->close or die "Can't close $newpart: $!";
11950      $mem->desiredCompressionMethod($oldc);
11951      $mem->endRead();
11952      $extractedcount++;
11953    }
11954  }
11955  snmp_count('OpsDecByArZip');
11956  my($retval) = 1;
11957  if ($any_unsupp_compmeth) {
11958    $retval = 2;
11959    do_log(-1, sprintf("do_unzip: %s, unsupported compr. method: %s",
11960                       $part->base_name, $any_unsupp_compmeth));
11961  } elsif ($any_zero_length) {  # possible zip vulnerability exploit
11962    $retval = 2;
11963    do_log(1, sprintf("do_unzip: %s, zero length members, archive retained",
11964                      $part->base_name));
11965  } elsif ($encryptedcount) {
11966    $retval = 2;
11967    do_log(1, sprintf(
11968      "do_unzip: %s, %d members are encrypted, %s extracted, archive retained",
11969                      $part->base_name, $encryptedcount,
11970                      !$extractedcount ? 'none' : $extractedcount));
11971  }
11972  $retval;
11973}
11974
11975# use external decompressor program from the gzip/bzip2/compress family
11976# (there *is* a perl module for bzip2, but is not ready for prime time)
11977sub do_uncompress($$$) {
11978  my($part, $tempdir, $decompressor) = @_;
11979  ll(4) && do_log(4,sprintf("do_uncompress %s by %s",
11980                            $part->base_name,$decompressor));
11981  my($decompressor_name) = basename((split(' ',$decompressor))[0]);
11982  snmp_count("OpsDecBy\u${decompressor_name}");
11983  my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
11984  $newpart_obj->mime_placement($part->mime_placement."/1");
11985  my($newpart) = $newpart_obj->full_name;
11986  my($type_short, $name_declared) = ($part->type_short, $part->name_declared);
11987  my(@rn);  # collect recommended file names
11988  push(@rn,$1)
11989    if $part->type_long =~ /^\S+\s+compressed data, was "(.+)"(\z|, from\b)/;
11990  for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) {
11991    next  if $name_d eq '';
11992    my($name) = $name_d;
11993    for (!ref $type_short ? ($type_short) : @$type_short) {
11994      /^F\z/   and  $name=~s/\.F\z//;
11995      /^Z\z/   and  $name=~s/\.Z\z//    || $name=~s/\.tg?z\z/.tar/;
11996      /^gz\z/  and  $name=~s/\.gz\z//   || $name=~s/\.tgz\z/.tar/;
11997      /^bz\z/  and  $name=~s/\.bz\z//   || $name=~s/\.tbz\z/.tar/;
11998      /^bz2\z/ and  $name=~s/\.bz2?\z// || $name=~s/\.tbz\z/.tar/;
11999      /^lzo\z/ and  $name=~s/\.lzo\z//;
12000      /^rpm\z/ and  $name=~s/\.rpm\z/.cpio/;
12001    }
12002    push(@rn,$name)  if !grep { $_ eq $name } @rn;
12003  }
12004  $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn)  if @rn;
12005  my($proc_fh,$pid) =
12006    run_command($part->full_name, undef, split(' ',$decompressor));
12007  my($rv,$rerr) = run_command_copy($newpart,$proc_fh);
12008  if ($rv) {
12009#   unlink($newpart) or die "Can't unlink $newpart: $!";
12010    die sprintf('Error running decompressor %s on %s, %s',
12011                $decompressor, $part->base_name, exit_status_str($rv,$rerr));
12012  }
12013  1;
12014}
12015
12016# use Zlib to inflate
12017sub do_gunzip($$) {
12018  my($part, $tempdir) = @_;
12019  do_log(4, "Inflating gzip archive " . $part->base_name);
12020  snmp_count('OpsDecByZlib');
12021  my($gz) = gzopen($part->full_name, "rb")
12022    or die sprintf("do_gunzip: Error opening %s: %s",
12023                   $part->full_name, $gzerrno);
12024  my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
12025  $newpart_obj->mime_placement($part->mime_placement."/1");
12026  my($newpart) = $newpart_obj->full_name;
12027  my($outpart) = IO::File->new;
12028  $outpart->open($newpart,'>') or die "Can't create file $newpart: $!";
12029  binmode($outpart) or die "Can't set file $newpart to binmode: $!";
12030  my($buffer); my($size) = 0;
12031  while ($gz->gzread($buffer) > 0) {
12032    $outpart->print($buffer) or die "Can't write to $newpart: $!";
12033    $size += length($buffer);
12034    consumed_bytes(length($buffer), 'do_gunzip');
12035  }
12036  $newpart_obj->size($size);
12037  $outpart->close or die "Can't close $newpart: $!";
12038  my(@rn);  # collect recommended file name
12039  my($name_declared) = $part->name_declared;
12040  for my $name_d (!ref $name_declared ? ($name_declared) : @$name_declared) {
12041    next  if $name_d eq '';
12042    my($name) = $name_d;
12043    $name=~s/\.(gz|Z)\z// || $name=~s/\.tgz\z/.tar/;
12044    push(@rn,$name)  if !grep { $_ eq $name } @rn;
12045  }
12046  $newpart_obj->name_declared(@rn==1 ? $rn[0] : \@rn)  if @rn;
12047  if ($gzerrno != Z_STREAM_END) {
12048    do_log(-1,sprintf("do_gunzip: Error reading %s: %s",
12049                      $part->full_name, $gzerrno));
12050    unlink($newpart) or die "Can't unlink $newpart: $!";
12051    $newpart_obj->size(undef);
12052    $gz->gzclose();
12053    return 0;
12054  }
12055  $gz->gzclose();
12056  1;
12057}
12058
12059# untar any tar archives with Archive-Tar, extract each file individually
12060sub do_tar($$) {
12061  my($part, $tempdir) = @_;
12062  snmp_count('OpsDecByArTar');
12063  # Work around bug in Archive-Tar
12064  my $tar = eval { Archive::Tar->new($part->full_name) };
12065  if (!defined($tar)) {
12066    chomp($@);
12067    do_log(4, sprintf("Faulty archive %s: %s", $part->full_name, $@));
12068    return 0;
12069  }
12070  do_log(4,"Untarring ".$part->base_name);
12071  my($item_num) = 0; my($parent_placement) = $part->mime_placement;
12072  my(@list) = $tar->list_files();
12073  for (@list) {
12074    next  if /\/\z/;  # ignore directories
12075                      # this is bad (reads whole file into scalar)
12076                      # need some error handling, too
12077    my $data = $tar->get_content($_);
12078    my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
12079    $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
12080    my($newpart) = $newpart_obj->full_name;
12081    my($outpart) = IO::File->new;
12082    $outpart->open($newpart,'>') or die "Can't create file $newpart: $!";
12083    binmode($outpart) or die "Can't set file $newpart to binmode: $!";
12084    $outpart->print($data) or die "Can't write to $newpart: $!";
12085    $newpart_obj->size(length($data));
12086    consumed_bytes(length($data), 'do_tar');
12087    $outpart->close or die "Can't close $newpart: $!";
12088  }
12089  1;
12090}
12091
12092# use external program to expand RAR archives
12093sub do_unrar($$) {
12094  my($part, $tempdir) = @_;
12095  ll(4) && do_log(4, "Attempting to expand RAR archive " . $part->base_name);
12096  my($decompressor_name) = basename((split(' ',$unrar))[0]);
12097  snmp_count("OpsDecBy\u${decompressor_name}Attempt");
12098  my(@common_rar_switches) = qw(-c- -p- -av- -idp);
12099  my($err, $retval, $rv1);
12100  # unrar exit codes: SUCCESS=0, WARNING=1, FATAL_ERROR=2, CRC_ERROR=3,
12101  #   LOCK_ERROR=4, WRITE_ERROR=5, OPEN_ERROR=6, USER_ERROR=7, MEMORY_ERROR=8,
12102  #   CREATE_ERROR=9, USER_BREAK=255
12103  # Check whether we can really unrar it
12104  $rv1 =
12105    system($unrar, 't', '-inul', @common_rar_switches, '--', $part->full_name);
12106  $err = $!; $retval = retcode($rv1);
12107  if ($retval == 7) {  # USER_ERROR
12108    do_log(-1,"do_unrar: $unrar does not recognize all switches, "
12109              . "it is probably too old. Retrying without '-av- -idp'. "
12110              . "Upgrade: http://www.rarlab.com/");
12111    @common_rar_switches = qw(-c- -p-);  # retry without new switches
12112    $rv1 = system($unrar, 't', '-inul', @common_rar_switches, '--',
12113                  $part->full_name);
12114    $err = $!; $retval = retcode($rv1);
12115  }
12116  if (!grep { $_ == $retval } (0,1,3)) {
12117    # not one of: SUCCESS, WARNING, CRC_ERROR
12118    # NOTE: password protected files in the archive cause CRC_ERROR
12119    do_log(4,sprintf("unrar 't' %s, command: %s",
12120                     exit_status_str($rv1,$err), $unrar));
12121    return 0;
12122  }
12123
12124  # We have to jump hoops because there is no simple way to
12125  # just list all the files
12126  ll(4) && do_log(4, "Expanding RAR archive " . $part->base_name);
12127
12128  my(@list); my($hypcount) = 0; my($encryptedcount) = 0;
12129  my($lcnt) = 0; my($member_name); my($bytes) = 0; my($last_line);
12130  my($item_num) = 0; my($parent_placement) = $part->mime_placement;
12131  my($proc_fh,$pid) =
12132    run_command(undef, "&1", $unrar, 'v', @common_rar_switches, '--',
12133                $part->full_name);
12134  while (defined($_ = $proc_fh->getline)) {
12135    $last_line = $_  if !/^\s*$/;  # keep last nonempty line
12136    chomp;
12137    if (/^unexpected end of archive/) {
12138      last;
12139    } elsif (/^------/) {
12140      $hypcount++;
12141      last  if $hypcount >= 2;
12142    } elsif ($hypcount < 1 && /^Encrypted file:/) {
12143      do_log(4,"do_unrar: ".$_);
12144      $part->attributes_add('U','C');
12145    } elsif ($hypcount == 1) {
12146      $lcnt++;
12147      if ($lcnt % 2 == 0) {  # information line (every other line)
12148        if (!/^\s+(\d+)\s+(\d+)\s+(\d+%|-->|<--)/) {
12149          do_log(-1,"do_unrar: can't parse info line for \"$member_name\" $_");
12150        } elsif (defined $member_name) {
12151          do_log(5,"do_unrar: member: \"$member_name\", size: $1");
12152          if ($1 > 0) { $bytes += $1; push(@list, $member_name) }
12153        }
12154        $member_name = undef;
12155      } elsif (/^(.)(.*)\z/s) {
12156        $member_name = $2; # all but the first character (space or an asterisk)
12157        if ($1 eq '*') {   # member is encrypted
12158          $encryptedcount++; $item_num++;
12159          # make a phantom entry - carrying only name and attributes
12160          my($newpart_obj)=Amavis::Unpackers::Part->new("$tempdir/parts",$part);
12161          $newpart_obj->mime_placement("$parent_placement/$item_num");
12162          $newpart_obj->name_declared($member_name);
12163          $newpart_obj->attributes_add('U','C');
12164          $member_name = undef;  # makes no sense extracting encrypted files
12165        }
12166      }
12167    }
12168  }
12169  # consume all remaining output to avoid broken pipe
12170  while (defined($_ = $proc_fh->getline)) { $last_line = $_  if !/^\s*$/ }
12171  $err = undef; $proc_fh->close or $err = $!; $retval = retcode($?);
12172  if ($retval == 3) {  # CRC_ERROR
12173    do_log(4,"do_unrar: CRC_ERROR - undecipherable");
12174    $part->attributes_add('U');
12175  }
12176  my($fn) = $part->full_name;
12177  if (!$bytes && $retval==0 && $last_line =~ /^\Q$fn\E is not RAR archive$/) {
12178    do_log(4,"do_unrar: ".$last_line);
12179    return 0;
12180  } elsif ($last_line !~ /^\s*(\d+)\s+(\d+)/s) {
12181    do_log(4,"do_unrar: unable to obtain orig total size: $last_line");
12182  } else {
12183    do_log(4,"do_unrar: summary size: $2, sum of sizes: $bytes")
12184      if abs($bytes - $2) > 100;
12185    $bytes = $2  if $2 > $bytes;
12186  }
12187  consumed_bytes($bytes, 'do_unrar-pre', 1);  # pre-check on estimated size
12188  snmp_count("OpsDecBy\u${decompressor_name}");
12189  if ($retval==0) {}  # SUCCESS
12190  elsif ($retval==1 && @list && $bytes > 0) {}  # WARNING, probably still ok
12191  else {              # WARNING and suspicious, or really bad
12192    die ("unrar: can't get a list of archive members: " .
12193         exit_status_str($?,$err) ."; ".$last_line);
12194  }
12195  if (!@list) {
12196    do_log(4,"do_unrar: no archive members, or not an archive at all");
12197#***return 0  if $exec;
12198  } else {
12199  # my $rv = store_mgr($tempdir, $part, \@list, $unrar,
12200  #                    qw(p -inul -kb), @common_rar_switches, '--',
12201  #                    $part->full_name);
12202    my($proc_fh,$pid) =
12203      run_command(undef, "&1", $unrar, qw(x -inul -ver -o- -kb),
12204                  @common_rar_switches, '--',
12205                  $part->full_name, "$tempdir/parts/rar/");
12206    my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ }
12207    my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
12208    if (!grep { $_ == $retval } (0,1,3)) {  # not one of: SUCCESS, WARNING, CRC
12209      do_log(-1, 'unrar '.exit_status_str($?,$err));
12210    }
12211    my($errn) = lstat("$tempdir/parts/rar") ? 0 : 0+$!;
12212    if ($errn != ENOENT) {
12213      my($b) = flatten_and_tidy_dir("$tempdir/parts/rar","$tempdir/parts",$part);
12214      consumed_bytes($b, 'do_unrar');
12215    }
12216  }
12217  if ($encryptedcount) {
12218    do_log(1, sprintf(
12219      "do_unrar: %s, %d members are encrypted, %s extracted, archive retained",
12220      $part->base_name, $encryptedcount, !@list ? 'none' : 0+@list ));
12221    return 2;
12222  }
12223  1;
12224}
12225
12226# use external program to expand LHA archives
12227sub do_lha($$) {
12228  my($part, $tempdir) = @_;
12229  ll(4) && do_log(4, "Attempting to expand LHA archive " . $part->base_name);
12230  my($decompressor_name) = basename((split(' ',$lha))[0]);
12231  snmp_count("OpsDecBy\u${decompressor_name}Attempt");
12232  # lha needs extension .exe to understand SFX!
12233  symlink($part->full_name, $part->full_name.".exe")
12234    or die sprintf("Can't symlink %s %s.exe: %s",
12235                   $part->full_name, $part->full_name, $!);
12236  # Check whether we can really lha it
12237  my($checkerr); my($retval) = 1;
12238  my($proc_fh,$pid) = run_command(undef, "&1", $lha, 'lq',
12239                                  $part->full_name.".exe");
12240  while (defined($_ = $proc_fh->getline)) {
12241    $checkerr = 1  if /Checksum error/i;
12242  }
12243  my($err); $proc_fh->close or $err = $!;
12244  if ($? || $checkerr) {
12245    $retval = 0;  # consider atomic
12246    do_log(4, "do_lha: not a LHA archive($checkerr) ? ".
12247              exit_status_str($?,$err));
12248  } else {
12249    do_log(4, "Expanding LHA archive " . $part->base_name . ".exe");
12250    snmp_count("OpsDecBy\u${decompressor_name}");
12251    ($proc_fh,$pid) =
12252      run_command(undef, undef, $lha, 'lq', $part->full_name.".exe");
12253    my(@list);
12254    while (defined($_ = $proc_fh->getline)) {
12255      chomp;
12256      next  if /\/\z/;  # ignore directories
12257      push(@list, (split(/\s+/))[-1]);  #***??? split on whitespace ???
12258    }
12259    $err=undef; $proc_fh->close or $err = $!;
12260    $?==0 or do_log(-1, 'do_lha: '.exit_status_str($?,$err));
12261    if (!@list) {
12262      do_log(4, "do_lha: no archive members, or not an archive at all");
12263#***  $retval = 0  if $exec;
12264    } else {
12265      my $rv = store_mgr($tempdir, $part, \@list, $lha, 'pq',
12266                         $part->full_name.".exe");
12267      do_log(-1, 'do_lha '.exit_status_str($rv))  if $rv;
12268      $retval = 1;  # consider decoded
12269    }
12270  }
12271  unlink($part->full_name.".exe")
12272    or die "Can't unlink " . $part->full_name . ".exe: $!";
12273  $retval;
12274}
12275
12276# use external program to expand ARC archives;
12277# works with original arc, or a GPL licensed 'nomarch'
12278# (http://rus.members.beeb.net/nomarch.html)
12279sub do_arc($$) {
12280  my($part, $tempdir) = @_;
12281  my($decompressor_name) = basename((split(' ',$arc))[0]);
12282  snmp_count("OpsDecBy\u${decompressor_name}");
12283  my($is_nomarch) = $arc =~ /nomarch/i;
12284  ll(4) && do_log(4,sprintf("Unarcing %s, using %s",
12285                   $part->base_name, ($is_nomarch ? "nomarch" : "arc") ));
12286  my($cmdargs) = ($is_nomarch ? "-l -U" : "ln") . " " . $part->full_name;
12287  my($proc_fh,$pid) =
12288    run_command(undef, '/dev/null', $arc, split(' ',$cmdargs));
12289  my(@list) = $proc_fh->getlines;
12290  my($err); $proc_fh->close or $err = $!;
12291  $?==0 or do_log(-1, 'do_arc: '.exit_status_str($?,$err));
12292
12293  #*** no spaces in filenames allowed???
12294  map { s/^([^ \t\r\n]*).*\z/$1/s } @list;  # keep only filenames
12295  if (@list) {
12296    my $rv = store_mgr($tempdir, $part, \@list, $arc,
12297                       ($is_nomarch ? ('-p', '-U') : 'p'), $part->full_name);
12298    do_log(-1, 'arc '.exit_status_str($rv))  if $rv;
12299  }
12300  1;
12301}
12302
12303# use external program to expand ZOO archives
12304sub do_zoo($$) {
12305  my($part, $tempdir) = @_;
12306  do_log(4, "Expanding ZOO archive " . $part->full_name);
12307  my($decompressor_name) = basename((split(' ',$zoo))[0]);
12308  snmp_count("OpsDecBy\u${decompressor_name}");
12309  # Zoo needs extension of .zoo!
12310  symlink($part->full_name, $part->full_name.".zoo")
12311    or die sprintf("Can't symlink %s %s.zoo: %s",
12312                   $part->full_name, $part->full_name, $!);
12313  my($proc_fh,$pid) =
12314    run_command(undef, undef, $zoo, 'lf1q', $part->full_name.".zoo");
12315  my(@list) = $proc_fh->getlines;
12316  my($err); $proc_fh->close or $err = $!;
12317  $?==0 or do_log(-1, 'do_zoo: '.exit_status_str($?,$err));
12318  if (@list) {
12319    chomp(@list);
12320    my $rv = store_mgr($tempdir, $part, \@list, $zoo, 'xpqqq:',
12321                       $part->full_name . ".zoo");
12322    do_log(-1, 'zoo '.exit_status_str($rv))  if $rv;
12323  }
12324  unlink($part->full_name.".zoo")
12325    or die "Can't unlink " . $part->full_name . ".zoo: $!";
12326  1;
12327}
12328
12329# use external program to expand ARJ archives
12330sub do_unarj($$) {
12331  my($part, $tempdir) = @_;
12332  do_log(4, "Expanding ARJ archive " . $part->base_name);
12333  my($decompressor_name) = basename((split(' ',$unrar))[0]);
12334  snmp_count("OpsDecBy\u${decompressor_name}");
12335  # options to arj, ignored by unarj
12336  # provide some password in -g to turn fatal error into 'bad password' error
12337  $ENV{ARJ_SW} = "-i -jo -b5 -2h -jyc -ja1 -gsecret -w$TEMPBASE";
12338  # unarj needs extension of .arj!
12339  symlink($part->full_name, $part->full_name.".arj")
12340    or die sprintf("Can't symlink %s %s.arj: %s",
12341                   $part->full_name, $part->full_name, $!);
12342  # obtain total original size of archive members from the index/listing
12343  my($proc_fh,$pid) =
12344    run_command(undef,'/dev/null', $unarj, 'l', $part->full_name.".arj");
12345  my($last_line);
12346  while (defined($_ = $proc_fh->getline)) { $last_line = $_  if !/^\s*$/ }
12347  my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?);
12348  if (!grep { $_ == $retval } (0,1,3)) {   # not one of: success, warn, CRC err
12349    die ("unarj: can't get a list of archive members: ".
12350         exit_status_str($?,$err));
12351  }
12352  if ($last_line !~ /^\s*(\d+)\s*files\s*(\d+)/s) {
12353    do_log(-1,"do_unarj: WARN: unable to obtain orig size of files: $last_line");
12354  } else {
12355    consumed_bytes($2, 'do_unarj-pre', 1); # pre-check on estimated size
12356  }
12357  # unarj has very limited extraction options, arj is much better!
12358  mkdir("$tempdir/parts/arj", 0750) or die "Can't mkdir $tempdir/parts/arj: $!";
12359  chdir("$tempdir/parts/arj") or die "Can't chdir to $tempdir/parts/arj: $!";
12360  ($proc_fh,$pid) =
12361    run_command(undef, "&1", $unarj, 'e', $part->full_name.".arj");
12362  my($encryptedcount,$skippedcount) = (0,0);
12363  while (defined($_ = $proc_fh->getline)) {
12364    $encryptedcount++
12365      if /^(Extracting.*\bBad file data or bad password|File is password encrypted, Skipped)\b/s;
12366    $skippedcount++
12367      if /(\bexists|^File is password encrypted|^Unsupported .*), Skipped\b/s;
12368  }
12369  $err = undef; $proc_fh->close or $err = $!; $retval = retcode($?);
12370  chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
12371  if (!grep { $_ == $retval } (0,1,3)) {  # not one of: success, warn, CRC err
12372    do_log(0, "unarj: error extracting: ".exit_status_str($?,$err));
12373  }
12374  # add attributes to the parent object, because we didn't remember names
12375  # of its scrambled members
12376  $part->attributes_add('U')  if $skippedcount;
12377  $part->attributes_add('C')  if $encryptedcount;
12378  my($errn) = lstat("$tempdir/parts/arj") ? 0 : 0+$!;
12379  if ($errn != ENOENT) {
12380    my($b) = flatten_and_tidy_dir("$tempdir/parts/arj","$tempdir/parts",$part);
12381    consumed_bytes($b, 'do_unarj');
12382    snmp_count("OpsDecBy\u${decompressor_name}");
12383  }
12384  unlink($part->full_name.".arj")
12385    or die "Can't unlink " . $part->full_name . ".arj: $!";
12386  if (!grep { $_ == $retval } (0,1,3)) {  # not one of: success, warn, CRC err
12387    die ("unarj: can't extract archive members: ".exit_status_str($?,$err));
12388  }
12389  if ($encryptedcount || $skippedcount) {
12390    do_log(1, sprintf(
12391      "do_unarj: %s, %d members are encrypted, %d skipped, archive retained",
12392      $part->base_name, $encryptedcount, $skippedcount));
12393    return 2;
12394  }
12395  1;
12396}
12397
12398# use Convert-TNEF
12399sub do_tnef($$) {
12400  my($part, $tempdir) = @_;
12401  do_log(4, "Extracting TNEF attachment " . $part->base_name);
12402  snmp_count('OpsDecByTnef');
12403  chdir("$tempdir/parts") or die "Can't chdir to $tempdir/parts: $!";
12404  my $tnef =
12405    Convert::TNEF->read_in($part->full_name, {ignore_checksum => "true"});
12406  if (!$tnef) {
12407    chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
12408    return 0;  # Not TNEF - treat as atomic
12409  }
12410  my($item_num) = 0; my($parent_placement) = $part->mime_placement;
12411  for my $a ($tnef->message, $tnef->attachments) {
12412    if (my $dh = $a->datahandle) {
12413      my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",$part);
12414      $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
12415      $newpart_obj->name_declared([$a->name, $a->longname]);
12416      $newpart_obj->size($a->size);
12417      consumed_bytes($a->size, 'do_tnef');
12418      my($newpart) = $newpart_obj->full_name;
12419      my($outpart) = IO::File->new;
12420      $outpart->open($newpart,'>') or die "Can't create file $newpart: $!";
12421      binmode($outpart) or die "Can't set file $newpart to binmode: $!";
12422      if (defined(my $file = $dh->path)) {
12423        copy($file, $outpart);
12424      } else {
12425        my($s) = $dh->as_string;
12426        $outpart->print($s) or die "Can't write to $newpart: $!";
12427        # consumed_bytes(length($s), 'do_tnef');
12428      }
12429      $outpart->close or die "Can't close $newpart: $!";
12430    }
12431  }
12432  $tnef->purge;
12433  chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
12434  1;
12435}
12436
12437# The pax and cpio utilities usually support the following archive formats:
12438#   cpio, bcpio, sv4cpio, sv4crc, tar (old tar), ustar (POSIX.2 tar).
12439# The utilities from http://heirloom.sourceforge.net/ support
12440# several other tar/cpio variants such as SCO, Sun, DEC, Cray, SGI
12441sub do_pax_cpio($$$) {
12442  my($part, $tempdir, $archiver) = @_;
12443  my($archiver_name) = basename((split(' ',$archiver))[0]);
12444  snmp_count("OpsDecBy\u${archiver_name}");
12445  ll(4) && do_log(4,sprintf("Expanding archive %s, using %s",
12446                            $part->base_name, $archiver_name));
12447  my($is_pax) = $archiver_name =~ /^cpio/i ? 0 : 1;
12448  do_log(-1,"WARN: Using $archiver_name instead of pax can be a security ".
12449            "risk; please add:  \$pax='pax';  to amavisd.conf and check that ".
12450            "the pax(1) utility is available on the system!")  if !$is_pax;
12451  my(@cmdargs) = $is_pax ? qw(-v) : qw(-i -t -v);
12452  my($proc_fh,$pid) = run_command($part->full_name, undef, $archiver,@cmdargs);
12453  my($bytes) = 0; local($1,$2,$3);
12454  while (defined($_ = $proc_fh->getline)) {
12455    chomp;
12456    next  if /^\d+ blocks\z/;
12457    last  if /^(cpio|pax): (.*bytes read|End of archive volume)/;
12458    if (!/^ (?: \S+\s+ ){4}
12459            (\d+) \s+
12460            ( (?: \s* \S+ ){3} (?: \s+ \d{4}, )? ) \s+
12461            (.+) \z/xs) {
12462      do_log(-1,"do_pax_cpio: can't parse toc line: $_");
12463    } else {
12464      my($mem,$size) = ($3,$1);
12465      $mem = $1 if $is_pax && $mem =~ /^(.*) =[=>] (.*)\z/; # hard or soft link
12466      do_log(5,"do_pax_cpio: member: \"$mem\", size: $size");
12467      $bytes += $size  if $size > 0;
12468    }
12469  }
12470  # consume remaining output to avoid broken pipe
12471  while (defined($proc_fh->getline)) { }
12472  my($err); $proc_fh->close or $err = $!;
12473  $?==0 or do_log(-1, 'do_pax_cpio/1: '.exit_status_str($?,$err));
12474  consumed_bytes($bytes, 'do_pax_cpio/pre', 1);  # pre-check on estimated size
12475  mkdir("$tempdir/parts/arch", 0750)
12476    or die "Can't mkdir $tempdir/parts/arch: $!";
12477  my($name_clash) = 0;
12478  my(%orig_names);  # maps filenames to archive member names when possible
12479  eval {
12480    chdir("$tempdir/parts/arch")
12481      or die "Can't chdir to $tempdir/parts/arch: $!";
12482    my(@cmdargs) = $is_pax ? qw(-r -k -p am -s /[^A-Za-z0-9_]/-/gp)
12483                       : qw(-i -d --no-absolute-filenames --no-preserve-owner);
12484    my($proc_fh,$pid) = run_command($part->full_name,"&1",$archiver,@cmdargs);
12485    my($output) = '';
12486    while (defined($_ = $proc_fh->getline)) {
12487      chomp;
12488      if (!$is_pax || !/^(.*) >> (\S*)\z/) { $output .= $_."\n" }
12489      else {  # parse output from pax -s///p
12490        my($member_name,$file_name) = ($1,$2);
12491        if (!exists $orig_names{$file_name}) {
12492          $orig_names{$file_name} = $member_name;
12493        } else {
12494          do_log(0,sprintf("do_pax_cpio: member \"%s\" is hidden by a ".
12495                           "previous archive member \"%s\", file: %s",
12496                           $member_name, $orig_names{$file_name}, $file_name));
12497          $orig_names{$file_name} = undef;  # cause it to exist but undefined
12498          $name_clash++;
12499        }
12500      }
12501    }
12502    chomp($output); my($err); $proc_fh->close or $err = $!;
12503    $?==0 or die (exit_status_str($?,$err).' '.$output);
12504  };
12505  chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
12506  my($b) = flatten_and_tidy_dir("$tempdir/parts/arch", "$tempdir/parts",
12507                                $part, 0, \%orig_names);
12508  consumed_bytes($b, 'do_pax_cpio');
12509  if ($@ ne '') { chomp($@); do_log(-1,"do_pax_cpio: $@"); 2 }
12510  elsif ($name_clash) { 2 } else { 1 }
12511}
12512
12513# ar is a standard Unix binary archiver, also used by Debian packages
12514sub do_ar($$) {
12515  my($part, $tempdir) = @_;
12516  ll(4) && do_log(4,"Expanding Unix ar archive ".$part->full_name);
12517  my($archiver_name) = basename((split(' ',$ar))[0]);
12518  snmp_count("OpsDecBy\u${archiver_name}");
12519  my($proc_fh,$pid) = run_command(undef, undef, $ar, 'tv', $part->full_name);
12520  my($bytes) = 0; local($1,$2,$3);
12521  while (defined($_ = $proc_fh->getline)) {
12522    chomp;
12523    if (!/^(?:\S+\s+){2}(\d+)\s+((?:\S+\s+){3}\S+)\s+(.*)\z/) {
12524      do_log(-1,"do_ar: can't parse contents listing line: $_");
12525    } else {
12526      do_log(5,"do_ar: member: \"$3\", size: $1");
12527      $bytes += $1  if $1 > 0;
12528    }
12529  }
12530  # consume remaining output to avoid broken pipe
12531  while (defined($proc_fh->getline)) { }
12532  my($err); $proc_fh->close or $err = $!;
12533  $?==0 or do_log(-1, 'ar-1 '.exit_status_str($?,$err));
12534
12535  consumed_bytes($bytes, 'do_ar-pre', 1);  # pre-check on estimated size
12536  mkdir("$tempdir/parts/ar", 0750)
12537    or die "Can't mkddir $tempdir/parts/ar: $!";
12538  chdir("$tempdir/parts/ar") or die "Can't chdir to $tempdir/parts/ar: $!";
12539  ($proc_fh,$pid) = run_command(undef, "&1", $ar, 'x', $part->full_name);
12540  my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ }
12541  $err = undef; $proc_fh->close or $err = $!;
12542  $?==0 or do_log(-1, 'ar-2 '.exit_status_str($?,$err).' '.$output);
12543  chdir($TEMPBASE) or die "Can't chdir to $TEMPBASE: $!";
12544  my($b) = flatten_and_tidy_dir("$tempdir/parts/ar","$tempdir/parts",$part);
12545  consumed_bytes($b, 'do_ar');
12546  1;
12547}
12548
12549sub do_cabextract($$) {
12550  my($part, $tempdir) = @_;
12551  do_log(4, "Expanding cab archive " . $part->base_name);
12552  my($archiver_name) = basename((split(' ',$cabextract))[0]);
12553  snmp_count("OpsDecBy\u${archiver_name}");
12554  my($bytes) = 0;
12555  my($proc_fh,$pid) =
12556    run_command(undef,undef,$cabextract,'-l',$part->full_name);
12557  while (defined($_ = $proc_fh->getline)) {
12558    chomp;
12559    next  if /^(File size|----|Viewing cabinet:|\z)/;
12560    if (!/^\s* (\d+) \s* \| [^|]* \| \s (.*) \z/x) {
12561      do_log(-1, "do_cabextract: can't parse toc line: $_");
12562    } else {
12563      do_log(5, "do_cabextract: member: \"$2\", size: $1");
12564      $bytes += $1  if $1 > 0;
12565    }
12566  }
12567  # consume remaining output to avoid broken pipe (just in case)
12568  while (defined($proc_fh->getline)) { }
12569  my($err); $proc_fh->close or $err = $!;
12570  $?==0 or do_log(-1, 'cabextract-1 '.exit_status_str($?,$err));
12571
12572  consumed_bytes($bytes, 'do_cabextract-pre', 1); # pre-check on estimated size
12573  mkdir("$tempdir/parts/cab", 0750) or die "Can't mkdir $tempdir/parts/cab: $!";
12574  ($proc_fh,$pid) = run_command(undef, '/dev/null', $cabextract, '-q', '-d',
12575                                "$tempdir/parts/cab", $part->full_name);
12576  my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ }
12577  $err = undef; $proc_fh->close or $err = $!;
12578  $?==0 or do_log(-1, 'cabextract-2 '.exit_status_str($?,$err).' '.$output);
12579  my($b) = flatten_and_tidy_dir("$tempdir/parts/cab", "$tempdir/parts", $part);
12580  consumed_bytes($b, 'do_cabextract');
12581  1;
12582}
12583
12584sub do_ole($$) {
12585  my($part, $tempdir) = @_;
12586  do_log(4,"Expanding MS OLE document " . $part->base_name);
12587  my($archiver_name) = basename((split(' ',$ripole))[0]);
12588  snmp_count("OpsDecBy\u${archiver_name}");
12589  mkdir("$tempdir/parts/ole",0750) or die "Can't mkdir $tempdir/parts/ole: $!";
12590  my($proc_fh,$pid) = run_command(undef, "&1", $ripole, '-v',
12591                            '-i', $part->full_name, '-d',"$tempdir/parts/ole");
12592  my($output) = ''; while (defined($_ = $proc_fh->getline)) { $output .= $_ }
12593  my($err); $proc_fh->close or $err = $!;
12594  $?==0 or do_log(0, 'ripOLE '.exit_status_str($?,$err).' '.$output);
12595  my($b) = flatten_and_tidy_dir("$tempdir/parts/ole", "$tempdir/parts", $part);
12596  if ($b > 0) {
12597    do_log(4, "ripOLE extracted $b bytes from an OLE document");
12598    consumed_bytes($b, 'do_ole');
12599  }
12600  2;  # always keep the original OLE document
12601}
12602
12603# Check for self-extracting archives.  Note that we don't rely on
12604# file magic here since it's not reliable.  Instead we will try each
12605# archiver.
12606sub do_executable($$) {
12607  my($part, $tempdir) = @_;
12608
12609  ll(4) && do_log(4,"Check whether ".$part->base_name.
12610                    " is a self-extracting archive");
12611  # ZIP?
12612  return 2  if eval { do_unzip($part, $tempdir) };
12613  chomp($@);
12614  do_log(-1,"do_executable/do_unzip failed, ignoring: $@")  if $@ ne '';
12615
12616  # RAR?
12617  return 2  if defined $unrar && eval { do_unrar($part, $tempdir) };
12618  chomp($@);
12619  do_log(-1,"do_executable/do_unrar failed, ignoring: $@")  if $@ ne '';
12620
12621  # LHA?
12622  return 2  if defined $lha && eval { do_lha($part, $tempdir) };
12623  chomp($@);
12624  do_log(-1,"do_executable/do_lha failed, ignoring: $@")    if $@ ne '';
12625
12626# # ARJ?
12627# return 2  if defined $unarj && eval { do_unarj($part, $tempdir) };
12628# chomp($@);
12629# do_log(-1,"do_executable/do_unarj failed, ignoring: $@")  if $@ ne '';
12630
12631  return 0;
12632}
12633
12634# my($k,$v,$fn);
12635# while (($k,$v) = each(%::)) {
12636#   local(*e)=$v; $fn=fileno(\*e);
12637#   printf STDERR ("%-10s %-10s %s$eol",$k,$v,$fn)  if defined $fn;
12638# }
12639
12640# Given a file handle (typically opened pipe to a subprocess, as returned
12641# from run_command), copy from it to a specified output file in binary mode.
12642sub run_command_copy($$) {
12643  my($outfile, $ifh) = @_;
12644  my($ofh) = IO::File->new;
12645  $ofh->open($outfile,'>') or die "Can't create file $outfile: $!";
12646  binmode($ofh) or die "Can't set file $outfile to binmode: $!";
12647  binmode($ifh) or die "Can't set binmode on pipe: $!";
12648  my($len, $buf, $offset, $written);
12649  while ($len = $ifh->sysread($buf, 16384)) {
12650    $offset = 0;
12651    while ($len > 0) {  # handle partial writes
12652      $written = syswrite($ofh, $buf, $len, $offset);
12653      defined($written) or die "syswrite to $outfile failed: $!";
12654      consumed_bytes($written, 'run_command_copy');
12655      $len -= $written; $offset += $written;
12656    }
12657  }
12658  my($rerr); $ifh->close or $rerr=$!; my($rv) = $?;
12659  $ofh->close or die "Can't close $outfile: $!";
12660  ($rv,$rerr);  # return subprocess termination status and errno
12661}
12662
12663# extract listed files from archive and store in new file
12664sub store_mgr($$$@) {
12665  my($tempdir, $parent_obj, $list, $cmd, @args) = @_;
12666
12667  my($item_num) = 0; my($parent_placement) = $parent_obj->mime_placement;
12668  my(@rv);
12669  for my $f (@$list) {
12670    next  if $f =~ m{/\z};  # ignore directories
12671    my($newpart_obj) = Amavis::Unpackers::Part->new("$tempdir/parts",
12672                                                    $parent_obj);
12673    $item_num++; $newpart_obj->mime_placement("$parent_placement/$item_num");
12674    $newpart_obj->name_declared($f);  # store tainted name
12675    my($newpart) = $newpart_obj->full_name;
12676    do_log(5,sprintf('store_mgr: extracting "%s" to file %s using %s',
12677                     $f, $newpart, $cmd));
12678    if ($f =~ m{^\.?[A-Za-z0-9_][A-Za-z0-9/._=~-]*\z}) {  # apparently safe arg
12679    } else {  # this is not too bad, as run_command does not use shell
12680      do_log(1, "store_mgr: NOTICE: untainting funny argument \"$f\"");
12681    }
12682    my($proc_fh,$pid) = run_command(undef,undef,$cmd,@args,untaint($f));
12683    my($rv,$rerr) = run_command_copy($newpart,$proc_fh);
12684    do_log(5,"store_mgr: extracted by $cmd, ".exit_status_str($rv,$rerr));
12685    push(@rv, $rv);
12686  }
12687  @rv = grep { $_ != 0 } @rv;
12688  @rv ? $rv[0] : 0;  # just return the first nonzero status (if any), or 0
12689}
12690
126911;
12692
12693__DATA__
12694#
12695# =============================================================================
12696# This text section governs how a main per-message amavisd-new log entry
12697# is formed. An empty text will prevent a log entry, multi-line text will
12698# produce several log entries, one for each nonempty line.
12699# Syntax is explained in the README.customize file.
12700[?%#D|#|Passed #
12701[? [?%#V|1] |INFECTED (%V)|#
12702[? [?%#F|1] |BANNED (%F)|#
12703[? [? %2|1] |SPAM|#
12704[? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
12705, [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%D|,]#
12706[? %q ||, quarantine: %i]#
12707[? %Q ||, Queue-ID: %Q]#
12708[? %m ||, Message-ID: %m]#
12709[? %r ||, Resent-Message-ID: %r]#
12710, Hits: %c#
12711#, size: %z#
12712#[? %j ||, Subject: "%j"]#
12713#[? %#T ||, tests=[%T|,]]#
12714, %y ms#
12715]
12716[?%#O|#|Blocked #
12717[? [?%#V|1] |INFECTED (%V)|#
12718[? [?%#F|1] |BANNED (%F)|#
12719[? [? %2|1] |SPAM|#
12720[? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
12721, [? %p ||%p ][?%a||[?%l||LOCAL ]\[%a\] ][?%e||\[%e\] ]<%o> -> [%O|,]#
12722[? %q ||, quarantine: %i]#
12723[? %Q ||, Queue-ID: %Q]#
12724[? %m ||, Message-ID: %m]#
12725[? %r ||, Resent-Message-ID: %r]#
12726, Hits: %c#
12727#, size: %z#
12728#[? %j ||, Subject: "%j"]#
12729#[? %#T ||, tests=[%T|,]]#
12730, %y ms#
12731]
12732__DATA__
12733#
12734# =============================================================================
12735# This text section governs how a main per-recipient amavisd-new log entry
12736# is formed. An empty text will prevent a log entry, multi-line text will
12737# produce several log entries, one for each nonempty line.
12738# Macro %. might be useful, it counts recipients starting from 1.
12739# Syntax is explained in the README.customize file.
12740#
12741[?%#D||Passed #
12742[? [?%#V|1] |INFECTED (%V)|#
12743[? [?%#F|1] |BANNED (%F)|#
12744[? [? %2|1] |SPAM|#
12745[? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
12746, <%o> -> [%D|,], Hits: %c#
12747, tag=%3, tag2=%4, kill=%5#  NOTE: macros %3, %4, %5 are experimental
12748, %0/%1/%2/%k#
12749]
12750[?%#O||Blocked #
12751[? [?%#V|1] |INFECTED (%V)|#
12752[? [?%#F|1] |BANNED (%F)|#
12753[? [? %2|1] |SPAM|#
12754[? [?%#X|1] |BAD-HEADER|CLEAN]]]]#
12755, <%o> -> [%O|,], Hits: %c#
12756, tag=%3, tag2=%4, kill=%5#  NOTE: macros %3, %4, %5 are experimental
12757, %0/%1/%2/%k#
12758]
12759__DATA__
12760#
12761# =============================================================================
12762# This is a template for (neutral: non-virus, non-spam, non-banned) DELIVERY
12763# STATUS NOTIFICATIONS to sender. For syntax and customization instructions
12764# see README.customize. Note that only valid header fields are allowed;
12765# non-standard header field heads must begin with "X-" .
12766# The From, To and Date header fields will be provided automatically.
12767#
12768Subject: Undeliverable mail[?%#X||, invalid characters in header]
12769Message-ID: <DSN%n@%h>
12770
12771[? %#X ||INVALID HEADER (INVALID CHARACTERS OR SPACE GAP)
12772
12773[%X\n]
12774]\
12775This nondelivery report was generated by the amavisd-new program
12776at host %h. Our internal reference code for your message
12777is %n.
12778
12779[? %#X ||
12780WHAT IS AN INVALID CHARACTER IN MAIL HEADER?
12781
12782  The RFC 2822 standard specifies rules for forming internet messages.
12783  It does not allow the use of characters with codes above 127 to be used
12784  directly (non-encoded) in mail header (it also prohibits NUL and bare CR).
12785
12786  If characters (e.g. with diacritics) from ISO Latin or other alphabets
12787  need to be included in the header, these characters need to be properly
12788  encoded according to RFC 2047. This encoding is often done transparently
12789  by mail reader (MUA), but if automatic encoding is not available (e.g.
12790  by some older MUA) it is the user's responsibility to avoid the use
12791  of such characters in mail header, or to encode them manually. Typically
12792  the offending header fields in this category are 'Subject', 'Organization',
12793  and comment fields in e-mail addresses of the 'From', 'To' and 'Cc'.
12794
12795  Sometimes such invalid header fields are inserted automatically
12796  by some MUA, MTA, content checker, or other mail handling service.
12797  If this is the case, that service needs to be fixed or properly configured.
12798  Typically the offending header fields in this category are 'Date',
12799  'Received', 'X-Mailer', 'X-Priority', 'X-Scanned', etc.
12800
12801  If you don't know how to fix or avoid the problem, please report it
12802  to _your_ postmaster or system manager.
12803]\
12804
12805Return-Path: %s
12806Your message[?%m|| %m][?%r|| (Resent-Message-ID: %r)]
12807could not be delivered to:[\n   %N]
12808__DATA__
12809#
12810# =============================================================================
12811# This is a template for VIRUS/BANNED SENDER NOTIFICATIONS.
12812# For syntax and customization instructions see README.customize.
12813# Note that only valid header fields are allowed;
12814# non-standard header field heads must begin with "X-" .
12815# The From, To and Date header fields will be provided automatically.
12816#
12817Subject: [? %#V |[? %#F |Unknown problem|BANNED (%F)]|VIRUS (%V)] IN MAIL FROM YOU
12818[? %m  |#|In-Reply-To: %m]
12819Message-ID: <VS%n@%h>
12820
12821[? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED CONTENTS ALERT]|VIRUS ALERT]
12822
12823Our content checker found
12824[? %#V |#|    [? %#V |viruses|virus|viruses]: %V]
12825[? %#F |#|    banned [? %#F |names|name|names]: %F]
12826[? %#X |#|\n[%X\n]]
12827in email presumably from you (%s),
12828to the following [? %#R |recipients|recipient|recipients]:[
12829-> %R]
12830
12831Our internal reference code for your message is %n.
12832
12833[? %#V ||Please check your system for viruses,
12834or ask your system administrator to do so.
12835
12836]#
12837[? %#D |Delivery of the email was stopped!
12838
12839]#
12840[? %#V |[? %#F ||#
12841The message has been blocked because it contains a component
12842(as a MIME part or nested within) with declared name
12843or MIME type or contents type violating our access policy.
12844
12845To transfer contents that may be considered risky or unwanted
12846by site policies, or simply too large for mailing, please consider
12847publishing your content on the web, and only sending an URL of the
12848document to the recipient.
12849
12850Depending on the recipient and sender site policies, with a little
12851effort it might still be possible to send any contents (including
12852viruses) using one of the following methods:
12853
12854- encrypted using pgp, gpg or other encryption methods;
12855
12856- wrapped in a password-protected or scrambled container or archive
12857  (e.g.: zip -e, arj -g, arc g, rar -p, or other methods)
12858
12859Note that if the contents is not intended to be secret, the
12860encryption key or password may be included in the same message
12861for recipient's convenience.
12862
12863We are sorry for inconvenience if the contents was not malicious.
12864
12865The purpose of these restrictions is to cut the most common propagation
12866methods used by viruses and other malware. These often exploit automatic
12867mechanisms and security holes in certain mail readers (Microsoft mail
12868readers and browsers are a common and easy target). By requiring an
12869explicit and decisive action from the recipient to decode mail,
12870the dangers of automatic malware propagation is largely reduced.
12871#
12872# Details of our mail restrictions policy are available at ...
12873
12874]]#
12875For your reference, here are headers from your email:
12876------------------------- BEGIN HEADERS -----------------------------
12877Return-Path: %s
12878[%H
12879]\
12880-------------------------- END HEADERS ------------------------------
12881__DATA__
12882#
12883# =============================================================================
12884# This is a template for non-spam (VIRUS,...) ADMINISTRATOR NOTIFICATIONS.
12885# For syntax and customization instructions see README.customize.
12886# Note that only valid header fields are allowed; non-standard header
12887# field heads must begin with "X-" .
12888#
12889Date: %d
12890From: %f
12891Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED (%F)]|VIRUS (%V)]#
12892 FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>]
12893To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
12894[? %#C |#|Cc: [<%C>|, ]]
12895Message-ID: <VA%n@%h>
12896
12897[? %#V |No viruses were found.
12898|A virus was found: %V
12899|Two viruses were found:\n  %V
12900|%#V viruses were found:\n  %V
12901]
12902[? %#F |#\
12903|A banned name was found:\n  %F
12904|Two banned names were found:\n  %F
12905|%#F banned names were found:\n  %F
12906]
12907[? %#X |#\
12908|Bad header was found:[\n  %X]
12909]
12910[? %#W |#\
12911|Scanner detecting a virus: %W
12912|Scanners detecting a virus: %W
12913]
12914The mail originated from: <%o>
12915[? %a |#|First upstream SMTP client IP address: \[%a\] %g
12916]
12917[? %t |#|According to the 'Received:' trace, the message originated at:
12918  \[%e\]
12919  %t
12920]
12921[? %#S |Notification to sender will not be mailed.
12922
12923]#
12924[? %#D |#|The message WILL BE delivered to:[\n%D]
12925]
12926[? %#N |#|The message WAS NOT delivered to:[\n%N]
12927]
12928[? %#V |#|[? %#v |#|Virus scanner output:[\n  %v]
12929]]
12930[? %q  |Not quarantined.|The message has been quarantined as:\n  %q
12931]
12932------------------------- BEGIN HEADERS -----------------------------
12933Return-Path: %s
12934[%H
12935]\
12936-------------------------- END HEADERS ------------------------------
12937__DATA__
12938#
12939# =============================================================================
12940# This is a template for VIRUS/BANNED/BAD-HEADER RECIPIENTS NOTIFICATIONS.
12941# For syntax and customization instructions see README.customize.
12942# Note that only valid header fields are allowed; non-standard header
12943# field heads must begin with "X-" .
12944#
12945Date: %d
12946From: %f
12947Subject: [? %#V |[? %#F |[? %#X ||INVALID HEADER]|BANNED]|VIRUS (%V)]#
12948 IN MAIL TO YOU (from [?%o|(?)|<%o>])
12949To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
12950[? %#C |#|Cc: [<%C>|, ]]
12951Message-ID: <VR%n@%h>
12952
12953[? %#V |[? %#F ||BANNED CONTENTS ALERT]|VIRUS ALERT]
12954
12955Our content checker found
12956[? %#V |#|    [? %#V |viruses|virus|viruses]: %V]
12957[? %#F |#|    banned [? %#F |names|name|names]: %F]
12958[? %#X |#|\n[%X\n]]
12959
12960in an email to you [? %S |from unknown sender:|from:]
12961  %o
12962[? %S |claiming to be: %s|#]
12963
12964[? %a |#|First upstream SMTP client IP address: \[%a\] %g
12965]
12966[? %t |#|According to the 'Received:' trace, the message originated at:
12967  \[%e\]
12968  %t
12969]
12970Our internal reference code for this message is %n.
12971[? %q |Not quarantined.|The message has been quarantined as:
12972  %q]
12973
12974Please contact your system administrator for details.
12975__DATA__
12976#
12977# =============================================================================
12978# This is a template for SPAM SENDER NOTIFICATIONS.
12979# For syntax and customization instructions see README.customize.
12980# Note that only valid header fields are allowed;
12981# non-standard header field heads must begin with "X-" .
12982# The From, To and Date header fields will be provided automatically.
12983#
12984Subject: Considered UNSOLICITED BULK EMAIL from you
12985[? %m  |#|In-Reply-To: %m]
12986Message-ID: <SS%n@%h>
12987
12988Your message to:[
12989-> %R]
12990
12991was considered unsolicited bulk e-mail (UBE).
12992[? %#X |#|\n[%X\n]]
12993Subject: %j
12994Return-Path: %s
12995Our internal reference code for your message is %n.
12996
12997[? %#D |Delivery of the email was stopped!
12998]#
12999#
13000# SpamAssassin report:
13001# [%A
13002# ]\
13003__DATA__
13004#
13005# =============================================================================
13006# This is a template for SPAM ADMINISTRATOR NOTIFICATIONS.
13007# For syntax and customization instructions see README.customize.
13008# Note that only valid header fields are allowed; non-standard header
13009# field heads must begin with "X-" .
13010#
13011Date: %d
13012From: %f
13013Subject: SPAM FROM [?%l||LOCAL ][?%a||\[%a\] ][?%o|(?)|<%o>]
13014To: [? %#T |undisclosed-recipients: ;|[<%T>|, ]]
13015[? %#C |#|Cc: [<%C>|, ]]
13016[? %#B |#|Bcc: [<%B>|, ]]
13017Message-ID: <SA%n@%h>
13018
13019Unsolicited bulk email [? %S |from unknown or forged sender:|from:]
13020  %o
13021Subject: %j
13022
13023[? %a |#|First upstream SMTP client IP address: \[%a\] %g
13024]
13025[? %t |#|According to the 'Received:' trace, the message originated at:
13026  \[%e\]
13027  %t
13028]
13029[? %#D |#|The message WILL BE delivered to:[\n%D]
13030]
13031[? %#N |#|The message WAS NOT delivered to:[\n%N]
13032]
13033[? %q |Not quarantined.|The message has been quarantined as:\n  %q
13034]
13035SpamAssassin report:
13036[%A
13037]\
13038
13039------------------------- BEGIN HEADERS -----------------------------
13040Return-Path: %s
13041[%H
13042]\
13043-------------------------- END HEADERS ------------------------------
Note: See TracBrowser for help on using the repository browser.