You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spamassassin.apache.org by he...@apache.org on 2021/05/28 09:40:10 UTC

svn commit: r1890274 - in /spamassassin/trunk: ./ lib/Mail/SpamAssassin/ lib/Mail/SpamAssassin/Conf/ lib/Mail/SpamAssassin/Plugin/ t/

Author: hege
Date: Fri May 28 09:40:09 2021
New Revision: 1890274

URL: http://svn.apache.org/viewvc?rev=1890274&view=rev
Log:
Bug 7735 - Meta rules need to handle missing/unrun dependencies

- Meta rules no longer use priority values, they are evaluated dynamically
  when the rules they depend on are finished.

- API: New $pms->rule_pending() and $pms->rule_ready() functions.
  $pms->rule_pending($rulename) must be called from rules eval-function, if
  the result can arrive later than when exiting the function (async
  lookups).  $pms->rule_ready($rulename) or $pms->got_hit(...) must be
  called when the result has arrived.  If these are not used, it can break
  depending meta rule evaluation.

- API: Deprecated $pms->harvest_until_rule_completes, $pms->is_rule_complete


Added:
    spamassassin/trunk/t/basic_meta_net.t   (with props)
Modified:
    spamassassin/trunk/MANIFEST
    spamassassin/trunk/UPGRADE
    spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Dns.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/ASN.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/AskDNS.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Check.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DCC.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DNSEval.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/HashBL.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/OneLineBodyRuleType.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Pyzor.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Razor2.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Reuse.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/URIDNSBL.pm
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/URILocalBL.pm
    spamassassin/trunk/t/basic_meta2.t
    spamassassin/trunk/t/dcc.t
    spamassassin/trunk/t/if_can.t
    spamassassin/trunk/t/priorities.t

Modified: spamassassin/trunk/MANIFEST
URL: http://svn.apache.org/viewvc/spamassassin/trunk/MANIFEST?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/MANIFEST (original)
+++ spamassassin/trunk/MANIFEST Fri May 28 09:40:09 2021
@@ -249,6 +249,7 @@ t/basic_lint_net.t
 t/basic_lint_without_sandbox.t
 t/basic_meta.t
 t/basic_meta2.t
+t/basic_meta_net.t
 t/basic_obj_api.t
 t/bayesbdb.t
 t/bayesdbm.t

Modified: spamassassin/trunk/UPGRADE
URL: http://svn.apache.org/viewvc/spamassassin/trunk/UPGRADE?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/UPGRADE (original)
+++ spamassassin/trunk/UPGRADE Fri May 28 09:40:09 2021
@@ -2,6 +2,16 @@
 Note for Users Upgrading to SpamAssassin 4.0.0
 ----------------------------------------------
 
+- Meta rules no longer use priority values, they are evaluated dynamically
+  when the rules they depend on are finished.  (Bug 7735)
+
+- API: New $pms->rule_pending() and $pms->rule_ready() functions. 
+  $pms->rule_pending($rulename) must be called from rules eval-function, if
+  the result can arrive later than when exiting the function (async
+  lookups).  $pms->rule_ready($rulename) or $pms->got_hit(...) must be
+  called when the result has arrived.  If these are not used, it can break
+  depending meta rule evaluation.
+
 - Improved internal header address (From/To/Cc) parser, now also handles
   multiple addresses.  Optional support for external Email::Address::XS
   parser, which can handle nested comments and other oddities.
@@ -92,12 +102,14 @@ Note for Users Upgrading to SpamAssassin
   pyzor_max setting to pyzor_count_min.  Added pyzor_whitelist_min and
   pyzor_whitelist_factor setting.  Also try to ignore "empty body" FPs.
 
-- API: deprecated register_async_rule_start/finish calls (they can be left
-  in code for backwards compatibility).  Plugins should only use
-  bgsend_and_start_lookup which handles required things automatically
-  (direct calls to bgsend or start_lookup should not be used),
-  bgsend_and_start_lookup should always contain $ent->{rulename} for correct
-  meta dependency handling. Deprecated also start_lookup, get_lookup, lookup_ns.
+- API: deprecated $pms->register_async_rule_start() and
+  $pms->register_async_rule_finish() calls (they can be left in code for
+  backwards compatibility).  Plugins should only use
+  $pms->bgsend_and_start_lookup() which handles required things
+  automatically (direct calls to bgsend or start_lookup should not be used). 
+  $pms->bgsend_and_start_lookup() should always contain $ent->{rulename} for
+  correct meta dependency handling.  Deprecated also start_lookup,
+  get_lookup, lookup_ns, harvest_until_rule_completes, is_rule_complete.
 
 - SPF: Mail::SPF is now only supported.  Mail::SPF::Query use is deprecated,
   along with settings do_not_use_mail_spf, do_not_use_mail_spf_query.  SPF

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm Fri May 28 09:40:09 2021
@@ -4832,7 +4832,9 @@ sub new {
   $self->{meta_tests} = { };
   $self->{eval_plugins} = { };
   $self->{eval_plugins_types} = { };
-  $self->{duplicate_rules} = { };
+
+  # meta dependencies
+  $self->{meta_dependencies} = {};
 
   # map eval function names to rulenames
   $self->{eval_to_rule} = {};
@@ -5382,7 +5384,6 @@ sub free_uncompiled_rule_source {
   {
     #delete $self->{if_stack}; # it's Parser not Conf?
     #delete $self->{source_file};
-    #delete $self->{meta_dependencies};
   }
 }
 
@@ -5431,6 +5432,7 @@ sub feature_bayes_stopwords { 1 } # mult
 sub feature_get_host { 1 } # $pms->get() :host :domain :ip :revip # was implemented together with AskDNS::has_tag_header # Bug 7734
 sub feature_blocklist_welcomelist { 1 } # bz 7826
 sub feature_header_address_parser { 1 } # improved header address parsing using Email::Address::XS, $pms->get() list context
+sub feature_local_tests_only { 1 } # Config parser supports "if (local_tests_only)"
 sub has_tflags_nosubject { 1 } # tflags nosubject
 sub has_tflags_nolog { 1 } # tflags nolog
 sub perl_min_version_5010000 { return $] >= 5.010000 }  # perl version check ("perl_version" not neatly backwards-compatible)

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm Fri May 28 09:40:09 2021
@@ -546,6 +546,9 @@ sub handle_conditional {
     elsif ($token eq 'perl_version') {
       $eval .= $]." ";
     }
+    elsif ($token eq 'local_tests_only') {
+      $eval .= '($self->{conf}->{main}->{local_tests_only}?1:0) '
+    }
     elsif ($token =~ /^(?:\W{1,5}|[+-]?\d+(?:\.\d+)?)$/) {
       # using tainted subr. argument may taint the whole expression, avoid
       my $u = untaint_var($token);
@@ -885,17 +888,11 @@ sub finish_parsing {
     $conf->{main}->call_plugins("user_conf_parsing_start", { conf => $conf });
   }
 
-  $self->trace_meta_dependencies();
+  # compile meta rules
+  $self->compile_meta_rules();
   $self->fix_priorities();
   $self->fix_tflags();
 
-  # don't do this if allow_user_rules is active, since it deletes entries
-  # from {tests}
-  if (!$conf->{allow_user_rules}) {
-    # Duplicate merging is buggy, disabled, code to be removed
-    #$self->find_dup_rules();          # must be after fix_priorities()
-  }
-
   dbg("config: finish parsing");
 
   while (my ($name, $text) = each %{$conf->{tests}}) {
@@ -969,7 +966,7 @@ sub finish_parsing {
         $conf->{head_tests}->{$priority}->{$name} = $text;
       }
       elsif ($type == $Mail::SpamAssassin::Conf::TYPE_META_TESTS) {
-        $conf->{meta_tests}->{$priority}->{$name} = $text;
+        # Handled by compile_meta_rules()
       }
       elsif ($type == $Mail::SpamAssassin::Conf::TYPE_URI_TESTS) {
         $conf->{uri_tests}->{$priority}->{$name} = $text;
@@ -1008,82 +1005,161 @@ sub finish_parsing {
   }
 }
 
-sub trace_meta_dependencies {
+# Returns all rulenames matching glob (FOO_*)
+sub expand_ruleglob {
+  my ($self, $ruleglob, $rulename) = @_;
+  my $expanded;
+  if (exists $self->{ruleglob_cache}{$ruleglob}) {
+    $expanded = $self->{ruleglob_cache}{$ruleglob};
+  } else {
+    my $reglob = $ruleglob;
+    $reglob =~ s/\?/./g;
+    $reglob =~ s/\*/.*?/g;
+    # Glob rules, but do not match ourselves..
+    my @rules = grep {/^${reglob}$/ && $_ ne $rulename} keys %{$self->{conf}->{scores}};
+    if (@rules) {
+      $expanded = join('+', sort @rules);
+    } else {
+      $expanded = '0';
+    }
+  }
+  my $logstr = $expanded eq '0' ? 'no matches' : $expanded;
+  dbg("rules: meta $rulename rules_matching($ruleglob) expanded: $logstr");
+  $self->{ruleglob_cache}{$ruleglob} = $expanded;
+  return " ($expanded) ";
+}
+
+sub compile_meta_rules {
   my ($self) = @_;
+  my (%meta, %meta_deps, %rule_deps);
   my $conf = $self->{conf};
-  $conf->{meta_dependencies} = { };
 
   foreach my $name (keys %{$conf->{tests}}) {
-    next unless ($conf->{test_types}->{$name}
-                    == $Mail::SpamAssassin::Conf::TYPE_META_TESTS);
-    my $alreadydone = {};
-    $self->_meta_deps_recurse($conf, $name, $name, $alreadydone);
-  }
-}
+    next unless $conf->{test_types}->{$name} == $Mail::SpamAssassin::Conf::TYPE_META_TESTS;
+    my $rule = $conf->{tests}->{$name};
 
-sub _meta_deps_recurse {
-  my ($self, $conf, $toprule, $name, $alreadydone) = @_;
+    # Expand meta rules_matching() before lexing
+    $rule =~ s/${META_RULES_MATCHING_RE}/$self->expand_ruleglob($1,$name)/ge;
 
-  # Avoid recomputing the dependencies of a rule
-  return split(' ', $conf->{meta_dependencies}->{$name}) if defined $conf->{meta_dependencies}->{$name};
+    # Lex the rule into tokens using a rather simple RE method ...
+    my @tokens = ($rule =~ /$ARITH_EXPRESSION_LEXER/og);
 
-  # Obviously, don't trace empty or nonexistent rules
-  my $rule = $conf->{tests}->{$name};
-  unless ($rule) {
-      $conf->{meta_dependencies}->{$name} = '';
-      return ( );
-  }
+    # Set the rule blank to start
+    $meta{$name} = '';
+
+    # List dependencies that are meta tests in the same priority band
+    $meta_deps{$name} = [ ];
+
+    # List all rule dependencies
+    $rule_deps{$name} = [ ];
+
+    # Go through each token in the meta rule
+    foreach my $token (@tokens) {
+      # operator (triage, already validated by is_meta_valid)
+      if ($token !~ tr/+&|()!<>=//c) {
+        $meta{$name} .= "$token ";
+      }
+      # rule-like check for local_tests_only
+      elsif ($token eq 'local_tests_only') {
+        $meta{$name} .= '($_[0]->{main}->{local_tests_only}||0) ';
+      }
+      # ... rulename?
+      elsif ($token =~ IS_RULENAME) {
+        # Will end up later in a compiled sub called from do_meta_tests:
+        #  $_[0] = $pms
+        #  $_[1] = $h ($pms->{tests_already_hit}),
+        #  $_[2] = hashref list of unrun rules
+        $meta{$name} .= "(\$_[1]->{'$token'}||(\$_[2]->{'$token'}?1:0)) ";
+
+        if (!exists $conf->{test_types}->{$token}) {
+          dbg("rules: meta test $name has undefined dependency '$token'");
+          push @{$rule_deps{$name}}, $token;
+          next;
+        }
 
-  # Avoid infinite recursion
-  return ( ) if exists $alreadydone->{$name};
-  $alreadydone->{$name} = ( );
+        if ($conf->{scores}->{$token} == 0) {
+          # bug 5040: net rules in a non-net scoreset
+          # there are some cases where this is expected; don't warn
+          # in those cases.
+          unless ((($conf->get_score_set()) & 1) == 0 &&
+              ($conf->{tflags}->{$token}||'') =~ /\bnet\b/)
+          {
+            dbg("rules: meta test $name has dependency '$token' with a zero score");
+          }
+        }
 
-  my %deps;
+        # If the token is another meta rule, add it as a dependency
+        if ($conf->{test_types}->{$token} == $Mail::SpamAssassin::Conf::TYPE_META_TESTS) {
+          push @{$meta_deps{$name}}, $token;
+        }
 
-  # Lex the rule into tokens using a rather simple RE method ...
-  my @tokens = ($rule =~ /($ARITH_EXPRESSION_LEXER)/og);
+        # Record all dependencies
+        push @{$rule_deps{$name}}, $token;
+      }
+      # ... number or operator (already validated by is_meta_valid)
+      else {
+        $meta{$name} .= "$token ";
+      }
+    }
+  }
 
-  # Go through each token in the meta rule
-  my $conf_tests = $conf->{tests};
-  foreach my $token (@tokens) {
-    # has to be an alpha+numeric token
-    next if $token =~ tr{A-Za-z0-9_}{}c || substr($token,0,1) =~ tr{A-Za-z_}{}c; # even faster
+  # Sort by length of dependencies list.  It's more likely we'll get
+  # the dependencies worked out this way.
+  my @metas = sort { @{$meta_deps{$a}} <=> @{$meta_deps{$b}} } keys %meta;
+  my $count;
+  do {
+    $count = $#metas;
+    my %metas = map { $_ => 1 } @metas; # keep a small cache for fast lookups
+    # Go through each meta rule we haven't done yet
+    for (my $i = 0 ; $i <= $#metas ; $i++) {
+      next if (grep( $metas{$_}, @{ $meta_deps{ $metas[$i] } }));
+      splice @metas, $i--, 1;    # remove this rule from our list
+    }
+  } while ($#metas != $count && $#metas > -1); # run until we can't go anymore
 
-    # and has to be a rule name
-    next unless exists $conf_tests->{$token};
+  # If there are any rules left, we can't solve the dependencies so complain
+  my %unsolved_metas = map { $_ => 1 } @metas; # keep a small cache for fast lookups
+  foreach my $rulename_t (@metas) {
+    my $msg = "rules: excluding meta test $rulename_t, unsolved meta dependencies: ".
+              join(", ", grep($unsolved_metas{$_}, @{ $meta_deps{$rulename_t} }));
+    $self->lint_warn($msg);
+  }
 
-    # add and recurse
-    $deps{untaint_var($token)} = ( );
-    my @subdeps = $self->_meta_deps_recurse($conf, $toprule, $token, $alreadydone);
-    @deps{@subdeps} = ( );
+  foreach my $name (keys %meta) {
+    if (@{$rule_deps{$name}}) {
+      $conf->{meta_dependencies}->{$name} = $rule_deps{$name};
+    }
+    if ($unsolved_metas{$name}) {
+      $conf->{meta_tests}->{$name} = sub { 0 };
+    } else {
+      # Compile meta sub
+      eval '$conf->{meta_tests}->{$name} = sub { '.$meta{$name}.'};';
+      # Paranoid check
+      die "rules: meta compilation failed for $name: '$meta{$name}': $@" if ($@);
+    }
   }
-  $conf->{meta_dependencies}->{$name} = join (' ', keys %deps);
-  return keys %deps;
 }
 
 sub fix_priorities {
   my ($self) = @_;
   my $conf = $self->{conf};
 
-  die unless $conf->{meta_dependencies};    # order requirement
+  return unless $conf->{meta_dependencies};    # order requirement
   my $pri = $conf->{priority};
 
   # sort into priority order, lowest first -- this way we ensure that if we
   # rearrange the pri of a rule early on, we cannot accidentally increase its
   # priority later.
-  foreach my $rule (sort {
-            $pri->{$a} <=> $pri->{$b}
-          } keys %{$pri})
-  {
+  foreach my $rule (sort { $pri->{$a} <=> $pri->{$b} } keys %{$pri}) {
     # we only need to worry about meta rules -- they are the
     # only type of rules which depend on other rules
     my $deps = $conf->{meta_dependencies}->{$rule};
     next unless (defined $deps);
 
     my $basepri = $pri->{$rule};
-    foreach my $dep (split ' ', $deps) {
+    foreach my $dep (@$deps) {
       my $deppri = $pri->{$dep};
-      if ($deppri > $basepri) {
+      if (defined $deppri && $deppri > $basepri) {
         dbg("rules: $rule (pri $basepri) requires $dep (pri $deppri): fixed");
         $pri->{$dep} = $basepri;
       }
@@ -1100,7 +1176,7 @@ sub fix_tflags {
   while (my($rulename,$deps) = each %{$conf->{meta_dependencies}}) {
     my $tfl = $tflags->{$rulename}||'';
     next if $tfl =~ /\bnet\b/;
-    foreach my $deprule (split(' ', $deps)) {
+    foreach my $deprule (@$deps) {
       if (($tflags->{$deprule}||'') =~ /\bnet\b/) {
         dbg("rules: meta $rulename inherits tflag net, depends on $deprule");
         $tflags->{$rulename} = $tfl eq '' ? 'net' : "$tfl net";
@@ -1110,58 +1186,6 @@ sub fix_tflags {
   }
 }
 
-sub find_dup_rules {
-  my ($self) = @_;
-  my $conf = $self->{conf};
-
-  my %names_for_text;
-  my %dups;
-  while (my ($name, $text) = each %{$conf->{tests}}) {
-    my $type = $conf->{test_types}->{$name};
-
-    # skip eval and empty tests
-    next if ($type & 1) ||
-      ($type eq $Mail::SpamAssassin::Conf::TYPE_EMPTY_TESTS);
-
-    my $tf = ($conf->{tflags}->{$name}||''); $tf =~ s/\s+/ /gs;
-    # ensure similar, but differently-typed, rules are not marked as dups;
-    # take tflags into account too due to "tflags multiple"
-    $text = "$type\t$text\t$tf";
-
-    if (defined $names_for_text{$text}) {
-      $names_for_text{$text} .= " ".$name;
-      $dups{$text} = undef;     # found (at least) one
-    } else {
-      $names_for_text{$text} = $name;
-    }
-  }
-
-  foreach my $text (keys %dups) {
-    my $first;
-    my $first_pri;
-    my @names = sort {$a cmp $b} split(' ', $names_for_text{$text});
-    foreach my $name (@names) {
-      my $priority = $conf->{priority}->{$name} || 0;
-
-      if (!defined $first || $priority < $first_pri) {
-        $first_pri = $priority;
-        $first = $name;
-      }
-    }
-    # $first is now the earliest-occurring rule. mark others as dups
-
-    my @dups;
-    foreach my $name (@names) {
-      next if $name eq $first;
-      push @dups, $name;
-      delete $conf->{tests}->{$name};
-    }
-
-    dbg("rules: $first merged duplicates: ".join(' ', @dups));
-    $conf->{duplicate_rules}->{$first} = \@dups;
-  }
-}
-
 # Deprecated function
 sub pack_eval_method {
   warn "deprecated function pack_eval_method() used\n";
@@ -1358,12 +1382,6 @@ sub add_test {
      dbg("config: auto-learn: $name has type $type = $conf->{test_types}->{$name} during add_test\n");
   }
 
-  if ($type == $Mail::SpamAssassin::Conf::TYPE_META_TESTS) {
-    $conf->{priority}->{$name} ||= 500;
-  }
-  else {
-    $conf->{priority}->{$name} ||= 0;
-  }
   $conf->{priority}->{$name} ||= 0;
 
   if ($conf->{main}->{keep_config_parsing_metadata}) {
@@ -1417,8 +1435,8 @@ sub is_meta_valid {
   my $meta = '';
 
   # Paranoid check (Bug #7557)
-  if ($rule =~ /(?:\:\:|->)/)  {
-    warn("config: invalid meta $name rule: $rule") ;
+  if ($rule =~ /(?:\:\:|->|[\$\@\%\;\{\}])/) {
+    warn("config: invalid meta $name rule: $rule\n");
     return 0;
   }
 

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Dns.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Dns.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Dns.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Dns.pm Fri May 28 09:40:09 2021
@@ -311,27 +311,8 @@ sub check_subtest {
   return 0;
 }
 
-sub harvest_until_rule_completes {
-  my ($self, $rule) = @_;
-
-  dbg("dns: harvest_until_rule_completes");
-  my $result = 0;
-
-  for (my $first=1;  ; $first=0) {
-    # complete_lookups() may call completed_callback(), which may
-    # call start_lookup() again (like in Plugin::URIDNSBL)
-    my ($alldone,$anydone) =
-      $self->{async}->complete_lookups($first ? 0 : 1.0,  1);
-
-    $result = 1  if $self->is_rule_complete($rule);
-    last  if $result || $alldone;
-
-    dbg("dns: harvest_until_rule_completes - check_tick");
-    $self->{main}->call_plugins ("check_tick", { permsgstatus => $self });
-  }
-
-  return $result;
-}
+# Deprecated since 4.0, meta rules do not depend on priorities anymore
+sub harvest_until_rule_completes {}
 
 sub harvest_dnsbl_queries {
   my ($self) = @_;
@@ -702,16 +683,19 @@ sub cleanup_kids {
 sub register_async_rule_start {}
 sub register_async_rule_finish {}
 sub mark_all_async_rules_complete {}
+sub is_rule_complete {}
 
-sub is_rule_complete {
+# Return number of pending lookups for a rule,
+# or list all of rules still pending
+sub get_pending_lookups {
   my ($self, $rule) = @_;
-
-  return 1 if !exists $self->{async}->{pending_rules}{$rule};
-  return 1 if !%{$self->{async}->{pending_rules}{$rule}};
-  return 1 if $self->{tests_already_hit}->{$rule};
-
-  dbg("dns: $rule is not complete yet");
-  return 0;
+  if (defined $rule) {
+    return 0 if !exists $self->{async}->{pending_rules}{$rule};
+    return scalar keys %{$self->{async}->{pending_rules}{$rule}};
+  } else {
+    return grep { %{$self->{async}->{pending_rules}{$_}} }
+             keys %{$self->{async}->{pending_rules}};
+  }
 }
 
 ###########################################################################

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm Fri May 28 09:40:09 2021
@@ -294,6 +294,7 @@ sub new {
     'subtest_names_hit' => [ ],
     'spamd_result_log_items' => [ ],
     'tests_already_hit' => { },
+    'tests_pending'     => { },
     'get_cache'         => { },
     'tag_data'          => { },
     'rule_errors'       => 0,
@@ -2862,32 +2863,8 @@ sub add_uri_detail_list {
 
 ###########################################################################
 
-sub ensure_rules_are_complete {
-  my $self = shift;
-  my $metarule = shift;
-  # @_ is now the list of rules
-
-  foreach my $r (@_) {
-    # dbg("rules: meta rule depends on net rule $r");
-    next if ($self->is_rule_complete($r));
-
-    dbg("rules: meta rule $metarule depends on pending rule $r, blocking");
-    my $timer = $self->{main}->time_method("wait_for_pending_rules");
-
-    my $start = time;
-    $self->harvest_until_rule_completes($r);
-    my $elapsed = sprintf "%.2f", time - $start;
-
-    if (!$self->is_rule_complete($r)) {
-      dbg("rules: rule $r is still not complete; exited early?");
-    }
-    elsif ($elapsed > 0) {
-      my $txt = "rules: $r took $elapsed seconds to complete, for $metarule";
-      # Info only if something took over 1 sec to wait, prevent log flood
-      if ($elapsed >= 1) { info($txt); } else { dbg($txt); }
-    }
-  }
-}
+# Deprecated since 4.0, meta rules do not depend on priorities anymore
+sub ensure_rules_are_complete {}
 
 ###########################################################################
 
@@ -3134,18 +3111,54 @@ sub got_hit {
             $params{ruletype},
             $rule_descr);
 
-  # take care of duplicate rules, too (bug 5206)
-  # ... removed as it's buggy
-  #my $dups = $conf_ref->{duplicate_rules}->{$rule};
-  #if ($dups && @{$dups}) {
-  #  foreach my $dup (@{$dups}) {
-  #    $self->got_hit($dup, $area, %params);
-  #  }
-  #}
-
   return 1;
 }
 
+=item $status->rule_pending ($rulename)
+
+Register a pending rule.  Must be called from rules eval-function, if the
+result can arrive later than when exiting the function (async lookups).
+
+$status->rule_done($rulename) or $status->got_hit(...) must be called when
+the result has arrived.  If these are not used, it can break depending meta
+rule evaluation.
+
+=cut
+
+sub rule_pending {
+  my ($self, $rule) = @_;
+
+  $self->{tests_pending}->{$rule} = 1;
+
+  if (exists $self->{tests_already_hit}->{$rule}) {
+    # Only clear result if not hit
+    if ($self->{tests_already_hit}->{$rule} == 0) {
+      delete $self->{tests_already_hit}->{$rule};
+    }
+  }
+}
+
+=item $status->rule_ready ($rulename)
+
+Mark a previously marked $status->rule_pending() rule ready.  Alternatively
+$status->got_hit() will also mark rule ready.  If these are not used, it can
+break depending meta rule evaluation.
+
+=cut
+
+sub rule_ready {
+  my ($self, $rule) = @_;
+
+  if ($self->get_pending_lookups($rule)) {
+    # Can't be ready if there are pending lookups, ignore for now.
+    # Final do_meta_tests() in Check.pm will allow pending anyway.
+    return;
+  }
+
+  delete $self->{tests_pending}->{$rule};
+  $self->{tests_already_hit}->{$rule} ||= 0;
+}
+
 ###########################################################################
 
 =item $status->test_log ($text [, $rulename])

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/ASN.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/ASN.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/ASN.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/ASN.pm Fri May 28 09:40:09 2021
@@ -573,6 +573,7 @@ sub check_asn {
   my ($self, $pms, $re) = @_;
 
   my $rulename = $pms->get_current_eval_rule_name();
+  $pms->rule_pending($rulename); # mark async
 
   if (!defined $re) {
     warn "asn: rule $rulename eval argument missing\n";
@@ -597,6 +598,8 @@ sub check_asn {
 sub _check_asn {
   my ($self, $pms, $rulename, $rec) = @_;
 
+  $pms->rule_ready($rulename); # mark rule ready for metas
+
   my $asn = $pms->get_tag('ASN');
   return if !defined $asn;
 

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/AskDNS.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/AskDNS.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/AskDNS.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/AskDNS.pm Fri May 28 09:40:09 2021
@@ -469,6 +469,8 @@ sub process_response_packet {
       # and returns a number for the rest; we deal with numbers from here on
       $rcode = $rcode_value{$rcode}  if exists $rcode_value{$rcode};
     }
+
+    $pms->rule_ready($rulename); # mark rule ready for metas
   }
   if (!@answer) {
     # a trick to make the following loop run at least once, so that we can

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Check.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Check.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Check.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Check.pm Fri May 28 09:40:09 2021
@@ -28,9 +28,6 @@ use Mail::SpamAssassin::Constants qw(:sa
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
-my $ARITH_EXPRESSION_LEXER = ARITH_EXPRESSION_LEXER;
-my $META_RULES_MATCHING_RE = META_RULES_MATCHING_RE;
-
 # methods defined by the compiled ruleset; deleted in finish_tests()
 our @TEMPORARY_METHODS;
 
@@ -98,6 +95,10 @@ sub check_main {
 
   my @uris = $pms->get_uri_list();
 
+  # initialize meta stuff
+  $pms->{meta_pending} = {};
+  $pms->{meta_pending}->{$_} = 1 foreach (keys %{$pms->{conf}->{meta_tests}});
+
   # Make sure priority -100 exists for launching DNS
   $pms->{conf}->{priorities}->{-100} ||= 1 if $do_dns;
 
@@ -176,14 +177,13 @@ sub check_main {
     $pms->harvest_completed_queries() if $rbls_running;
     last if $pms->{deadline_exceeded} || $pms->{shortcircuited};
 
-    $self->do_meta_tests($pms, $priority);
-    $pms->harvest_completed_queries() if $rbls_running;
-    last if $pms->{deadline_exceeded} || $pms->{shortcircuited};
-
     # we may need to call this more often than once through the loop, but
     # it needs to be done at least once, either at the beginning or the end.
     $self->{main}->call_plugins ("check_tick", { permsgstatus => $pms });
     $pms->harvest_completed_queries() if $rbls_running;
+
+    # check for ready metas
+    $self->do_meta_tests($pms, $priority);
   }
 
   # Finish DNS results
@@ -205,6 +205,12 @@ sub check_main {
   undef $bodytext;
   undef $fulltext;
 
+  # last chance to handle left callbacks, make rule hits etc
+  $self->{main}->call_plugins ("check_cleanup", { permsgstatus => $pms });
+
+  # final check for ready metas
+  $self->finish_meta_tests($pms);
+
   # check dns_block_rule (bug 6728)
   # TODO No idea yet what would be the most logical place to do all these..
   if ($pms->{conf}->{dns_block_rule}) {
@@ -222,8 +228,7 @@ sub check_main {
     }
   }
 
-  # last chance to handle left callbacks, make rule hits etc
-  $self->{main}->call_plugins ("check_cleanup", { permsgstatus => $pms });
+  # PMS cleanup will write reports etc, all rule hits must be registered by now
   $pms->check_cleanup();
 
   if ($pms->{deadline_exceeded}) {
@@ -263,6 +268,113 @@ sub finish_tests {
 
 ###########################################################################
 
+sub do_meta_tests {
+  my ($self, $pms, $priority) = @_;
+
+  # Needed for Reuse to work, otherwise we don't care about priorities
+  if ($self->{main}->have_plugin('start_rules')) {
+    $self->{main}->call_plugins('start_rules', {
+      permsgstatus => $pms,
+      ruletype => 'meta',
+      priority => $priority
+    });
+  }
+
+  return if $self->{am_compiling}; # nothing to compile here
+
+  my $mp = $pms->{meta_pending};
+  my $tp = $pms->{tests_pending};
+  my $md = $pms->{conf}->{meta_dependencies};
+  my $mt = $pms->{conf}->{meta_tests};
+  my $h = $pms->{tests_already_hit};
+  my $retry;
+
+  # Get pending async rule list
+  my %pl = map { $_ => 1 } $pms->get_pending_lookups();
+
+RULE:
+  foreach my $rulename (keys %$mp) {
+    # Meta is not ready if some dependency has not run yet
+    foreach my $deprule (@{$md->{$rulename}||[]}) {
+      if (!exists $h->{$deprule} || $tp->{$deprule} || $pl{$deprule}) {
+        next RULE;
+      }
+    }
+    # Metasubs look like ($_[0]->{$rulename}||($_[1]->{$rulename}?1:0)) ...
+    my $result = $mt->{$rulename}->($pms, $h, {});
+    if ($result) {
+      dbg("rules: ran meta rule $rulename ======> got hit ($result)");
+      $pms->got_hit($rulename, '', ruletype => 'meta', value => $result);
+    } else {
+      dbg("rules-all: ran meta rule $rulename, no hit") if $would_log_rules_all;
+      $h->{$rulename} = 0; # mark meta done
+    }
+    delete $mp->{$rulename};
+    # Reiterate all metas again, in case some meta depended on us
+    $retry = 1;
+  }
+
+  goto RULE if $retry--;
+}
+
+sub finish_meta_tests {
+  my ($self, $pms) = @_;
+
+  return if $self->{am_compiling}; # nothing to compile here
+
+  my $mp = $pms->{meta_pending};
+  my $md = $pms->{conf}->{meta_dependencies};
+  my $mt = $pms->{conf}->{meta_tests};
+  my $h = $pms->{tests_already_hit};
+  my $retry;
+  my %unrun_metas;
+
+RULE:
+  foreach my $rulename (keys %$mp) {
+    my %unrun;
+    # Meta is not ready if some dependency has not run yet
+    foreach my $deprule (@{$md->{$rulename}||[]}) {
+      if (!exists $h->{$deprule}) {
+        # Record all unrun deps for second meta evaluation
+        $unrun{$deprule} = 1;
+      }
+    }
+    # Metasubs look like ($_[0]->{$rulename}||($_[1]->{$rulename}?1:0)) ...
+    my $result = $mt->{$rulename}->($pms, $h, {});
+    my $result2 = $result;
+    if (%unrun) {
+      # Evaluate all unrun rules as true using %unrun list
+      $result2 = $mt->{$rulename}->($pms, $h, \%unrun);
+    }
+    # Evaluated second time with all unrun rules as true.  If result is not
+    # the same, we can't safely finish the meta. (Bug 7735)
+    if ($result != $result2) {
+      $unrun_metas{$rulename} = \%unrun  if $would_log_rules_all;
+      next RULE;
+    } elsif ($result) {
+      dbg("rules: ran meta rule $rulename ======> got hit ($result)");
+      $pms->got_hit($rulename, '', ruletype => 'meta', value => $result);
+    } else {
+      dbg("rules-all: ran meta rule $rulename, no hit") if $would_log_rules_all;
+      $h->{$rulename} = 0; # mark meta done
+    }
+    delete $mp->{$rulename};
+    # Reiterate all metas again, in case some meta depended on us
+    $retry = 1;
+  }
+
+  goto RULE if $retry--;
+
+  if ($would_log_rules_all && %unrun_metas) {
+    foreach (sort keys %unrun_metas) {
+      dbg("rules-all: unrun dependencies prevented meta $_ from running: ".
+          join(', ', sort keys %{$unrun_metas{$_}}));
+    }
+  }
+}
+
+###########################################################################
+
 sub run_rbl_eval_tests {
   my ($self, $pms) = @_;
 
@@ -346,6 +458,7 @@ sub run_generic_tests {
         # start_rules_plugin_code '.$ruletype.' '.$priority.'
         my $scoresptr = $self->{conf}->{scores};
         my $qrptr = $self->{conf}->{test_qrs};
+        my $hitsptr = $self->{tests_already_hit};
     ');
     if (defined $opts{pre_loop_body}) {
       $opts{pre_loop_body}->($self, $pms, $conf, %nopts);
@@ -529,170 +642,6 @@ sub add_temporary_method {
 
 ###########################################################################
 
-# Returns all rulenames matching glob (FOO_*)
-sub expand_ruleglob {
-  my ($self, $ruleglob, $pms, $conf, $rulename) = @_;
-  my $expanded;
-  if (exists $pms->{ruleglob_cache}{$ruleglob}) {
-    $expanded = $pms->{ruleglob_cache}{$ruleglob};
-  } else {
-    my $reglob = $ruleglob;
-    $reglob =~ s/\?/./g;
-    $reglob =~ s/\*/.*?/g;
-    # Glob rules, but do not match ourselves..
-    my @rules = grep {/^${reglob}$/ && $_ ne $rulename} keys %{$conf->{scores}};
-    if (@rules) {
-      $expanded = join('+', sort @rules);
-    } else {
-      $expanded = '0';
-    }
-  }
-  my $logstr = $expanded eq '0' ? 'no matches' : $expanded;
-  dbg("rules: meta $rulename rules_matching($ruleglob) expanded: $logstr");
-  $pms->{ruleglob_cache}{$ruleglob} = $expanded;
-  return " ($expanded) ";
-};
-
-sub do_meta_tests {
-  my ($self, $pms, $priority) = @_;
-  my (%rule_deps, %meta, $rulename);
-
-  $self->run_generic_tests ($pms, $priority,
-    consttype => $Mail::SpamAssassin::Conf::TYPE_META_TESTS,
-    type => 'meta',
-    testhash => $pms->{conf}->{meta_tests},
-    args => [ ],
-    loop_body => sub
-  {
-    my ($self, $pms, $conf, $rulename, $rule, %opts) = @_;
-
-    # Expand meta rules_matching() before lexing
-    $rule =~ s/${META_RULES_MATCHING_RE}/$self->expand_ruleglob($1,$pms,$conf,$rulename)/ge;
-
-    # Lex the rule into tokens using a rather simple RE method ...
-    my @tokens = ($rule =~ /$ARITH_EXPRESSION_LEXER/og);
-
-    # Set the rule blank to start
-    $meta{$rulename} = "";
-
-    # List dependencies that are meta tests in the same priority band
-    $rule_deps{$rulename} = [ ];
-
-    # Go through each token in the meta rule
-    foreach my $token (@tokens) {
-
-      # ... rulename?
-      if ($token =~ IS_RULENAME) {
-        # the " || 0" formulation is to avoid "use of uninitialized value"
-        # warnings; this is better than adding a 0 to a hash for every
-        # rule referred to in a meta...
-        $meta{$rulename} .= "(\$h->{'$token'}||0) ";
-      
-        if (!exists $conf->{scores}->{$token}) {
-          dbg("rules: meta test $rulename has undefined dependency '$token'");
-        }
-        elsif ($conf->{scores}->{$token} == 0) {
-          # bug 5040: net rules in a non-net scoreset
-          # there are some cases where this is expected; don't warn
-          # in those cases.
-          unless ((($conf->get_score_set()) & 1) == 0 &&
-              ($conf->{tflags}->{$token}||'') =~ /\bnet\b/)
-          {
-            info("rules: meta test $rulename has dependency '$token' with a zero score");
-          }
-        }
-
-        # If the token is another meta rule, add it as a dependency
-        push (@{ $rule_deps{$rulename} }, $token)
-          if (exists $conf->{meta_tests}->{$opts{priority}}->{$token});
-      } else {
-        # ... number or operator
-        $meta{$rulename} .= "$token ";
-      }
-    }
-  },
-    pre_loop_body => sub
-  {
-    my ($self, $pms, $conf, %opts) = @_;
-    $self->push_evalstr_prefix($pms, '
-      my $r;
-      my $h = $self->{tests_already_hit};
-    ');
-  },
-    post_loop_body => sub
-  {
-    my ($self, $pms, $conf, %opts) = @_;
-
-    # Sort by length of dependencies list.  It's more likely we'll get
-    # the dependencies worked out this way.
-    my @metas = sort { @{ $rule_deps{$a} } <=> @{ $rule_deps{$b} } }
-                keys %{$conf->{meta_tests}->{$opts{priority}}};
-
-    my $count;
-    my $tflags = $conf->{tflags};
-
-    # Now go ahead and setup the eval string
-    do {
-      $count = $#metas;
-      my %metas = map { $_ => 1 } @metas; # keep a small cache for fast lookups
-
-      # Go through each meta rule we haven't done yet
-      for (my $i = 0 ; $i <= $#metas ; $i++) {
-
-        # If we depend on meta rules that haven't run yet, skip it
-        next if (grep( $metas{$_}, @{ $rule_deps{ $metas[$i] } }));
-
-        # If we depend on network tests, call ensure_rules_are_complete()
-        # to block until they are
-        if (!defined $conf->{meta_dependencies}->{ $metas[$i] }) {
-          warn "no meta_dependencies defined for $metas[$i]";
-        }
-        my $alldeps = join ' ', grep {
-                ($tflags->{$_}||'') =~ /\bnet\b/
-              } split (' ', $conf->{meta_dependencies}->{ $metas[$i] } );
-
-        if ($alldeps ne '') {
-          $self->add_evalstr($pms, '
-            $self->ensure_rules_are_complete(q{'.$metas[$i].'}, qw{'.$alldeps.'});
-          ');
-        }
-
-        # conditionally include the dbg in the eval str
-        my $dbgstr = '';
-        if (would_log('dbg')) {
-          $dbgstr = 'dbg("rules: ran meta rule '.$metas[$i].' ======> got hit");';
-        }
-
-        # Add this meta rule to the eval line
-        $self->add_evalstr($pms, '
-          $r = '.$meta{$metas[$i]}.';
-          if ($r) { $self->got_hit(q#'.$metas[$i].'#, "", ruletype => "meta", value => $r); '.$dbgstr.' }
-        ');
-
-        splice @metas, $i--, 1;    # remove this rule from our list
-      }
-    } while ($#metas != $count && $#metas > -1); # run until we can't go anymore
-
-    # If there are any rules left, we can't solve the dependencies so complain
-    my %metas = map { $_ => 1 } @metas; # keep a small cache for fast lookups
-    foreach my $rulename_t (@metas) {
-      $pms->{rule_errors}++; # flag to --lint that there was an error ...
-      my $msg =
-          "rules: excluding meta test $rulename_t, unsolved meta dependencies: " .
-              join(", ", grep($metas{$_}, @{ $rule_deps{$rulename_t} }));
-      if ($self->{main}->{lint_rules}) {
-        warn $msg."\n";
-      }
-      else {
-        info($msg);
-      }
-    }
-  }
-  );
-}
-
-###########################################################################
-
 sub do_head_tests {
   my ($self, $pms, $priority) = @_;
   # hash to hold the rules, "header\tdefault value" => rulename
@@ -787,6 +736,7 @@ sub do_head_tests {
 
           $self->add_evalstr($pms, '
           if ($scoresptr->{q{'.$rulename.'}}) {
+            $hitsptr->{q{'.$rulename.'}} ||= 0;
             '.$posline.'
             '.$self->hash_line_for_rule($pms, $rulename).'
             '.$ifwhile.' ('.$expr.') {
@@ -880,6 +830,7 @@ sub do_body_tests {
 
     $self->add_evalstr($pms, '
       if ($scoresptr->{q{'.$rulename.'}}) {
+        $hitsptr->{q{'.$rulename.'}} ||= 0;
         '.$sub.'
         '.$self->ran_rule_plugin_code($rulename, "body").'
       }
@@ -941,6 +892,7 @@ sub do_uri_tests {
 
     $self->add_evalstr($pms, '
       if ($scoresptr->{q{'.$rulename.'}}) {
+        $hitsptr->{q{'.$rulename.'}} ||= 0;
         '.$sub.'
         '.$self->ran_rule_plugin_code($rulename, "uri").'
       }
@@ -1001,6 +953,7 @@ sub do_rawbody_tests {
 
     $self->add_evalstr($pms, '
       if ($scoresptr->{q{'.$rulename.'}}) {
+        $hitsptr->{q{'.$rulename.'}} ||= 0;
         '.$sub.'
         '.$self->ran_rule_plugin_code($rulename, "rawbody").'
       }
@@ -1037,6 +990,7 @@ sub do_full_tests {
     $max ||= 0;
     $self->add_evalstr($pms, '
       if ($scoresptr->{q{'.$rulename.'}}) {
+        $hitsptr->{q{'.$rulename.'}} ||= 0;
         pos $$fullmsgref = 0;
         '.$self->hash_line_for_rule($pms, $rulename).'
         dbg("rules-all: running full rule %s", q{'.$rulename.'});
@@ -1193,6 +1147,7 @@ sub run_eval_tests {
 
     $evalstr .= '
     if ($scoresptr->{q{'.$rulename.'}}) {
+      $hitsptr->{q{'.$rulename.'}} ||= 0;
       $rulename = q#'.$rulename.'#;
 ';
  
@@ -1254,6 +1209,7 @@ sub run_eval_tests {
 
     my \$testptr = \$self->{conf}->{$evalname}->{$priority};
     my \$scoresptr = \$self->{conf}->{scores};
+    my \$hitsptr = \$self->{tests_already_hit};
     my \$prepend2desc = q#$prepend2desc#;
     my \$rulename;
     my \$result;
@@ -1403,5 +1359,17 @@ sub free_ruleset_source {
 }
 
 ###########################################################################
+
+sub compile_now_start {
+  my ($self, $params) = @_;
+  $self->{am_compiling} = 1;
+}
+
+sub compile_now_finish {
+  my ($self, $params) = @_;
+  delete $self->{am_compiling};
+}
+
+###########################################################################
 
 1;

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DCC.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DCC.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DCC.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DCC.pm Fri May 28 09:40:09 2021
@@ -730,6 +730,10 @@ sub _check_async {
             foreach (@{$pms->{conf}->{eval_to_rule}->{check_dcc}}) {
               $pms->got_hit($_, "", ruletype => 'eval');
             }
+          } else {
+            foreach (@{$pms->{conf}->{eval_to_rule}->{check_dcc}}) {
+              $pms->rule_ready($_);
+            }
           }
         }
       } else {
@@ -819,7 +823,13 @@ sub check_dcc {
   return 0 if $self->{dcc_disabled};
   return 0 if !$pms->{conf}->{use_dcc};
   return 0 if $pms->{dcc_abort};
-  return 0 if $pms->{dcc_async_start}; # async already handling?
+
+  # async already handling?
+  if ($pms->{dcc_async_start}) {
+    my $rulename = $pms->get_current_eval_rule_name();
+    $pms->rule_pending($rulename); # mark async
+    return 0;
+  }
 
   return $pms->{dcc_result} if defined $pms->{dcc_result};
 
@@ -866,6 +876,8 @@ sub check_dcc_reputation_range {
       # If callback, use got_hit()
       if ($result) {
         $pms->got_hit($cb_rulename, "", ruletype => 'eval');
+      } else {
+        $pms->rule_ready($cb_rulename);
       }
       return 0;
     } else {
@@ -875,6 +887,7 @@ sub check_dcc_reputation_range {
     # Install callback if waiting for async result
     if (!defined $cb_rulename) {
       my $rulename = $pms->get_current_eval_rule_name();
+      $pms->rule_pending($rulename); # mark async
       # array matches check_dcc_reputation_range() argument order
       push @{$pms->{dcc_range_callbacks}}, [undef, $min, $max, $rulename];
     }

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DNSEval.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DNSEval.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DNSEval.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DNSEval.pm Fri May 28 09:40:09 2021
@@ -394,6 +394,8 @@ sub check_rbl_backend {
     return 0;
   }
 
+  $pms->rule_pending($rule); # mark async
+
   dbg("dnseval: only inspecting the following IPs: ".join(", ", @ips));
 
   foreach my $ip (@ips) {
@@ -426,7 +428,9 @@ sub check_rbl_txt {
 }
 
 sub check_rbl_sub {
+  my ($self, $pms, $rule, $set, $subtest) = @_;
   # just a dummy, check_start / init_rbl_subs handles the subs
+  $pms->rule_pending($rule); # mark async
   return 0;
 }
 
@@ -438,6 +442,8 @@ sub check_rbl_from_host {
   return 0 if $self->{main}->{conf}->{skip_rbl_checks};
   return 0 if !$pms->is_dns_available();
 
+  $pms->rule_pending($rule); # mark async
+
   $self->_check_rbl_addresses($pms, $rule, $set, $rbl_server,
     $subtest, $pms->all_from_addrs());
 }
@@ -701,6 +707,8 @@ sub check_dns_sender {
   $host = idn_to_ascii($host);
   dbg("dnseval: checking A and MX for host $host");
 
+  $pms->rule_pending($rule); # mark async
+
   $self->do_sender_lookup($pms, $rule, 'A', $host);
   $self->do_sender_lookup($pms, $rule, 'MX', $host);
 
@@ -717,7 +725,8 @@ sub do_sender_lookup {
   $pms->{async}->bgsend_and_start_lookup(
     $host, $type, undef, $ent, sub {
       my ($ent, $pkt) = @_;
-      return if !$pkt;
+      return if !$pkt; # aborted / timed out
+      $pms->rule_ready($ent->{rulename}); # mark as run, could still hit
       foreach my $answer ($pkt->answer) {
         next if !$answer;
         next if $answer->type ne 'A' && $answer->type ne 'MX';

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/HashBL.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/HashBL.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/HashBL.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/HashBL.pm Fri May 28 09:40:09 2021
@@ -444,6 +444,8 @@ sub check_hashbl_emails {
     $seen{$email} = 1;
   }
 
+  return 0 unless @filtered_emails;
+
   # Randomize order
   if ($opts =~ /\bshuffle\b/) {
     Mail::SpamAssassin::Util::fisher_yates_shuffle(\@filtered_emails);
@@ -453,6 +455,8 @@ sub check_hashbl_emails {
   my $max = $opts =~ /\bmax=(\d+)\b/ ? $1 : 10;
   $#filtered_emails = $max-1 if scalar @filtered_emails > $max;
 
+  $pms->rule_pending($rulename); # mark async
+
   foreach my $email (@filtered_emails) {
     $self->_submit_query($pms, $rulename, $email, $list, $opts, $subtest);
   }
@@ -513,6 +517,8 @@ sub check_hashbl_uris {
     $seen{$uri} = 1;
   }
 
+  return 0 unless @filtered_uris;
+
   # Randomize order
   if ($opts =~ /\bshuffle\b/) {
     Mail::SpamAssassin::Util::fisher_yates_shuffle(\@filtered_uris);
@@ -522,6 +528,8 @@ sub check_hashbl_uris {
   my $max = $opts =~ /\bmax=(\d+)\b/ ? $1 : 10;
   $#filtered_uris = $max-1 if scalar @filtered_uris > $max;
 
+  $pms->rule_pending($rulename); # mark async
+
   foreach my $furi (@filtered_uris) {
     $self->_submit_query($pms, $rulename, $furi, $list, $opts, $subtest);
   }
@@ -608,6 +616,8 @@ sub check_hashbl_bodyre {
   my $max = $opts =~ /\bmax=(\d+)\b/ ? $1 : 10;
   $#matches = $max-1 if scalar @matches > $max;
 
+  $pms->rule_pending($rulename); # mark async
+
   foreach my $match (@matches) {
     $self->_submit_query($pms, $rulename, $match, $list, $opts, $subtest);
   }
@@ -673,6 +683,8 @@ sub _finish_query {
     return;
   }
 
+  $pms->rule_ready($rulename); # mark rule ready for metas
+
   my $dnsmatch = $ent->{subtest} ? $ent->{subtest} : qr/^127\./;
   my @answer = $pkt->answer;
   foreach my $rr (@answer) {

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/OneLineBodyRuleType.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/OneLineBodyRuleType.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/OneLineBodyRuleType.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/OneLineBodyRuleType.pm Fri May 28 09:40:09 2021
@@ -92,6 +92,7 @@ sub do_one_line_body_tests {
     my $sub = '
       my ($self, $line) = @_;
       my $qrptr = $self->{main}->{conf}->{test_qrs};
+      my $hitsptr = $self->{tests_already_hit};
     ';
 
     if (($conf->{tflags}->{$rulename}||'') =~ /\bmultiple\b/)
@@ -101,8 +102,8 @@ sub do_one_line_body_tests {
       $max = untaint_var($max);
       if ($max) {
         $sub .= '
-          if (exists $self->{tests_already_hit}->{q{'.$rulename.'}}) {
-            return 0 if $self->{tests_already_hit}->{q{'.$rulename.'}} >= '.$max.';
+          if ($hitsptr->{q{'.$rulename.'}}) {
+            return 0 if $hitsptr->{q{'.$rulename.'}} >= '.$max.';
           }
         ';
       }
@@ -129,6 +130,10 @@ sub do_one_line_body_tests {
 
     }
 
+    $sub .= '
+      $self->rule_ready(q{'.$rulename.'});
+    ';
+
     return if ($opts{doing_user_rules} &&
                   !$self->is_user_rule_sub($rulename.'_one_line_body_test'));
 

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Pyzor.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Pyzor.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Pyzor.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Pyzor.pm Fri May 28 09:40:09 2021
@@ -324,6 +324,7 @@ sub check_pyzor {
   ## forking method
 
   $pms->{pyzor_rulename} = $pms->get_current_eval_rule_name();
+  $pms->rule_pending($pms->{pyzor_rulename}); # mark async
 
   # create socketpair for communication
   $pms->{pyzor_backchannel} = Mail::SpamAssassin::SubProcBackChannel->new();
@@ -502,6 +503,8 @@ sub _check_forked_result {
     return 0;
   }
 
+  $pms->rule_ready($pms->{pyzor_rulename}); # mark rule ready for metas
+
   dbg("pyzor: child process $kid_pid finished, reading results");
 
   my $backmsg;

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Razor2.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Razor2.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Razor2.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Razor2.pm Fri May 28 09:40:09 2021
@@ -443,6 +443,7 @@ sub check_razor2 {
   ## forking method
 
   $pms->{razor2_rulename} = $pms->get_current_eval_rule_name();
+  $pms->rule_pending($pms->{razor2_rulename}); # mark async
 
   # create socketpair for communication
   $pms->{razor2_backchannel} = Mail::SpamAssassin::SubProcBackChannel->new();
@@ -549,6 +550,8 @@ sub _check_forked_result {
     return 0;
   }
 
+  $pms->rule_ready($pms->{razor2_rulename}); # mark rule ready for metas
+
   dbg("razor2: child process $kid_pid finished, reading results");
 
   my $backmsg;
@@ -635,16 +638,21 @@ sub check_razor2_range {
   # continue.
   return unless $self->{razor2_available};
   return unless $self->{main}->{conf}->{use_razor2};
-  return if $pms->{razor2_abort};
 
   # Check if callback overriding rulename
   if (!defined $rulename) {
     $rulename = $pms->get_current_eval_rule_name();
   }
 
+  if ($pms->{razor2_abort}) {
+    $pms->rule_ready($rulename); # mark rule ready for metas
+    return;
+  }
+
   # If forked, call back later unless results are in
   if ($self->{main}->{conf}->{razor_fork}) {
     if (!defined $pms->{razor2_result}) {
+      $pms->rule_pending($rulename); # mark async
       dbg("razor2: delaying check_razor2_range call for $rulename");
       # array matches check_razor2_range() argument order
       push @{$pms->{razor2_range_callbacks}},
@@ -660,6 +668,8 @@ sub check_razor2_range {
     }
   }
 
+  $pms->rule_ready($rulename); # mark rule ready for metas
+
   my $cf = 0;
   if ($engine) {
     $cf = $pms->{razor2_cf_score}->{$engine};

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Reuse.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Reuse.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Reuse.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Reuse.pm Fri May 28 09:40:09 2021
@@ -197,6 +197,8 @@ sub check_start {
         dbg("reuse: rule $rule hit, will add at priority $priority, stage " .
            "$stage");
         last;
+      } else {
+        $pms->rule_ready($rule);
       }
     }
   }

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/URIDNSBL.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/URIDNSBL.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/URIDNSBL.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/URIDNSBL.pm Fri May 28 09:40:09 2021
@@ -342,6 +342,9 @@ sub new {
 
 # this is just a placeholder; in fact the results are dealt with later	 
 sub check_uridnsbl {
+  my ($self, $pms) = @_;
+  my $rulename = $pms->get_current_eval_rule_name();
+  $pms->rule_pending($rulename); # mark async
   return 0;
 }
 
@@ -1058,19 +1061,19 @@ sub lookup_single_dnsbl {
 sub complete_dnsbl_lookup {
   my ($self, $pms, $ent, $pkt) = @_;
 
+  my $rulename = $ent->{rulename};
+
   if (!$pkt) {
     # $pkt will be undef if the DNS query was aborted (e.g. timed out)
     dbg("uridnsbl: complete_dnsbl_lookup aborted %s %s",
-        $ent->{rulename}, $ent->{key});
+        $rulename, $ent->{key});
     return;
   }
 
-  dbg("uridnsbl: complete_dnsbl_lookup $ent->{key} $ent->{rulename}");
-  my $conf = $pms->{conf};
-
-  my $rulename = $ent->{rulename};
-  my $rulecf = $conf->{uridnsbls}->{$rulename};
+  $pms->rule_ready($rulename); # mark rule ready for metas
+  dbg("uridnsbl: complete_dnsbl_lookup $ent->{key} $rulename");
 
+  my $rulecf = $pms->{conf}->{uridnsbls}->{$rulename};
   my @subtests;
   my @answer = $pkt->answer;
   foreach my $rr (@answer)
@@ -1127,7 +1130,9 @@ sub complete_dnsbl_lookup {
           :              sprintf('%08x%s%08x', $n1,$delim,$n2),
           $match ? 'match' : 'no');
     }
-    $self->got_dnsbl_hit($pms, $ent, $rdatastr, $rulename) if $match;
+    if ($match) {
+      $self->got_dnsbl_hit($pms, $ent, $rdatastr, $rulename);
+    }
   }
 }
 

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/URILocalBL.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/URILocalBL.pm?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/URILocalBL.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/URILocalBL.pm Fri May 28 09:40:09 2021
@@ -390,9 +390,13 @@ sub check_uri_local_bl {
     }
   }
 
+  return 0 unless %found_hosts;
+
   # bail out now if dns not available
   return 0 if !$pms->is_dns_available();
 
+  $pms->rule_pending($rulename); # mark async
+
   foreach my $host (keys %found_hosts) {
     $host = idn_to_ascii($host);
     dbg("launching A/AAAA lookup for $host");
@@ -428,6 +432,8 @@ sub _finish_lookup {
       return;
   }
 
+  $pms->rule_ready($rulename); # mark rule ready for metas
+
   my @answer = $pkt->answer;
   my @addrs;
   foreach my $rr (@answer) {

Modified: spamassassin/trunk/t/basic_meta2.t
URL: http://svn.apache.org/viewvc/spamassassin/trunk/t/basic_meta2.t?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/t/basic_meta2.t (original)
+++ spamassassin/trunk/t/basic_meta2.t Fri May 28 09:40:09 2021
@@ -5,23 +5,29 @@ use lib 't';
 use SATest; sa_t_init("basic_meta2");
 
 use Test::More;
-plan tests => 5;
+plan tests => 15;
 
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
   q{ TEST_FOO_1 }     => '',
   q{ TEST_FOO_2 }     => '',
   q{ TEST_FOO_3 }     => '',
-  q{ TEST_META_1 }     => '',
-
+  q{ TEST_META_1 }    => '',
+  q{ TEST_META_3 }    => '',
+  q{ TEST_META_5 }    => '',
+  q{ TEST_META_7 }    => '',
+  q{ TEST_META_A }    => '',
+  q{ TEST_META_B }    => '',
 );
 
 %anti_patterns = (
-
   q{ TEST_NEG_1 }     => '',
-
+  q{ TEST_META_2 }    => '',
+  q{ TEST_META_4 }    => '',
+  q{ TEST_META_6 }    => '',
+  q{ TEST_META_8 }    => '',
+  q{ TEST_META_9 }    => '',
 );
 
 tstlocalrules (qq{
@@ -39,6 +45,41 @@ tstlocalrules (qq{
 
    meta TEST_META_1 (TEST_FOO_1 + TEST_FOO_2 + TEST_NEG_1) == 2
 
+   ##
+   ## Unrun rule dependencies (Bug 7735)
+   ##
+
+   # Non-existing rule
+   # Should not hit, meta is evaled twice: (!0) && (!1)
+   meta TEST_META_2 !NONEXISTINGRULE
+   # Should hit, meta is evaled twice: (!0 || 0) && (!1 || 1)
+   meta TEST_META_3 !NONEXISTINGRULE || NONEXISTINGRULE
+
+   # Disabled rule, same as above
+   body TEST_DISABLED /a/
+   score TEST_DISABLED 0
+   # Should not hit
+   meta TEST_META_4 !TEST_DISABLED
+   # Should hit
+   meta TEST_META_5 !TEST_DISABLED || TEST_DISABLED
+
+   # Unrun rule (due to local tests only), same as above
+   askdns TEST_DISABLED2 spamassassin.org TXT /./
+   # Should not hit
+   meta TEST_META_6 !TEST_DISABLED2
+   # Should hit
+   meta TEST_META_7 !TEST_DISABLED2 || TEST_DISABLED2
+
+   # Should not hit
+   meta TEST_META_8 __FOO_1 + NONEXISTINGRULE == 2
+   # Should not hit
+   meta TEST_META_9 __FOO_1 + NONEXISTINGRULE + __FOO_2 == 2
+   # Should hit (both eval checks are true thanks to >1)
+   meta TEST_META_A __FOO_1 + NONEXISTINGRULE + __FOO_2 > 1
+
+   # local_tests_only
+   meta TEST_META_B NONEXISTINGRULE || local_tests_only
+
 });
 
 sarun ("-L -t < data/nice/001 2>&1", \&patterns_run_cb);

Added: spamassassin/trunk/t/basic_meta_net.t
URL: http://svn.apache.org/viewvc/spamassassin/trunk/t/basic_meta_net.t?rev=1890274&view=auto
==============================================================================
--- spamassassin/trunk/t/basic_meta_net.t (added)
+++ spamassassin/trunk/t/basic_meta_net.t Fri May 28 09:40:09 2021
@@ -0,0 +1,88 @@
+#!/usr/bin/perl -T
+
+use lib '.'; 
+use lib 't';
+use SATest; sa_t_init("basic_meta_net");
+use Test::More;
+
+plan skip_all => "Net tests disabled"          unless conf_bool('run_net_tests');
+plan skip_all => "Can't use Net::DNS Safely"   unless can_use_net_dns_safely();
+
+plan tests => 20;
+
+# ---------------------------------------------------------------------------
+
+
+%patterns = (
+  q{ X_META_POS4 } => '',
+);
+%anti_patterns = (
+  q{ X_URIBL_A }    => '',
+  q{ X_ASKDNS }     => '',
+  q{ X_META_POS1 }  => '',
+  q{ X_META_POS2 }  => '',
+  q{ X_META_POS3 }  => '',
+  q{ X_META_NEG1 }  => '',
+  q{ X_META_NEG2 }  => '',
+  q{ X_META_NEG3 }  => '',
+  q{ X_META_NEG4 } => '',
+);
+
+#
+# Nothing should hit with a failed lookup
+#
+
+tstlocalrules (qq{
+   # Force DNS queries to fail/timeout
+   rbl_timeout 2 1
+   dns_server 240.0.0.240
+
+   urirhssub  X_URIBL_A  dnsbltest.spamassassin.org. A 2
+   body       X_URIBL_A  eval:check_uridnsbl('X_URIBL_A')
+   tflags     X_URIBL_A  net
+
+   askdns     X_ASKDNS spamassassin.org TXT /./
+
+   meta X_META_POS1 X_URIBL_A
+   meta X_META_POS2 X_ASKDNS
+   meta X_META_POS3 X_URIBL_A || X_ASKDNS
+
+   meta X_META_NEG1 !X_URIBL_A
+   meta X_META_NEG2 !X_ASKDNS
+   meta X_META_NEG3 !X_URIBL_A || !X_ASKDNS
+
+   # local_tests_only
+   meta X_META_NEG4 local_tests_only
+   meta X_META_POS4 !local_tests_only
+});
+
+sarun ("-t < data/spam/dnsbl.eml 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+
+#
+# Local only, nothing should hit as nothing is queried
+#
+
+tstlocalrules (qq{
+   urirhssub  X_URIBL_A  dnsbltest.spamassassin.org. A 2
+   body       X_URIBL_A  eval:check_uridnsbl('X_URIBL_A')
+   tflags     X_URIBL_A  net
+
+   askdns     X_ASKDNS spamassassin.org TXT /./
+
+   meta X_META_POS1 X_URIBL_A
+   meta X_META_POS2 X_ASKDNS
+   meta X_META_POS3 X_URIBL_A || X_ASKDNS
+
+   meta X_META_NEG1 !X_URIBL_A
+   meta X_META_NEG2 !X_ASKDNS
+   meta X_META_NEG3 !X_URIBL_A || !X_ASKDNS
+
+   # local_tests_only
+   meta X_META_POS4 local_tests_only
+   meta X_META_NEG4 !local_tests_only
+});
+
+sarun ("-t -L < data/spam/dnsbl.eml", \&patterns_run_cb);
+ok_all_patterns();
+

Propchange: spamassassin/trunk/t/basic_meta_net.t
------------------------------------------------------------------------------
    svn:executable = *

Modified: spamassassin/trunk/t/dcc.t
URL: http://svn.apache.org/viewvc/spamassassin/trunk/t/dcc.t?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/t/dcc.t (original)
+++ spamassassin/trunk/t/dcc.t Fri May 28 09:40:09 2021
@@ -9,7 +9,7 @@ use Test::More;
 plan skip_all => "Net tests disabled" unless conf_bool('run_net_tests');
 plan skip_all => "DCC tests disabled" unless conf_bool('run_dcc_tests');
 plan skip_all => "DCC executable not found in path" unless HAS_DCC;
-plan tests => 8;
+plan tests => 16;
 
 diag('Note: Failure may not be an SpamAssassin bug, as DCC tests can fail due to problems with the DCC servers.');
 
@@ -17,15 +17,17 @@ diag('Note: Failure may not be an SpamAs
 # ---------------------------------------------------------------------------
 
 %patterns = (
-
   q{ spam reported to DCC }, 'dcc report',
-
 );
 
 tstprefs ("
   dns_available no
   use_dcc 1
+  meta X_META_POS DCC_CHECK
+  meta X_META_NEG !DCC_CHECK
   score DCC_CHECK 3.3
+  score X_META_POS 3.3
+  score X_META_NEG 3.3
 ");
 
 ok sarun ("-t -D info -r < data/spam/gtubedcc.eml 2>&1", \&patterns_run_cb);
@@ -34,12 +36,26 @@ ok sarun ("-t -D info -r < data/spam/gtu
 ok_all_patterns();
 
 %patterns = (
-
   q{ 3.3 DCC_CHECK }, 'dcc',
-
+  q{ 3.3 X_META_POS }, 'pos',
+);
+%anti_patterns = (
+  q{ 3.3 X_META_NEG }, 'neg',
 );
 
 ok sarun ("-t < data/spam/gtubedcc.eml 2>&1", \&patterns_run_cb);
 ok_all_patterns();
 ok sarun ("-t < data/spam/gtubedcc_crlf.eml 2>&1", \&patterns_run_cb);
 ok_all_patterns();
+
+# Local only, metas should not hit as no queries are made
+%patterns = (
+);
+%anti_patterns = (
+  q{ 3.3 DCC_CHECK }, 'dcc',
+  q{ 3.3 X_META_POS }, 'pos',
+  q{ 3.3 X_META_NEG }, 'neg',
+);
+ok sarun ("-t -L < data/spam/gtubedcc.eml 2>&1", \&patterns_run_cb);
+ok_all_patterns();
+

Modified: spamassassin/trunk/t/if_can.t
URL: http://svn.apache.org/viewvc/spamassassin/trunk/t/if_can.t?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/t/if_can.t (original)
+++ spamassassin/trunk/t/if_can.t Fri May 28 09:40:09 2021
@@ -2,7 +2,7 @@
 
 use lib '.'; use lib 't';
 use SATest; sa_t_init("if_can");
-use Test::More tests => 17;
+use Test::More tests => 19;
 
 # ---------------------------------------------------------------------------
 
@@ -20,6 +20,7 @@ use Test::More tests => 17;
   q{ SHOULD_BE_CALLED09 }, 'should_be_called09',
   q{ SHOULD_BE_CALLED10 }, 'should_be_called10',
   q{ SHOULD_BE_CALLED11 }, 'should_be_called11',
+  q{ SHOULD_BE_CALLED12 }, 'should_be_called12',
 
 );
 %anti_patterns = (
@@ -28,6 +29,7 @@ use Test::More tests => 17;
   q{ SHOULD_NOT_BE_CALLED02 }, 'should_not_be_called02',
   q{ SHOULD_NOT_BE_CALLED03 }, 'should_not_be_called03',
   q{ SHOULD_NOT_BE_CALLED04 }, 'should_not_be_called04',
+  q{ SHOULD_NOT_BE_CALLED05 }, 'should_not_be_called05',
 
 );
 tstlocalrules (q{
@@ -82,6 +84,13 @@ tstlocalrules (q{
   endif
   endif
 
+  if can(Mail::SpamAssassin::Conf::feature_local_tests_only) && local_tests_only
+    body SHOULD_BE_CALLED12 /./
+  endif
+  if can(Mail::SpamAssassin::Conf::feature_local_tests_only) && !local_tests_only
+    body SHOULD_NOT_BE_CALLED05 /./
+  endif
+
 });
 
 ok (sarun ("-L -t < data/spam/gtube.eml", \&patterns_run_cb));

Modified: spamassassin/trunk/t/priorities.t
URL: http://svn.apache.org/viewvc/spamassassin/trunk/t/priorities.t?rev=1890274&r1=1890273&r2=1890274&view=diff
==============================================================================
--- spamassassin/trunk/t/priorities.t (original)
+++ spamassassin/trunk/t/priorities.t Fri May 28 09:40:09 2021
@@ -83,8 +83,8 @@ ok assert_rule_pri 'FOO1', -28;
 sub assert_rule_pri {
   my ($r, $pri) = @_;
 
-  if (defined $conf->{rbl_evals}->{$r}) {
-    # ignore rbl_evals; they do not use the priority system at all
+  if (defined $conf->{rbl_evals}->{$r} || defined $conf->{meta_tests}->{$r}) {
+    # ignore rbl_evals and metas; they do not use the priority system at all
     return 1;
   }