--- amavisd.conf~	Thu Nov 21 22:35:33 2002
+++ amavisd.conf	Thu Nov 21 23:08:20 2002
@@ -1,4 +1,4 @@
-#use strict;
+use strict;
 
 # Configuration file for amavisd-new
 #
@@ -37,7 +37,7 @@
 
 # Set the user and group to which the daemon will change when started as root:
 $daemon_user  = 'vscan';	# (no default (undef))
-$daemon_group = 'sweep';	# (no default (undef))
+$daemon_group = 'amavis';	# (no default (undef))
 
 # Runtime directory (no trailing slash, defaults to '/var/amavis')
 $TEMPBASE = '/var/amavis';
@@ -86,7 +86,7 @@
 # and see below what these two lookup list really mean.
 #
 # @bypass_virus_checks_acl = qw( . );  # uncomment to DISABLE ANTI-VIRUS code
-# @bypass_spam_checks_acl  = qw( . );  # uncommend to DISABLE ANTI-SPAM code
+# @bypass_spam_checks_acl  = qw( . );  # uncomment to DISABLE ANTI-SPAM code
 #
 # Any setting can be changed with a new assignment, so make sure
 # you do not unintentionally override these settings further down!
@@ -135,7 +135,7 @@
                                   # (default is qw( 127.0.0.1 ) )
 
 # when MTA (one or more) is on a different host, use the following
-# @inet_acl = qw(127/8 10.1.0.1 10.1.0.2);
+# @inet_acl = qw(127/8 10.1.0.1 10.1.0.2);  # adjust the list as appropriate
 # $inet_socket_bind = undef;      # bind to all IP interfaces
 #
 # Example1:
@@ -226,8 +226,8 @@
 # Safe to leave empty, but set it up correctly is you need features that
 # rely on this setting.
 #
-#@local_domains = qw();   # default is empty, no domain is treated as local
-@local_domains = qw( .example.com );
+# @local_domains = qw();   # default is empty, no domain is treated as local
+# @local_domains = qw( .example.com );
 #
 # $local_domains_re = Amavis::Lookup::RE->new( qr'[@.]example\.com$'i );
 
@@ -238,7 +238,7 @@
 #   which in turn defaults to undef (empty) )
 $mailfrom_notify_admin  = 'virusalert@example.com';
 $mailfrom_notify_recip  = 'virusalert@example.com';
-$mailfrom_notify_sender = '';    # null reserse path, like in MTA notifications
+$mailfrom_notify_sender = '';    # null reverse path, like in MTA notifications
 $mailfrom_notify_spamadmin = 'spam.police@example.com';
 
 # whom quarantined messages appear to be sent from (envelope sender)
@@ -379,7 +379,7 @@
 
 # Checking for banned MIME types and names. If any mail part matches,
 # the whole mail is rejected, much like the way viruses are handled.
-# A list @banned_filename_patterns can be defined to provide a list
+# A list @banned_filename_patterns_re can be defined to provide a list
 # of Perl regular expressions to be matched against each part's:
 #
 #  * Content-Type value (both declared and effective mime-type),
@@ -554,13 +554,9 @@
 # does no harm, provided the $recipients_delimiter matches the setting
 # on the final MTA's LDA.
 
-# $addr_extension_virus  = undef;	# (default is undef, same as empty)
-# $addr_extension_spam   = undef;	# (default is undef, same as empty)
-# $addr_extension_banned = undef;	# (default is undef, same as empty)
-#
-$addr_extension_virus = 'virus';
-$addr_extension_spam  = 'spam';
-$addr_extension_banned= 'banned';
+# $addr_extension_virus  = 'virus';	# (default is undef, same as empty)
+# $addr_extension_spam   = 'spam';	# (default is undef, same as empty)
+# $addr_extension_banned = 'banned';	# (default is undef, same as empty)
 
 
 # Delimiter between local part of the recipient address and address extension
@@ -815,8 +811,9 @@
     sub {chdir($TEMPBASE) or die "Can't chdir back to $TEMPBASE $!"},
   ],
 
-  ['KasperskyLab AVPDaemonClient', 'avpdc',
-    [], [0], [3,4,5,6], qr/(?m)infected: (.+)/ ],
+  ['KasperskyLab AVPDaemonClient',
+   ['/opt/AVP/AvpDaemonClient','AvpDaemonClient','avpdc'],
+   '{}', [0], [3,4,5,6], qr/(?m)infected: (.+)/ ],
 
   ['H+B EDV AntiVir', 'antivir',
     '-allfiles -noboot -s -z {}', [0], [1],
@@ -907,7 +904,9 @@
     '-vexit {}', [0], [23],
     qr/(?m)##==>>>> VIRUS ID: CVDL (.+)/ ],
 
-  ['Trend Micro FileScanner', 'vscan',
+  ['Trend Micro FileScanner', ['/etc/iscan/vscan','vscan'],
     '-a {}/*', [0], qr/(?m)Found virus/, qr/(?m)Found virus (.+) in/ ],
 
 );
+
+1;  # insure a defined return
--- amavisd~	Tue Nov 19 20:54:04 2002
+++ amavisd	Thu Nov 21 22:56:25 2002
@@ -1,4 +1,4 @@
-#!/usr/local/bin/perl -T
+#!/usr/bin/perl -T
 
 #------------------------------------------------------------------------------
 # This is amavisd-new.
@@ -405,7 +405,8 @@
     my($config_file) = @_;
     -e($config_file) or die "Cannot find config file $config_file";
     -r(_)            or die "Cannot read config file $config_file";
-    do $config_file or die "Error in config file $config_file: $@";
+    do $config_file;
+    if ($@ ne '') { die "Error in config file $config_file: $@" }
     # compatibility with $mailfrom:
     if (!$mailfrom_notify_admin && !$mailfrom_notify_sender &&
 	!$mailfrom_notify_recip && !$mailfrom_notify_spamadmin) {
@@ -1169,7 +1170,7 @@
     $sth->execute(@keys);  # do the query
     my($a_ref,$found,$match); $match = {};
     while ( defined($a_ref=$sth->fetch) ) {  # fetch query results
-        my(@names) = @{$sth->{NAME_lc}};
+	my(@names) = @{$sth->{NAME_lc}};
 	$found = 1; $match = {}; @$match{@names} = @$a_ref;
 	my($keyname) = @names[0];  my($keyvalue) = $a_ref->[0];
 	do_log(5, "lookup_sql: key($keyname)=\"$keyvalue\" matches, result=(".
@@ -1177,7 +1178,10 @@
 	last if $found; # first match wins, the loop is for possible future use
     }
     $sth->finish();
-    do_log(5, "lookup_sql, no match")  if !$found;
+    if (!$found) {
+	$match = undef;
+	do_log(5, "lookup_sql, no match");
+    }
     # save for future use, but only within processing of this message
     $self->{cache}->{$addr} = $match;
     section_time('lookup_sql');
@@ -2824,11 +2828,11 @@
     my($entity) = shift;
     my($first_received);
     if (defined($entity)) {
-	my($received) = $entity->head->get('received',-1);  # last Received: header
+	my($received) = $entity->head->get('received',-1);  # last Received:
 	$received =~ s/\n([ \t])/$1/g;	# unfold
 	$received =~ s/[\r\n]/ /g;	# turn remaining CR or NL into spaces
 	$first_received = $received;
-	if ($received =~			# not an exact science this parsing
+	if ($received =~		# not an exact science this parsing
 	    /^ (?: \( [^)]* \) | < [^>]* > | \[ [^]]* \] | [^(<\[] )*?
 		\b from \s+
 		( (?: \( [^)]* \) | < [^>]* > | \[ [^]]* \] | [^(<\[] )*? )
@@ -3108,8 +3112,9 @@
 	do_log(3, "Checking for banned (contents-based) file types, " .
 		   scalar(@$parts) . " parts");
 	for my $part (@$parts) {
-	    my($ft) = $file_generator_object->file_type($part);
-	    if ($ft ne '') {   # file type as determined by 'file' util
+	    for my $ft ($file_generator_object->file_type($part),
+			$file_generator_object->file_type_long($part) ) {
+	        next  if $ft eq '';
 		do_log(5, "check_for_banned ($part) - file type: $ft");
 		my($result,$patt) = $acl_re->lookup_re($ft);
 		if ($result) {
@@ -3204,6 +3209,7 @@
 	/^TIFF image data/i           and $ty = '.tif';
 	/^MP3\b/i                     and $ty = '.mp3';
 	/^MPEG\b.*\bstream data/i     and $ty = '.mpeg';
+	/^RIFF.*\bAVI/                and $ty = '.avi';
 
 	/^PostScript document text/i  and $ty = '.ps';
 	/^PDF document/i              and $ty = '.pdf';
@@ -4058,7 +4064,7 @@
 
 use vars qw($spam_level $spam_status $spam_report);
 
-use vars qw($virus_lovers_sql $banned_files_lovers_sql $spam_lovers_sql
+use vars qw($virus_lovers_sql $banned_files_lovers_sql
 	    $bypass_virus_checks_sql $bypass_spam_checks_sql
 	    $spam_tag_level_sql $spam_kill_level_sql);
 
@@ -4185,8 +4191,6 @@
 		Amavis::Lookup::SQLfield->new($sql, 'virus_lover', 'B');
 	$banned_files_lovers_sql =
 		Amavis::Lookup::SQLfield->new($sql, 'banned_file_lover', 'B');
-	$spam_lovers_sql =
-		Amavis::Lookup::SQLfield->new($sql, 'spam_lover', 'B');
 	$bypass_virus_checks_sql =
 		Amavis::Lookup::SQLfield->new($sql, 'bypass_virus_checks','B');
 	$bypass_spam_checks_sql =
@@ -4598,8 +4602,7 @@
 	    my(@offended_recips);  # recipients that consider this mail spam
 	    for my $r (@{$msginfo->per_recip_data}) {
 		if ($r->recip_done) {    # already dealt with
-		} elsif (lookup($r->recip_addr, $spam_lovers_sql) ||
-			 $spam_level < lookup($r->recip_addr,
+		} elsif ($spam_level < lookup($r->recip_addr,
 				$spam_kill_level_sql, $sa_kill_level_deflt) ||
 			 lookup($r->recip_addr, \%spam_lovers,
 				\@spam_lovers_acl, $spam_lovers_re) ) {
@@ -4642,21 +4645,25 @@
 
 	prolong_timer($which_section);
 
-	if ($forward_method ne '') {
-	    # message must be delivered explicitly
+	if ($forward_method ne '') {  # message must be delivered explicitly
 	    $which_section = "forwarding";
-
-	    # UNFINISHED: if spam levels are different for multiple recipients,
-	    #   they should get individually delivered mail. For now the
-	    #   first recipient dictates the spam thresholds for all !!!
-
-	    my($hdr_edits) = Amavis::Out::EditHeader->new;
-	    $hdr_edits = add_forwarding_header_edits(
-		$conn,$msginfo,$hdr_edits,1,$hold);
-	    $msginfo->header_edits($hdr_edits);
 	    # will forward only to those recipients not yet marked
 	    # as 'done' by the above content filtering sections
-	    mail_dispatch($forward_method,$msginfo,0);
+	    for (;;) {
+		my($hdr_edits) = Amavis::Out::EditHeader->new;
+		$hdr_edits = add_forwarding_header_edits_common(
+		    $conn,$msginfo,$hdr_edits,$hold);
+		my($done_all);
+		my($recip_cl);  # ref to a list of similar recip objects
+		($hdr_edits,$recip_cl,$done_all) =
+		    add_forwarding_header_edits_per_recip(
+					     $conn,$msginfo,$hdr_edits,$hold);
+		last  if !@$recip_cl;
+		$msginfo->header_edits($hdr_edits);
+		mail_dispatch($forward_method,$msginfo,0,
+			      sub {my($r)=@_; grep {$_ eq $r} @$recip_cl} );
+		last  if $done_all;
+	    }
 	}
 	prolong_timer($which_section);
 
@@ -4717,12 +4724,12 @@
     ($smtp_resp,$exit_code,$preserve_evidence);
 }
 
-sub add_forwarding_header_edits($$$$$) {
-    my($conn, $msginfo, $hdr_edits, $allow_edits, $hold) = @_;
+sub add_forwarding_header_edits_common($$$$) {
+    my($conn, $msginfo, $hdr_edits, $hold) = @_;
+
     $hdr_edits->prepend_header('Received',
 	received_line($conn,$msginfo,am_id(),1),
-	1)  if $insert_received_line && $mta_in_type ne 'milter';
-
+	1)  if $insert_received_line && $forward_method ne '';
     # discard existing X-AMaViS-HOLD header field, only allow our own
     $hdr_edits->delete_header('X-Amavis-Hold');
     if ($hold ne '') {
@@ -4731,8 +4738,8 @@
     }
     if ($extra_code_antivirus) {
 	if ($X_HEADER_LINE && $X_HEADER_TAG =~ /^[!-9;-\176]+$/) {
-	    $hdr_edits->delete_header($X_HEADER_TAG)  if $allow_edits &&
-		$remove_existing_x_scanned_headers;
+	    if ($remove_existing_x_scanned_headers)
+		{ $hdr_edits->delete_header($X_HEADER_TAG) }
 	    $hdr_edits->append_header($X_HEADER_TAG, $X_HEADER_LINE);
 	}
 	$hdr_edits->delete_header('X-Amavis-Alert');
@@ -4748,34 +4755,84 @@
 	    $hdr_edits->append_header('X-Amavis-Alert', $msg, 1);
 	}
     }
-    if ($extra_code_antispam) {
-# NOT FINISHED: the first recipient gives the levels to all recipients !!!
-	my($tag_level)  = lookup($msginfo->recips->[0],
-				$spam_tag_level_sql,  $sa_tag_level_deflt);
-	my($kill_level) = lookup($msginfo->recips->[0],
-				$spam_kill_level_sql, $sa_kill_level_deflt);
-	$hdr_edits->edit_header('Subject', sub {$sa_spam_subject_tag . $_[1]})
-	    if $allow_edits && $sa_spam_subject_tag ne '' && $spam_level>=$kill_level;
-	$hdr_edits->delete_header('X-Spam-Status');
-	$hdr_edits->delete_header('X-Spam-Flag');
-	$hdr_edits->delete_header('X-Spam-Level');
-	$hdr_edits->delete_header('X-Spam-Report');
-	if ($spam_level >= $tag_level) {
-	    my($full_spam_status) =
+    $hdr_edits;
+}
+
+# Prepare header edits for the first not-yet-done recipient.
+# Inspect remaining recipients, returning the list of recipient objects
+# that are receiving the same set of header edits (so the message may be
+# delivered to them in one transaction).
+#
+sub add_forwarding_header_edits_per_recip($$$$$) {
+    my($conn, $msginfo, $hdr_edits, $hold, $filter) = @_;
+    my(@recip_cluster);
+    my(@per_recip_data) = grep {!$_->recip_done && (!$filter || &$filter($_))}
+			       @{$msginfo->per_recip_data};
+    my($per_recip_data_len) = scalar(@per_recip_data);
+    if (!$extra_code_antispam)
+	{ @recip_cluster = @per_recip_data; @per_recip_data = () }
+    my($first) = 1;  my($cluster_key);
+    for my $r (@per_recip_data) {
+	my($recip) = $r->recip_addr;
+	my($is_local) =
+	    lookup($recip, \@local_domains, $local_domains_re);
+	my($tag_level) =
+	    lookup($recip, $spam_tag_level_sql, $sa_tag_level_deflt);
+	my($kill_level) =
+	    lookup($recip, $spam_kill_level_sql, $sa_kill_level_deflt);
+	my($do_tag)  = $is_local && $spam_level >= min($tag_level,$kill_level);
+	my($do_kill) = $is_local && $spam_level >= $kill_level;
+	$do_tag = $do_tag ? 1 : 0;  $do_kill = $do_kill ? 1 : 0;  # normalize
+	my($spam_level_bar, $full_spam_status);
+	if ($do_tag) {
+	    $spam_level_bar = '*' x (min( max(int($spam_level+0.5),0), 60));
+	    $full_spam_status =
 		sprintf("%s,\n hits=%3.1f\n tagged_above=%3.1f\n required=%3.1f\n %s",
 		    ($spam_level >= $kill_level ? 'Yes' : 'No'),
 		    $spam_level, $tag_level, $kill_level, $spam_status);
-	    $hdr_edits->append_header('X-Spam-Status', $full_spam_status, 1);
-	    if ($spam_level >= $kill_level) {
+	}
+	my($key) = join("\000", $do_tag, $do_kill,
+				$spam_level_bar, $full_spam_status);
+	if ($first) {
+	    do_log(5, "headers CLUSTERING: NEW CLUSTER <$recip>: $do_tag, $do_kill");
+	    $cluster_key = $key;
+	} elsif ($key eq $cluster_key) {
+	    do_log(5, "headers CLUSTERING: <$recip> joining cluster");
+	} else {
+	    do_log(5, "headers CLUSTERING: skipping <$recip> ($do_tag, $do_kill)" );
+	    next;
+	}
+	if ($do_tag || $do_kill) {
+	    $hdr_edits->delete_header('X-Spam-Status');
+	    $hdr_edits->delete_header('X-Spam-Level');
+	    $hdr_edits->delete_header('X-Spam-Flag');
+	    $hdr_edits->delete_header('X-Spam-Report');
+	    if ($do_tag) {
+		$hdr_edits->append_header('X-Spam-Status',$full_spam_status,1);
+		$hdr_edits->append_header('X-Spam-Level',$spam_level_bar);
+	    }
+	    if ($do_kill) {
+		$hdr_edits->edit_header('Subject',
+					sub { $sa_spam_subject_tag . $_[1] }
+		    )  if $sa_spam_subject_tag ne '';
 		$hdr_edits->append_header('X-Spam-Flag', 'YES');
-		$hdr_edits->append_header('X-Spam-Level',
-		    '*' x (min( max(int($spam_level+0.5),0), 40) ));
 #		$hdr_edits->append_header('X-Spam-Report',
 #		    $spam_report, 1)  if $spam_report ne '';
 	    }
 	}
+	push(@recip_cluster, $r);  $first = 0;
     }
-    $hdr_edits;
+    my($done_all);
+    if (@recip_cluster == $per_recip_data_len) {
+	do_log(3, "headers CLUSTERING: ".
+		  "done all $per_recip_data_len recips in one go");
+	$done_all = 1;
+    } else {
+	do_log(3, sprintf("headers CLUSTERING: got %d recips out of %d: %s",
+		    scalar(@recip_cluster), $per_recip_data_len,
+		    join(", ", map {"<".$_->recip_addr.">"} @recip_cluster) ));
+    }
+    ($hdr_edits, \@recip_cluster, $done_all);
 }
 
 sub do_quarantine($$$$) {
@@ -4900,7 +4957,7 @@
     # suggest a name to be used as 'X-Quarantine-id:' or file name
     $VIRUSFILE = sprintf("spam-%s-%s-%s", $body_digest,
 			 strftime("%Y%m%d-%H%M%S",localtime), am_id());
-    # NOT FINISHED: the first recipient gives the levels to all recipients !!!
+    # !!! the first recipient gives the levels to quarantined headers !!!
     my($tag_level)  = lookup($msginfo->recips->[0],
 				$spam_tag_level_sql,  $sa_tag_level_deflt);
     my($kill_level) = lookup($msginfo->recips->[0],
@@ -5023,12 +5080,12 @@
 	my(@fv_cmd) = split(' ',$fv);
 	if (!@fv_cmd) {               # empty, not available
 	} elsif ($fv_cmd[0] =~ /^\//) {  # absolute path
-	    if (-f $fv_cmd[0]) { $found = join(' ',@fv_cmd) }
+	    if (-x $fv_cmd[0] && !-d _) { $found = join(' ',@fv_cmd) }
 	} elsif ($fv_cmd[0] =~ /\//) {   # relative path
 	    die "find_program_path: relative paths not implemented: @fv_cmd\n";
 	} else {                      # walk through the specified PATH
 	    for my $p (@$path_list_ref) {
-		if (-f "$p/$fv_cmd[0]") {
+		if (-x "$p/$fv_cmd[0]" && !-d _) {
 		    $found = $p . '/' . join(' ',@fv_cmd);
 		    last;
 		}
