File Coverage

blib/lib/Mail/SpamAssassin/Plugin/Check.pm
Criterion Covered Total %
statement 961 1249 76.9
branch 196 460 42.6
condition 44 110 40.0
subroutine 95 100 95.0
pod 3 32 9.3
total 1299 1951 66.5


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Mail::SpamAssassin::Plugin::Check - primary message check functionality
4              
5             =head1 SYNOPSIS
6              
7             loadplugin Mail::SpamAssassin::Plugin::Check
8              
9             =head1 DESCRIPTION
10              
11             This plugin provides the primary message check functionality.
12              
13             =cut
14              
15             package Mail::SpamAssassin::Plugin::Check;
16              
17 22     22   164 use strict;
  22         52  
  22         685  
18 22     22   117 use warnings;
  22         47  
  22         905  
19 22     22   134 use re 'taint';
  22         53  
  22         959  
20              
21 22     22   146 use Time::HiRes qw(time);
  22         59  
  22         345  
22              
23 22     22   3793 use Mail::SpamAssassin::Plugin;
  22         61  
  22         586  
24 22     22   124 use Mail::SpamAssassin::Logger;
  22         63  
  22         1402  
25 22     22   180 use Mail::SpamAssassin::Util qw(untaint_var);
  22         43  
  22         1262  
26 22     22   172 use Mail::SpamAssassin::Timeout;
  22         47  
  22         687  
27 22     22   145 use Mail::SpamAssassin::Constants qw(:sa);
  22         48  
  22         48138  
28              
29             our @ISA = qw(Mail::SpamAssassin::Plugin);
30              
31             my $ARITH_EXPRESSION_LEXER = ARITH_EXPRESSION_LEXER;
32             my $META_RULES_MATCHING_RE = META_RULES_MATCHING_RE;
33              
34             # methods defined by the compiled ruleset; deleted in finish_tests()
35             our @TEMPORARY_METHODS;
36              
37             # constructor
38             sub new {
39 59     59 1 250 my $class = shift;
40 59         115 my $mailsaobject = shift;
41              
42 59   33     427 $class = ref($class) || $class;
43 59         464 my $self = $class->SUPER::new($mailsaobject);
44 59         180 bless ($self, $class);
45              
46 59         503 return $self;
47             }
48              
49             ###########################################################################
50              
51             sub check_main {
52 96     96 1 307 my ($self, $args) = @_;
53              
54 96         237 my $pms = $args->{permsgstatus};
55              
56 96         250 my $suppl_attrib = $pms->{msg}->{suppl_attrib};
57 96 50 66     598 if (ref $suppl_attrib && ref $suppl_attrib->{rule_hits}) {
58 0         0 my @caller_rule_hits = @{$suppl_attrib->{rule_hits}};
  0         0  
59 0         0 dbg("check: adding caller rule hits, %d rules", scalar(@caller_rule_hits));
60 0         0 for my $caller_rule_hit (@caller_rule_hits) {
61 0 0       0 next if ref $caller_rule_hit ne 'HASH';
62             my($rulename, $area, $score, $defscore, $value,
63             $ruletype, $tflags, $description) =
64 0         0 @$caller_rule_hit{qw(rule area score defscore value
65             ruletype tflags descr)};
66 0 0       0 $pms->got_hit($rulename, $area,
    0          
    0          
    0          
    0          
67             !defined $score ? () : (score => $score),
68             !defined $defscore ? () : (defscore => $defscore),
69             !defined $value ? () : (value => $value),
70             !defined $tflags ? () : (tflags => $tflags),
71             !defined $description ? () : (description => $description),
72             ruletype => $ruletype);
73             }
74             }
75              
76             # bug 4353:
77             # Do this before the RBL tests are kicked off. The metadata parsing
78             # will figure out the (un)trusted relays and such, which are used in the
79             # rbl calls.
80 96         745 $pms->extract_message_metadata();
81              
82             # Here, we launch all the DNS RBL queries and let them run while we
83             # inspect the message
84 96         699 $self->run_rbl_eval_tests($pms);
85 96         288 my $needs_dnsbl_harvest_p = 1; # harvest needs to be run
86              
87 96         367 my $decoded = $pms->get_decoded_stripped_body_text_array();
88 96         437 my $bodytext = $pms->get_decoded_body_text_array();
89 96         477 my $fulltext = $pms->{msg}->get_pristine();
90 96         435 my $master_deadline = $pms->{master_deadline};
91 96 50       825 dbg("check: check_main, time limit in %.3f s",
92             $master_deadline - time) if $master_deadline;
93              
94 96         569 my @uris = $pms->get_uri_list();
95              
96 96         210 foreach my $priority (sort { $a <=> $b } keys %{$pms->{conf}->{priorities}}) {
  207         713  
  96         836  
97             # no need to run if there are no priorities at this level. This can
98             # happen in Conf.pm when we switch a rule from one priority to another
99 258 50       1062 next unless ($pms->{conf}->{priorities}->{$priority} > 0);
100              
101 258 50 33     2889 if ($pms->{deadline_exceeded}) {
    50          
    50          
102 0         0 last;
103             } elsif ($master_deadline && time > $master_deadline) {
104 0         0 info("check: exceeded time limit, skipping further tests");
105 0         0 $pms->{deadline_exceeded} = 1;
106 0         0 last;
107             } elsif ($self->{main}->call_plugins("have_shortcircuited",
108             { permsgstatus => $pms })) {
109             # if shortcircuiting is hit, we skip all other priorities...
110 0         0 last;
111             }
112              
113 258         1344 my $timer = $self->{main}->time_method("tests_pri_".$priority);
114 258         1242 dbg("check: running tests for priority: $priority");
115              
116             # only harvest the dnsbl queries once priority HARVEST_DNSBL_PRIORITY
117             # has been reached and then only run once
118             #
119             # TODO: is this block still needed here? is HARVEST_DNSBL_PRIORITY used?
120             #
121 258 100 100     1478 if ($priority >= HARVEST_DNSBL_PRIORITY
      66        
122             && $needs_dnsbl_harvest_p
123             && !$self->{main}->call_plugins("have_shortcircuited",
124             { permsgstatus => $pms }))
125             {
126             # harvest the DNS results
127 81         503 $pms->harvest_dnsbl_queries();
128 81         172 $needs_dnsbl_harvest_p = 0;
129              
130             # finish the DNS results
131 81         374 $pms->rbl_finish();
132 81         447 $self->{main}->call_plugins("check_post_dnsbl", { permsgstatus => $pms });
133 81 100       366 $pms->{resolver}->finish_socket() if $pms->{resolver};
134             }
135              
136 258         1291 $pms->harvest_completed_queries();
137             # allow other, plugin-defined rule types to be called here
138 258         1552 $self->{main}->call_plugins ("check_rules_at_priority",
139             { permsgstatus => $pms, priority => $priority, checkobj => $self });
140              
141             # do head tests
142 258         1369 $self->do_head_tests($pms, $priority);
143 258         5968 $pms->harvest_completed_queries();
144 258 50       865 last if $pms->{deadline_exceeded};
145              
146 258         989 $self->do_head_eval_tests($pms, $priority);
147 258         1029 $pms->harvest_completed_queries();
148 258 50       804 last if $pms->{deadline_exceeded};
149              
150 258         974 $self->do_body_tests($pms, $priority, $decoded);
151 258         3905 $pms->harvest_completed_queries();
152 258 50       803 last if $pms->{deadline_exceeded};
153              
154 258         1005 $self->do_uri_tests($pms, $priority, @uris);
155 258         3222 $pms->harvest_completed_queries();
156 258 50       792 last if $pms->{deadline_exceeded};
157              
158 258         1021 $self->do_body_eval_tests($pms, $priority, $decoded);
159 258         887 $pms->harvest_completed_queries();
160 258 50       824 last if $pms->{deadline_exceeded};
161            
162 258         860 $self->do_rawbody_tests($pms, $priority, $bodytext);
163 258         3408 $pms->harvest_completed_queries();
164 258 50       778 last if $pms->{deadline_exceeded};
165              
166 258         928 $self->do_rawbody_eval_tests($pms, $priority, $bodytext);
167 258         701 $pms->harvest_completed_queries();
168 258 50       663 last if $pms->{deadline_exceeded};
169            
170 258         923 $self->do_full_tests($pms, $priority, \$fulltext);
171 258         2683 $pms->harvest_completed_queries();
172 258 50       869 last if $pms->{deadline_exceeded};
173              
174 258         982 $self->do_full_eval_tests($pms, $priority, \$fulltext);
175 258         865 $pms->harvest_completed_queries();
176 258 50       861 last if $pms->{deadline_exceeded};
177              
178 258         924 $self->do_meta_tests($pms, $priority);
179 258         6601 $pms->harvest_completed_queries();
180 258 50       812 last if $pms->{deadline_exceeded};
181              
182             # we may need to call this more often than once through the loop, but
183             # it needs to be done at least once, either at the beginning or the end.
184 258         1559 $self->{main}->call_plugins ("check_tick", { permsgstatus => $pms });
185 258         1025 $pms->harvest_completed_queries();
186 258 50       1195 last if $pms->{deadline_exceeded};
187             }
188              
189             # sanity check, it is possible that no rules >= HARVEST_DNSBL_PRIORITY ran so the harvest
190             # may not have run yet. Check, and if so, go ahead and harvest here.
191 96 100       501 if ($needs_dnsbl_harvest_p) {
192 15 50       57 if (!$self->{main}->call_plugins("have_shortcircuited",
193             { permsgstatus => $pms }))
194             {
195             # harvest the DNS results
196 15         64 $pms->harvest_dnsbl_queries();
197             }
198              
199             # finish the DNS results
200 15         119 $pms->rbl_finish();
201 15         90 $self->{main}->call_plugins ("check_post_dnsbl", { permsgstatus => $pms });
202 15 50       59 $pms->{resolver}->finish_socket() if $pms->{resolver};
203             }
204              
205 96 50       353 if ($pms->{deadline_exceeded}) {
206 0         0 $pms->got_hit('TIME_LIMIT_EXCEEDED', '', defscore => 0.001,
207             description => 'Exceeded time limit / deadline');
208             }
209              
210             # finished running rules
211 96         282 delete $pms->{current_rule_name};
212 96         188 undef $decoded;
213 96         181 undef $bodytext;
214 96         210 undef $fulltext;
215              
216 96 50 33     6888 if ($pms->{deadline_exceeded}) {
    50          
217             # dbg("check: exceeded time limit, skipping auto-learning");
218             } elsif ($master_deadline && time > $master_deadline) {
219 0         0 info("check: exceeded time limit, skipping auto-learning");
220 0         0 $pms->{deadline_exceeded} = 1;
221             } else {
222             # auto-learning
223 96         694 $pms->learn();
224 102         649 $self->{main}->call_plugins ("check_post_learn", { permsgstatus => $pms });
225             }
226              
227             # track user_rules recompilations; each scanned message is 1 tick on this counter
228 102 50       507 if ($self->{done_user_rules}) {
229 0         0 my $counters = $pms->{conf}->{want_rebuild_for_type};
230 0         0 foreach my $type (keys %{$self->{done_user_rules}}) {
  6         26  
231 0 0       0 if ($counters->{$type} > 0) {
232 0         0 $counters->{$type}--;
233             }
234             dbg("rules: user rules done; ticking want_rebuild counter for type $type to ".
235 0         0 $counters->{$type});
236             }
237             }
238              
239 96         426 return 1;
240             }
241              
242             sub finish_tests {
243 34     34 1 140 my ($self, $params) = @_;
244              
245 34         141 foreach my $method (@TEMPORARY_METHODS) {
246 103         210 undef &{$method};
  103         12755  
247             }
248 34         200 @TEMPORARY_METHODS = (); # clear for next time
249             }
250              
251             ###########################################################################
252              
253             sub run_rbl_eval_tests {
254 96     96 0 322 my ($self, $pms) = @_;
255 96         301 my ($rulename, $pat, @args);
256              
257             # XXX - possible speed up, moving this check out of the subroutine into Check->new()
258 96 100       368 if ($self->{main}->{local_tests_only}) {
259 92         306 dbg("rules: local tests only, ignoring RBL eval");
260 92         209 return 0;
261             }
262              
263 4         11 while (my ($rulename, $test) = each %{$pms->{conf}->{rbl_evals}}) {
  4         29  
264 0         0 my $score = $pms->{conf}->{scores}->{$rulename};
265 0 0       0 next unless $score;
266              
267 0         0 %{$pms->{test_log_msgs}} = (); # clear test state
  0         0  
268              
269 0         0 my $function = $test->[0];
270 0 0       0 if (!exists $pms->{conf}->{eval_plugins}->{$function}) {
271 0         0 warn("rules: unknown eval '$function' for $rulename, ignoring RBL eval\n");
272 0         0 return 0;
273             }
274              
275 0         0 my $result;
276             eval {
277 0         0 $result = $pms->$function($rulename, @{$test->[1]}); 1;
  0         0  
  0         0  
278 0 0       0 } or do {
279 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
280 0 0       0 die "rules: $eval_stat\n" if $eval_stat =~ /__alarm__ignore__/;
281 0         0 warn "rules: failed to run $rulename RBL test, skipping:\n".
282             "\t($eval_stat)\n";
283 0         0 $pms->{rule_errors}++;
284 0         0 next;
285             };
286             }
287             }
288              
289             ###########################################################################
290              
291             sub run_generic_tests {
292 1548     1548 0 8889 my ($self, $pms, $priority, %opts) = @_;
293              
294 1548         3001 my $master_deadline = $pms->{master_deadline};
295 1548 50 33     12073 if ($pms->{deadline_exceeded}) {
    50          
    50          
296 0         0 return;
297             } elsif ($master_deadline && time > $master_deadline) {
298 0         0 info("check: (run_generic) exceeded time limit, skipping further tests");
299 0         0 $pms->{deadline_exceeded} = 1;
300 0         0 return;
301             } elsif ($self->{main}->call_plugins("have_shortcircuited",
302             { permsgstatus => $pms })) {
303 0         0 return;
304             }
305              
306 1548         3802 my $ruletype = $opts{type};
307 1548         8727 dbg("rules: running $ruletype tests; score so far=".$pms->{score});
308 1548         2493 %{$pms->{test_log_msgs}} = (); # clear test state
  1548         2962  
309              
310 1548         2558 my $conf = $pms->{conf};
311 1548         3659 my $doing_user_rules = $conf->{want_rebuild_for_type}->{$opts{consttype}};
312 1548 50       3030 if ($doing_user_rules) { $self->{done_user_rules}->{$opts{consttype}}++; }
  0         0  
313              
314             # clean up priority value so it can be used in a subroutine name
315 1548         2225 my $clean_priority;
316 1548         3646 ($clean_priority = $priority) =~ s/-/neg/;
317 1548         2335 my $package_name = __PACKAGE__;
318 1548         3272 my $methodname = $package_name."::_".$ruletype."_tests_".$clean_priority;
319              
320 1548 100 66     1964 if (!defined &{$methodname} || $doing_user_rules) {
  1548         11835  
321              
322             # use %nopts for named parameter-passing; it's more friendly
323             # to future-proof subclassing, since new parameters can be added without
324             # breaking third-party subclassed implementations of this plugin.
325 150         638 my %nopts = (
326             ruletype => $ruletype,
327             doing_user_rules => $doing_user_rules,
328             priority => $priority,
329             clean_priority => $clean_priority
330             );
331              
332             # build up the eval string...
333 150         328 $self->{evalstr_methodname} = $methodname;
334 150         258 $self->{evalstr_chunk_current_methodname} = undef;
335 150         298 $self->{evalstr_chunk_methodnames} = [];
336 150         304 $self->{evalstr_chunk_prefix} = []; # stack (array) of source code sections
337 150         272 $self->{evalstr} = ''; $self->{evalstr_l} = 0;
  150         222  
338 150         242 $self->{evalstr2} = '';
339 150         431 $self->begin_evalstr_chunk($pms);
340              
341 150         591 $self->push_evalstr_prefix($pms, '
342             # start_rules_plugin_code '.$ruletype.' '.$priority.'
343             my $scoresptr = $self->{conf}->{scores};
344             my $qrptr = $self->{conf}->{test_qrs};
345             ');
346 150 100       363 if (defined $opts{pre_loop_body}) {
347 75         281 $opts{pre_loop_body}->($self, $pms, $conf, %nopts);
348             }
349 150         619 $self->add_evalstr($pms,
350             $self->start_rules_plugin_code($ruletype, $priority) );
351 150         249 while (my($rulename, $test) = each %{$opts{testhash}->{$priority}}) {
  274         1228  
352 124         358 $opts{loop_body}->($self, $pms, $conf, $rulename, $test, %nopts);
353             }
354 150 100       429 if (defined $opts{post_loop_body}) {
355 50         181 $opts{post_loop_body}->($self, $pms, $conf, %nopts);
356             }
357              
358             # dbg("rules: generated matching code:\n".$self->{evalstr});
359              
360 150         429 $self->flush_evalstr($pms, 'run_generic_tests');
361 150         439 $self->free_ruleset_source($pms, $ruletype, $priority);
362              
363             # clear out a previous version of this method
364 150         222 undef &{$methodname};
  150         746  
365              
366             # generate the loop that goes through each line...
367 150         751 my $evalstr = <<"EOT";
368             {
369             package $package_name;
370              
371             $self->{evalstr2}
372              
373             sub $methodname {
374             EOT
375              
376 150         238 for my $chunk_methodname (@{$self->{evalstr_chunk_methodnames}}) {
  150         321  
377 150         349 $evalstr .= " $chunk_methodname(\@_);\n";
378             }
379              
380 150         241 $evalstr .= <<"EOT";
381             }
382              
383             1;
384             }
385             EOT
386              
387 150         272 delete $self->{evalstr}; # free up some RAM before we eval()
388 150         213 delete $self->{evalstr2};
389 150         212 delete $self->{evalstr_methodname};
390 150         205 delete $self->{evalstr_chunk_current_methodname};
391 150         248 delete $self->{evalstr_chunk_methodnames};
392 150         228 delete $self->{evalstr_chunk_prefix};
393              
394 150         495 dbg("rules: run_generic_tests - compiling eval code: %s, priority %s",
395             $ruletype, $priority);
396             # dbg("rules: eval code to compile: %s", $evalstr);
397 150         204 my $eval_result;
398 150         192 { my $timer = $self->{main}->time_method('compile_gen');
  150         427  
399 150     96   12093 $eval_result = eval($evalstr);
  96     81   2178  
  81     81   1650  
  81     96   1522  
  96     81   2063  
  81     81   1570  
  81     96   1649  
  96     81   2088  
  81     81   1498  
  81     96   1627  
  96     81   2014  
  81     81   1526  
  81     96   1508  
  96     81   2009  
  81     81   1527  
  81     96   1586  
  96     81   1952  
  81     81   1621  
  81         1531  
400             }
401 150 50       452 if (!$eval_result) {
402 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
403 0         0 warn "rules: failed to compile $ruletype tests, skipping:\n".
404             "\t($eval_stat)\n";
405 0         0 $pms->{rule_errors}++;
406 0         0 return;
407             }
408 150         608 dbg("rules: compiled $ruletype tests");
409             }
410              
411             #run_compiled_method:
412             # dbg("rules: run_generic_tests - calling %s", $methodname);
413 1548         6931 my $t = Mail::SpamAssassin::Timeout->new({ deadline => $master_deadline });
414             my $err = $t->run(sub {
415 22     22   218 no strict "refs";
  22         73  
  22         96525  
416 1548     1548   2442 $methodname->($pms, @{$opts{args}});
  1548         46448  
417 1548         9249 });
418 1548 0 33     7279 if ($t->timed_out() && $master_deadline && time > $master_deadline) {
      33        
419 0         0 info("check: exceeded time limit in $methodname, skipping further tests");
420 0         0 $pms->{deadline_exceeded} = 1;
421             }
422             }
423              
424             sub begin_evalstr_chunk {
425 300     300 0 504 my ($self, $pms) = @_;
426 300         400 my $n = 0;
427 300 50       661 if ($self->{evalstr_chunk_methodnames}) {
428 300         376 $n = scalar(@{$self->{evalstr_chunk_methodnames}});
  300         519  
429             }
430 300         1350 my $chunk_methodname = sprintf("%s_%d", $self->{evalstr_methodname}, $n+1);
431             # dbg("rules: begin_evalstr_chunk %s", $chunk_methodname);
432 300         448 undef &{$chunk_methodname};
  300         2025  
433 300         541 my $package_name = __PACKAGE__;
434 300         693 my $evalstr = <<"EOT";
435             package $package_name;
436             sub $chunk_methodname {
437             my \$self = shift;
438             my \$hits = 0;
439             EOT
440 300         379 $evalstr .= ' '.$_ for @{$self->{evalstr_chunk_prefix}};
  300         1847  
441 300         648 $self->{evalstr} = $evalstr;
442 300         501 $self->{evalstr_l} = length($evalstr);
443 300         817 $self->{evalstr_chunk_current_methodname} = $chunk_methodname;
444             }
445              
446             sub end_evalstr_chunk {
447 150     150 0 240 my ($self, $pms) = @_;
448             # dbg("rules: end_evalstr_chunk");
449 150         228 my $evalstr = "}; 1;\n";
450 150         279 $self->{evalstr} .= $evalstr;
451 150         278 $self->{evalstr_l} += length($evalstr);
452             }
453              
454             sub flush_evalstr {
455 150     150 0 266 my ($self, $pms, $caller_name) = @_;
456 150         231 my $chunk_methodname = $self->{evalstr_chunk_current_methodname};
457 150         371 $self->end_evalstr_chunk($pms);
458             dbg("rules: flush_evalstr (%s) compiling %d chars of %s",
459 150         472 $caller_name, $self->{evalstr_l}, $chunk_methodname);
460             # dbg("rules: eval code(2): %s", $self->{evalstr});
461 150         192 my $eval_result;
462 150         191 { my $timer = $self->{main}->time_method('compile_gen');
  150         402  
463 27 50 66 27   193 $eval_result = eval($self->{evalstr});
  27 50 66 8   98  
  27     14   5897  
  26     96   232  
  26     81   70  
  26     81   678  
  8     96   64  
  8     81   26  
  8     81   391  
  150     96   22952  
  96     81   321  
  96     81   196  
  96     96   292  
  96     81   1564  
  81     81   252  
  81     96   163  
  81     81   227  
  81     81   1166  
  81     96   271  
  81     81   161  
  81     81   262  
  81         1140  
  96         400  
  96         189  
  96         297  
  96         205  
  96         1368  
  81         243  
  81         158  
  81         261  
  81         192  
  81         1177  
  81         287  
  81         193  
  81         228  
  81         190  
  81         1176  
  96         310  
  96         208  
  96         368  
  96         242  
  96         340  
  81         347  
  81         231  
  81         191  
  81         227  
  81         231  
  81         1162  
  81         239  
  81         165  
  81         250  
  81         185  
  81         1164  
  96         336  
  96         197  
  96         253  
  96         236  
  96         193  
  96         1367  
  81         245  
  81         162  
  81         312  
  81         197  
  81         157  
  81         1213  
  81         249  
  81         191  
  81         233  
  81         186  
  81         154  
  81         173  
  81         582  
  81         241  
  0         0  
  81         419  
  81         1281  
  0         0  
  96         311  
  96         193  
  96         350  
  96         1430  
  81         295  
  81         158  
  81         254  
  81         1166  
  81         228  
  81         150  
  81         233  
  81         1216  
  96         362  
  96         207  
  96         299  
  96         485  
  81         249  
  81         178  
  81         233  
  81         1172  
  81         254  
  81         168  
  81         261  
  81         1185  
464             }
465 150 50       733 if (!$eval_result) {
466 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
467 0         0 warn "rules: failed to compile $chunk_methodname, skipping:\n".
468             "\t($eval_stat)\n";
469 0         0 $pms->{rule_errors}++;
470             } else {
471 150         252 push(@{$self->{evalstr_chunk_methodnames}}, $chunk_methodname);
  150         544  
472             }
473 150         323 $self->{evalstr} = ''; $self->{evalstr_l} = 0;
  150         231  
474 150         427 $self->begin_evalstr_chunk($pms);
475             }
476              
477             sub push_evalstr_prefix {
478 273     273 0 475 my ($self, $pms, $str) = @_;
479 273         625 $self->add_evalstr_corked($pms, $str); # must not flush!
480 273         332 push(@{$self->{evalstr_chunk_prefix}}, $str);
  273         738  
481             # dbg("rules: push_evalstr_prefix (%d) - <%s>",
482             # scalar(@{$self->{evalstr_chunk_prefix}}), $str);
483             }
484              
485             sub pop_evalstr_prefix {
486 48     48 0 83 my ($self) = @_;
487 48         63 pop(@{$self->{evalstr_chunk_prefix}});
  48         230  
488             # dbg("rules: pop_evalstr_prefix (%d)",
489             # scalar(@{$self->{evalstr_chunk_prefix}}));
490             }
491              
492             sub add_evalstr {
493 274     274 0 489 my ($self, $pms, $str) = @_;
494 274 100 66     1088 if (defined $str && $str ne '') {
495 124         202 my $new_code_l = length($str);
496             # dbg("rules: add_evalstr %d - <%s>", $new_code_l, $str);
497 124         318 $self->{evalstr} .= $str;
498 124         193 $self->{evalstr_l} += $new_code_l;
499 124 50       443 if ($self->{evalstr_l} > 60000) {
500 0         0 $self->flush_evalstr($pms, 'add_evalstr');
501             }
502             }
503             }
504              
505             # similar to add_evalstr, but avoids flushing on size
506             sub add_evalstr_corked {
507 273     273 0 430 my ($self, $pms, $str) = @_;
508 273 50       574 if (defined $str) {
509 273         356 my $new_code_l = length($str);
510 273         610 $self->{evalstr} .= $str;
511 273         507 $self->{evalstr_l} += $new_code_l;
512             }
513             }
514              
515             sub add_evalstr2 {
516 0     0 0 0 my ($self, $str) = @_;
517 0         0 $self->{evalstr2} .= $str;
518             }
519              
520             sub add_temporary_method {
521 0     0 0 0 my ($self, $methodname, $methodbody) = @_;
522 0         0 $self->add_evalstr2 (' sub '.$methodname.' { '.$methodbody.' } ');
523 0         0 push (@TEMPORARY_METHODS, $methodname);
524             }
525              
526             ###########################################################################
527              
528             # Returns all rulenames matching glob (FOO_*)
529             sub expand_ruleglob {
530 0     0 0 0 my ($self, $ruleglob, $pms, $conf, $rulename) = @_;
531 0         0 my $expanded;
532 0 0       0 if (exists $pms->{ruleglob_cache}{$ruleglob}) {
533 0         0 $expanded = $pms->{ruleglob_cache}{$ruleglob};
534             } else {
535 0         0 my $reglob = $ruleglob;
536 0         0 $reglob =~ s/\?/./g;
537 0         0 $reglob =~ s/\*/.*?/g;
538             # Glob rules, but do not match ourselves..
539 0 0       0 my @rules = grep {/^${reglob}$/ && $_ ne $rulename} keys %{$conf->{scores}};
  0         0  
  0         0  
540 0 0       0 if (@rules) {
541 0         0 $expanded = join('+', sort @rules);
542             } else {
543 0         0 $expanded = '0';
544             }
545             }
546 0 0       0 my $logstr = $expanded eq '0' ? 'no matches' : $expanded;
547 0         0 dbg("rules: meta $rulename rules_matching($ruleglob) expanded: $logstr");
548 0         0 $pms->{ruleglob_cache}{$ruleglob} = $expanded;
549 0         0 return " ($expanded) ";
550             };
551              
552             sub do_meta_tests {
553 258     258 0 631 my ($self, $pms, $priority) = @_;
554 258         507 my (%rule_deps, %meta, $rulename);
555              
556             $self->run_generic_tests ($pms, $priority,
557             consttype => $Mail::SpamAssassin::Conf::TYPE_META_TESTS,
558             type => 'meta',
559             testhash => $pms->{conf}->{meta_tests},
560             args => [ ],
561             loop_body => sub
562             {
563 16     16   58 my ($self, $pms, $conf, $rulename, $rule, %opts) = @_;
564              
565             # Expand meta rules_matching() before lexing
566 16         81 $rule =~ s/${META_RULES_MATCHING_RE}/$self->expand_ruleglob($1,$pms,$conf,$rulename)/ge;
  0         0  
567              
568             # Lex the rule into tokens using a rather simple RE method ...
569 16         197 my @tokens = ($rule =~ /$ARITH_EXPRESSION_LEXER/og);
570              
571             # Set the rule blank to start
572 16         53 $meta{$rulename} = "";
573              
574             # List dependencies that are meta tests in the same priority band
575 16         34 $rule_deps{$rulename} = [ ];
576              
577             # Go through each token in the meta rule
578 16         37 foreach my $token (@tokens) {
579              
580             # ... rulename?
581 96 100       263 if ($token =~ IS_RULENAME) {
582             # the " || 0" formulation is to avoid "use of uninitialized value"
583             # warnings; this is better than adding a 0 to a hash for every
584             # rule referred to in a meta...
585 40         98 $meta{$rulename} .= "(\$h->{'$token'}||0) ";
586            
587 40 50       159 if (!exists $conf->{scores}->{$token}) {
    50          
588 0         0 dbg("rules: meta test $rulename has undefined dependency '$token'");
589             }
590             elsif ($conf->{scores}->{$token} == 0) {
591             # bug 5040: net rules in a non-net scoreset
592             # there are some cases where this is expected; don't warn
593             # in those cases.
594 0 0 0     0 unless ((($conf->get_score_set()) & 1) == 0 &&
      0        
595             ($conf->{tflags}->{$token}||'') =~ /\bnet\b/)
596             {
597 0         0 info("rules: meta test $rulename has dependency '$token' with a zero score");
598             }
599             }
600              
601             # If the token is another meta rule, add it as a dependency
602 0         0 push (@{ $rule_deps{$rulename} }, $token)
603 40 50       129 if (exists $conf->{meta_tests}->{$opts{priority}}->{$token});
604             } else {
605             # ... number or operator
606 56         126 $meta{$rulename} .= "$token ";
607             }
608             }
609             },
610             pre_loop_body => sub
611             {
612 25     25   111 my ($self, $pms, $conf, %opts) = @_;
613 25         77 $self->push_evalstr_prefix($pms, '
614             my $r;
615             my $h = $self->{tests_already_hit};
616             ');
617             },
618             post_loop_body => sub
619             {
620 25     25   98 my ($self, $pms, $conf, %opts) = @_;
621              
622             # Sort by length of dependencies list. It's more likely we'll get
623             # the dependencies worked out this way.
624 8         21 my @metas = sort { @{ $rule_deps{$a} } <=> @{ $rule_deps{$b} } }
  8         21  
  8         56  
625 25         51 keys %{$conf->{meta_tests}->{$opts{priority}}};
  25         128  
626              
627 25         53 my $count;
628 25         56 my $tflags = $conf->{tflags};
629              
630             # Now go ahead and setup the eval string
631 25   66     62 do {
632 25         65 $count = $#metas;
633 25         65 my %metas = map { $_ => 1 } @metas; # keep a small cache for fast lookups
  16         84  
634              
635             # Go through each meta rule we haven't done yet
636 25         167 for (my $i = 0 ; $i <= $#metas ; $i++) {
637              
638             # If we depend on meta rules that haven't run yet, skip it
639 16 50       39 next if (grep( $metas{$_}, @{ $rule_deps{ $metas[$i] } }));
  16         50  
640              
641             # If we depend on network tests, call ensure_rules_are_complete()
642             # to block until they are
643 16 50       72 if (!defined $conf->{meta_dependencies}->{ $metas[$i] }) {
644 0         0 warn "no meta_dependencies defined for $metas[$i]";
645             }
646             my $alldeps = join ' ', grep {
647 40   50     198 ($tflags->{$_}||'') =~ /\bnet\b/
648 16         66 } split (' ', $conf->{meta_dependencies}->{ $metas[$i] } );
649              
650 16 50       65 if ($alldeps ne '') {
651 0         0 $self->add_evalstr($pms, '
652             $self->ensure_rules_are_complete(q{'.$metas[$i].'}, qw{'.$alldeps.'});
653             ');
654             }
655              
656             # Add this meta rule to the eval line
657             $self->add_evalstr($pms, '
658 16         113 $r = '.$meta{$metas[$i]}.';
659             if ($r) { $self->got_hit(q#'.$metas[$i].'#, "", ruletype => "meta", value => $r); }
660             ');
661              
662 16         106 splice @metas, $i--, 1; # remove this rule from our list
663             }
664             } while ($#metas != $count && $#metas > -1); # run until we can't go anymore
665              
666             # If there are any rules left, we can't solve the dependencies so complain
667 25         63 my %metas = map { $_ => 1 } @metas; # keep a small cache for fast lookups
  0         0  
668 25         98 foreach my $rulename_t (@metas) {
669 0         0 $pms->{rule_errors}++; # flag to --lint that there was an error ...
670             my $msg =
671             "rules: excluding meta test $rulename_t, unsolved meta dependencies: " .
672 0         0 join(", ", grep($metas{$_}, @{ $rule_deps{$rulename_t} }));
  0         0  
673 0 0       0 if ($self->{main}->{lint_rules}) {
674 0         0 warn $msg."\n";
675             }
676             else {
677 0         0 info($msg);
678             }
679             }
680             }
681 258         3569 );
682             }
683              
684             ###########################################################################
685              
686             sub do_head_tests {
687 258     258 0 632 my ($self, $pms, $priority) = @_;
688             # hash to hold the rules, "header\tdefault value" => rulename
689 258         507 my %ordered;
690             my %testcode; # tuples: [op_type, op, arg]
691             # op_type: 1=infix, 0:prefix/function
692             # op: operator, e.g. '=~', '!~', or a function like 'defined'
693             # arg: additional argument like a regexp for a patt matching op
694              
695             $self->run_generic_tests ($pms, $priority,
696             consttype => $Mail::SpamAssassin::Conf::TYPE_HEAD_TESTS,
697             type => 'head',
698             testhash => $pms->{conf}->{head_tests},
699             args => [ ],
700             loop_body => sub
701             {
702 72     72   241 my ($self, $pms, $conf, $rulename, $pat, %opts) = @_;
703 72         129 my ($op, $op_infix);
704 72         241 my $hdrname = $conf->{test_opt_header}->{$rulename};
705 72 100       190 if (exists $conf->{test_opt_exists}->{$rulename}) {
706 8         18 $op_infix = 0;
707 8 50       31 if (exists $conf->{test_opt_neg}->{$rulename}) {
708 0         0 $op = '!defined';
709             } else {
710 8         23 $op = 'defined';
711             }
712             }
713             else {
714 64         86 $op_infix = 1;
715 64 100       143 $op = $conf->{test_opt_neg}->{$rulename} ? '!~' : '=~';
716             }
717              
718 72         142 my $def = $conf->{test_opt_unset}->{$rulename};
719 72 100       100 push(@{ $ordered{$hdrname . (!defined $def ? '' : "\t".$def)} },
  72         298  
720             $rulename);
721              
722             return if ($opts{doing_user_rules} &&
723 72 50 33     173 !$self->is_user_rule_sub($rulename.'_head_test'));
724              
725 72         351 $testcode{$rulename} = [$op_infix, $op, $pat];
726             },
727             pre_loop_body => sub
728             {
729 25     25   126 my ($self, $pms, $conf, %opts) = @_;
730 25         71 $self->push_evalstr_prefix($pms, '
731             no warnings q(uninitialized);
732             my $hval;
733             ');
734             },
735             post_loop_body => sub
736             {
737 25     25   118 my ($self, $pms, $conf, %opts) = @_;
738             # setup the function to run the rules
739 25         138 while(my($k,$v) = each %ordered) {
740 48         149 my($hdrname, $def) = split(/\t/, $k, 2);
741 48 100       240 $self->push_evalstr_prefix($pms, '
742             $hval = $self->get(q{'.$hdrname.'}, ' .
743             (!defined($def) ? 'undef' : 'q{'.$def.'}') . ');
744             ');
745 48         75 foreach my $rulename (@{$v}) {
  48         92  
746 72         125 my $tc_ref = $testcode{$rulename};
747 72         106 my ($op_infix, $op, $pat);
748 72 50       222 ($op_infix, $op, $pat) = @$tc_ref if defined $tc_ref;
749              
750 72         114 my $posline = '';
751 72         85 my $ifwhile = 'if';
752 72         87 my $matchg = '';
753 72         101 my $whlast = '';
754              
755 72         94 my $matching_string_unavailable = 0;
756 72         95 my $expr;
757 72 100       136 if (!$op_infix) { # function or its negation
758 8         30 $expr = $op . '($hval)';
759 8         16 $matching_string_unavailable = 1;
760             }
761             else { # infix operator
762 64 50 50     294 if (($conf->{tflags}->{$rulename}||'') =~ /\bmultiple\b/) {
763 0         0 $posline = 'pos $hval = 0; $hits = 0;';
764 0         0 $ifwhile = 'while';
765 0         0 $matchg = 'g';
766 0 0       0 if ($conf->{tflags}->{$rulename} =~ /\bmaxhits=(\d+)\b/) {
767 0         0 $whlast = 'last if ++$hits >= '.untaint_var($1).';';
768             }
769             }
770 64 50       128 if ($matchg) {
771 0         0 $expr = '$hval '.$op.' /$qrptr->{q{'.$rulename.'}}/go';
772             } else {
773 64         159 $expr = '$hval '.$op.' /$qrptr->{q{'.$rulename.'}}/o';
774             }
775             }
776              
777 72         270 $self->add_evalstr($pms, '
778             if ($scoresptr->{q{'.$rulename.'}}) {
779             '.$posline.'
780             '.$self->hash_line_for_rule($pms, $rulename).'
781             '.$ifwhile.' ('.$expr.') {
782             $self->got_hit(q{'.$rulename.'}, "", ruletype => "header");
783             '.$self->hit_rule_plugin_code($pms, $rulename, "header", "",
784             $matching_string_unavailable).'
785             '.$whlast.'
786             }
787             '.$self->ran_rule_plugin_code($rulename, "header").'
788             }
789             ');
790             }
791 48         125 $self->pop_evalstr_prefix();
792             }
793             }
794 258         4080 );
795             }
796              
797             ###########################################################################
798              
799             sub do_body_tests {
800 258     258 0 633 my ($self, $pms, $priority, $textary) = @_;
801 258         390 my $loopid = 0;
802              
803             $self->run_generic_tests ($pms, $priority,
804             consttype => $Mail::SpamAssassin::Conf::TYPE_BODY_TESTS,
805             type => 'body',
806             testhash => $pms->{conf}->{body_tests},
807             args => [ @$textary ],
808             loop_body => sub
809             {
810 28     28   125 my ($self, $pms, $conf, $rulename, $pat, %opts) = @_;
811 28         60 my $sub = '';
812 28 50       98 if (would_log('dbg', 'rules-all') == 2) {
813 0         0 $sub .= '
814             dbg("rules-all: running body rule %s", q{'.$rulename.'});
815             ';
816             }
817 28   100     201 my $nosubject = ($conf->{tflags}->{$rulename}||'') =~ /\bnosubject\b/;
818 28 50       99 if ($nosubject) {
819 0         0 $sub .= '
820             my $nosubj = 1;
821             ';
822             }
823 28 50 66     180 if (($conf->{tflags}->{$rulename}||'') =~ /\bmultiple\b/)
824             {
825             # support multiple matches
826 0         0 $loopid++;
827 0   0     0 my ($max) = ($pms->{conf}->{tflags}->{$rulename}||'') =~ /\bmaxhits=(\d+)\b/;
828 0         0 $max = untaint_var($max);
829 0         0 $sub .= '
830             $hits = 0;
831             body_'.$loopid.': foreach my $l (@_) {
832             ';
833 0 0       0 if ($nosubject) {
834 0         0 $sub .= '
835             if ($nosubj) { $nosubj = 0; next; }
836             ';
837             }
838 0 0       0 $sub .= '
839             pos $l = 0;
840             '.$self->hash_line_for_rule($pms, $rulename).'
841             while ($l =~ /$qrptr->{q{'.$rulename.'}}/go) {
842             $self->got_hit(q{'.$rulename.'}, "BODY: ", ruletype => "body");
843             '. $self->hit_rule_plugin_code($pms, $rulename, "body", "") . '
844             '. ($max? 'last body_'.$loopid.' if ++$hits >= '.$max.';' : '') .'
845             }
846             }
847             ';
848             }
849             else {
850             # omitting the "pos" call, "body_loopid" label, use of while()
851             # instead of if() etc., shaves off 8 perl OPs.
852 28         64 $sub .= '
853             foreach my $l (@_) {
854             ';
855 28 50       60 if ($nosubject) {
856 0         0 $sub .= '
857             if ($nosubj) { $nosubj = 0; next; }
858             ';
859             }
860 28         93 $sub .= '
861             '.$self->hash_line_for_rule($pms, $rulename).'
862             if ($l =~ /$qrptr->{q{'.$rulename.'}}/o) {
863             $self->got_hit(q{'.$rulename.'}, "BODY: ", ruletype => "body");
864             '. $self->hit_rule_plugin_code($pms, $rulename, "body", "last") .'
865             }
866             }
867             ';
868             }
869              
870 28         131 $self->add_evalstr($pms, '
871             if ($scoresptr->{q{'.$rulename.'}}) {
872             '.$sub.'
873             '.$self->ran_rule_plugin_code($rulename, "body").'
874             }
875             ');
876              
877             return if ($opts{doing_user_rules} &&
878 28 50 33     151 !$self->is_user_rule_sub($rulename.'_body_test'));
879             }
880 258         2991 );
881             }
882              
883             ###########################################################################
884              
885             sub do_uri_tests {
886 258     258 0 662 my ($self, $pms, $priority, @uris) = @_;
887 258         428 my $loopid = 0;
888             $self->run_generic_tests ($pms, $priority,
889             consttype => $Mail::SpamAssassin::Conf::TYPE_URI_TESTS,
890             type => 'uri',
891             testhash => $pms->{conf}->{uri_tests},
892             args => [ @uris ],
893             loop_body => sub
894             {
895 8     8   46 my ($self, $pms, $conf, $rulename, $pat, %opts) = @_;
896 8         28 my $sub = '';
897 8 50       40 if (would_log('dbg', 'rules-all') == 2) {
898 0         0 $sub .= '
899             dbg("rules-all: running uri rule %s", q{'.$rulename.'});
900             ';
901             }
902 8 50 33     80 if (($conf->{tflags}->{$rulename}||'') =~ /\bmultiple\b/) {
903 0         0 $loopid++;
904 0   0     0 my ($max) = ($pms->{conf}->{tflags}->{$rulename}||'') =~ /\bmaxhits=(\d+)\b/;
905 0         0 $max = untaint_var($max);
906 0 0       0 $sub .= '
907             $hits = 0;
908             uri_'.$loopid.': foreach my $l (@_) {
909             pos $l = 0;
910             '.$self->hash_line_for_rule($pms, $rulename).'
911             while ($l =~ /$qrptr->{q{'.$rulename.'}}/go) {
912             $self->got_hit(q{'.$rulename.'}, "URI: ", ruletype => "uri");
913             '. $self->hit_rule_plugin_code($pms, $rulename, "uri", "") . '
914             '. ($max? 'last uri_'.$loopid.' if ++$hits >= '.$max.';' : '') .'
915             }
916             }
917             ';
918             } else {
919 8         40 $sub .= '
920             foreach my $l (@_) {
921             '.$self->hash_line_for_rule($pms, $rulename).'
922             if ($l =~ /$qrptr->{q{'.$rulename.'}}/o) {
923             $self->got_hit(q{'.$rulename.'}, "URI: ", ruletype => "uri");
924             '. $self->hit_rule_plugin_code($pms, $rulename, "uri", "last") .'
925             }
926             }
927             ';
928             }
929              
930 8         65 $self->add_evalstr($pms, '
931             if ($scoresptr->{q{'.$rulename.'}}) {
932             '.$sub.'
933             '.$self->ran_rule_plugin_code($rulename, "uri").'
934             }
935             ');
936              
937             return if ($opts{doing_user_rules} &&
938 8 50 33     67 !$self->is_user_rule_sub($rulename.'_uri_test'));
939             }
940 258         2275 );
941             }
942              
943             ###########################################################################
944              
945             sub do_rawbody_tests {
946 258     258 0 592 my ($self, $pms, $priority, $textary) = @_;
947 258         434 my $loopid = 0;
948             $self->run_generic_tests ($pms, $priority,
949             consttype => $Mail::SpamAssassin::Conf::TYPE_RAWBODY_TESTS,
950             type => 'rawbody',
951             testhash => $pms->{conf}->{rawbody_tests},
952             args => [ @$textary ],
953             loop_body => sub
954             {
955 0     0   0 my ($self, $pms, $conf, $rulename, $pat, %opts) = @_;
956 0         0 my $sub = '';
957 0 0       0 if (would_log('dbg', 'rules-all') == 2) {
958 0         0 $sub .= '
959             dbg("rules-all: running rawbody rule %s", q{'.$rulename.'});
960             ';
961             }
962 0 0 0     0 if (($pms->{conf}->{tflags}->{$rulename}||'') =~ /\bmultiple\b/)
963             {
964             # support multiple matches
965 0         0 $loopid++;
966 0   0     0 my ($max) = ($pms->{conf}->{tflags}->{$rulename}||'') =~ /\bmaxhits=(\d+)\b/;
967 0         0 $max = untaint_var($max);
968 0 0       0 $sub .= '
969             $hits = 0;
970             rawbody_'.$loopid.': foreach my $l (@_) {
971             pos $l = 0;
972             '.$self->hash_line_for_rule($pms, $rulename).'
973             while ($l =~ /$qrptr->{q{'.$rulename.'}}/go) {
974             $self->got_hit(q{'.$rulename.'}, "RAW: ", ruletype => "rawbody");
975             '. $self->hit_rule_plugin_code($pms, $rulename, "rawbody", "") . '
976             '. ($max? 'last rawbody_'.$loopid.' if ++$hits >= '.$max.';' : '') .'
977             }
978             }
979             ';
980             }
981             else {
982 0         0 $sub .= '
983             foreach my $l (@_) {
984             '.$self->hash_line_for_rule($pms, $rulename).'
985             if ($l =~ /$qrptr->{q{'.$rulename.'}}/o) {
986             $self->got_hit(q{'.$rulename.'}, "RAW: ", ruletype => "rawbody");
987             '. $self->hit_rule_plugin_code($pms, $rulename, "rawbody", "last") . '
988             }
989             }
990             ';
991             }
992              
993 0         0 $self->add_evalstr($pms, '
994             if ($scoresptr->{q{'.$rulename.'}}) {
995             '.$sub.'
996             '.$self->ran_rule_plugin_code($rulename, "rawbody").'
997             }
998             ');
999              
1000             return if ($opts{doing_user_rules} &&
1001 0 0 0     0 !$self->is_user_rule_sub($rulename.'_rawbody_test'));
1002             }
1003 258         2698 );
1004             }
1005              
1006             ###########################################################################
1007              
1008             sub do_full_tests {
1009 258     258 0 633 my ($self, $pms, $priority, $fullmsgref) = @_;
1010 258         454 my $loopid = 0;
1011             $self->run_generic_tests ($pms, $priority,
1012             consttype => $Mail::SpamAssassin::Conf::TYPE_FULL_TESTS,
1013             type => 'full',
1014             testhash => $pms->{conf}->{full_tests},
1015             args => [ $fullmsgref ],
1016             pre_loop_body => sub
1017             {
1018 25     25   121 my ($self, $pms, $conf, %opts) = @_;
1019 25         82 $self->push_evalstr_prefix($pms, '
1020             my $fullmsgref = shift;
1021             ');
1022             },
1023             loop_body => sub
1024             {
1025 0     0   0 my ($self, $pms, $conf, $rulename, $pat, %opts) = @_;
1026 0   0     0 my ($max) = ($pms->{conf}->{tflags}->{$rulename}||'') =~ /\bmaxhits=(\d+)\b/;
1027 0         0 $max = untaint_var($max);
1028 0   0     0 $max ||= 0;
1029 0         0 $self->add_evalstr($pms, '
1030             if ($scoresptr->{q{'.$rulename.'}}) {
1031             pos $$fullmsgref = 0;
1032             '.$self->hash_line_for_rule($pms, $rulename).'
1033             dbg("rules-all: running full rule %s", q{'.$rulename.'});
1034             $hits = 0;
1035             while ($$fullmsgref =~ /$qrptr->{q{'.$rulename.'}}/g) {
1036             $self->got_hit(q{'.$rulename.'}, "FULL: ", ruletype => "full");
1037             '. $self->hit_rule_plugin_code($pms, $rulename, "full", "last") . '
1038             last if ++$hits >= '.$max.';
1039             }
1040             pos $$fullmsgref = 0;
1041             '.$self->ran_rule_plugin_code($rulename, "full").'
1042             }
1043             ');
1044             }
1045 258         2444 );
1046             }
1047              
1048             ###########################################################################
1049              
1050             sub do_head_eval_tests {
1051 258     258 0 604 my ($self, $pms, $priority) = @_;
1052 258 100       1056 return unless (defined($pms->{conf}->{head_evals}->{$priority}));
1053 162         1077 dbg("rules: running head_eval tests; score so far=".$pms->{score});
1054 162         741 $self->run_eval_tests ($pms, $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS,
1055             'head_evals', '', $priority);
1056             }
1057              
1058             sub do_body_eval_tests {
1059 258     258 0 746 my ($self, $pms, $priority, $bodystring) = @_;
1060 258 100       1016 return unless (defined($pms->{conf}->{body_evals}->{$priority}));
1061 81         562 dbg("rules: running body_eval tests; score so far=".$pms->{score});
1062 81         366 $self->run_eval_tests ($pms, $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS,
1063             'body_evals', 'BODY: ', $priority, $bodystring);
1064             }
1065              
1066             sub do_rawbody_eval_tests {
1067 258     258 0 658 my ($self, $pms, $priority, $bodystring) = @_;
1068 258 50       959 return unless (defined($pms->{conf}->{rawbody_evals}->{$priority}));
1069 0         0 dbg("rules: running rawbody_eval tests; score so far=".$pms->{score});
1070 0         0 $self->run_eval_tests ($pms, $Mail::SpamAssassin::Conf::TYPE_RAWBODY_EVALS,
1071             'rawbody_evals', 'RAW: ', $priority, $bodystring);
1072             }
1073              
1074             sub do_full_eval_tests {
1075 258     258 0 981 my ($self, $pms, $priority, $fullmsgref) = @_;
1076 258 100       1090 return unless (defined($pms->{conf}->{full_evals}->{$priority}));
1077 81         549 dbg("rules: running full_eval tests; score so far=".$pms->{score});
1078 81         405 $self->run_eval_tests($pms, $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS,
1079             'full_evals', '', $priority, $fullmsgref);
1080             }
1081              
1082             sub run_eval_tests {
1083 324     324 0 960 my ($self, $pms, $testtype, $evalname, $prepend2desc, $priority, @extraevalargs) = @_;
1084            
1085 324         665 my $master_deadline = $pms->{master_deadline};
1086 324 50 33     2950 if ($pms->{deadline_exceeded}) {
    50          
    50          
1087 0         0 return;
1088             } elsif ($master_deadline && time > $master_deadline) {
1089 0         0 info("check: (run_eval) exceeded time limit, skipping further tests");
1090 0         0 $pms->{deadline_exceeded} = 1;
1091 0         0 return;
1092             } elsif ($self->{main}->call_plugins("have_shortcircuited",
1093             { permsgstatus => $pms })) {
1094 0         0 return;
1095             }
1096              
1097 324         911 my $conf = $pms->{conf};
1098 324         735 my $doing_user_rules = $conf->{want_rebuild_for_type}->{$testtype};
1099 324 50       768 if ($doing_user_rules) { $self->{done_user_rules}->{$testtype}++; }
  0         0  
1100              
1101             # clean up priority value so it can be used in a subroutine name
1102 324         464 my $clean_priority;
1103 324         827 ($clean_priority = $priority) =~ s/-/neg/;
1104 324         1106 my $scoreset = $conf->get_score_set();
1105 324         605 my $package_name = __PACKAGE__;
1106              
1107 324         1154 my $methodname = '_eval_tests'.
1108             '_type'.$testtype .
1109             '_pri'.$clean_priority .
1110             '_set'.$scoreset;
1111              
1112             # Some of the rules are scoreset specific, so we need additional
1113             # subroutines to handle those
1114 324 100 66     503 if (defined &{"${package_name}::${methodname}"}
  324         2703  
1115             && !$doing_user_rules)
1116             {
1117 126         309 my $method = "${package_name}::${methodname}";
1118             #dbg("rules: run_eval_tests - calling previously compiled %s", $method);
1119 126         530 my $t = Mail::SpamAssassin::Timeout->new({ deadline => $master_deadline });
1120             my $err = $t->run(sub {
1121 22     22   219 no strict "refs";
  22         58  
  22         12402  
1122 126     126   280 &{$method}($pms,@extraevalargs);
  126         3726  
1123 126         827 });
1124 126 0 33     653 if ($t->timed_out() && $master_deadline && time > $master_deadline) {
      33        
1125 0         0 info("check: exceeded time limit in $method, skipping further tests");
1126 0         0 $pms->{deadline_exceeded} = 1;
1127             }
1128 126         726 return;
1129             }
1130              
1131             # look these up once in advance to save repeated lookups in loop below
1132 198         543 my $evalhash = $conf->{$evalname}->{$priority};
1133 198         409 my $tflagsref = $conf->{tflags};
1134 198         340 my $scoresref = $conf->{scores};
1135 198         360 my $eval_pluginsref = $conf->{eval_plugins};
1136 198         665 my $have_start_rules = $self->{main}->have_plugin("start_rules");
1137 198         600 my $have_ran_rule = $self->{main}->have_plugin("ran_rule");
1138              
1139             # the buffer for the evaluated code
1140 198         456 my $evalstr = '';
1141              
1142             # conditionally include the dbg in the eval str
1143 198         331 my $dbgstr = '';
1144 198 50       618 if (would_log('dbg')) {
1145 0         0 $dbgstr = 'dbg("rules: ran eval rule $rulename ======> got hit ($result)");';
1146             }
1147              
1148 198         445 while (my ($rulename, $test) = each %{$evalhash}) {
  2018         6294  
1149 1820 100       5100 if ($tflagsref->{$rulename}) {
1150             # If the rule is a net rule, and we are in a non-net scoreset, skip it.
1151 1297 100       4555 if ($tflagsref->{$rulename} =~ /\bnet\b/) {
1152 400 100       1321 next if (($scoreset & 1) == 0);
1153             }
1154             # If the rule is a bayes rule, and we are in a non-bayes scoreset, skip it.
1155 907 100       3017 if ($tflagsref->{$rulename} =~ /\blearn\b/) {
1156 78 50       443 next if (($scoreset & 2) == 0);
1157             }
1158             }
1159              
1160             # skip if score zeroed
1161 1352 50       3924 next if !$scoresref->{$rulename};
1162            
1163 1352         3346 my $function = untaint_var($test->[0]); # was validated with \w+
1164 1352 50       2811 if (!$function) {
1165 0         0 warn "rules: error: no eval function defined for $rulename";
1166 0         0 next;
1167             }
1168              
1169 1352 50       3580 if (!exists $conf->{eval_plugins}->{$function}) {
1170 0         0 warn("rules: error: unknown eval '$function' for $rulename\n");
1171 0         0 next;
1172             }
1173              
1174 1352         2603 $evalstr .= '
1175             {
1176             $rulename = q#'.$rulename.'#;
1177             %{$self->{test_log_msgs}} = ();
1178             ';
1179            
1180             # only need to set current_rule_name for plugin evals
1181 1352 50       2842 if ($eval_pluginsref->{$function}) {
1182             # let plugins get the name of the rule that is currently being run,
1183             # and ensure their eval functions exist
1184 1352         2424 $evalstr .= '
1185             $self->{current_rule_name} = $rulename;
1186             $self->register_plugin_eval_glue(q#'.$function.'#);
1187             ';
1188             }
1189              
1190             # this stuff is quite slow, and totally superfluous if
1191             # no plugin is loaded for those hooks
1192 1352 50       2229 if ($have_start_rules) {
1193             # XXX - should we use helper function here?
1194 0         0 $evalstr .= '
1195             $self->{main}->call_plugins("start_rules", {
1196             permsgstatus => $self,
1197             ruletype => "eval",
1198             priority => '.$priority.'
1199             });
1200              
1201             ';
1202             }
1203              
1204 1352         3086 $evalstr .= '
1205             eval {
1206             $result = $self->'.$function.'(@extraevalargs, @{$testptr->{q#'.$rulename.'#}->[1]}); 1;
1207             } or do {
1208             $result = 0;
1209             die "rules: $@\n" if $@ =~ /__alarm__ignore__/;
1210             $self->handle_eval_rule_errors($rulename);
1211             };
1212             ';
1213              
1214 1352 50       2237 if ($have_ran_rule) {
1215             # XXX - should we use helper function here?
1216 0         0 $evalstr .= '
1217             $self->{main}->call_plugins("ran_rule", {
1218             permsgstatus => $self, ruletype => "eval", rulename => $rulename
1219             });
1220             ';
1221             }
1222              
1223 1352         3404 $evalstr .= '
1224             if ($result) {
1225             $self->got_hit($rulename, $prepend2desc, ruletype => "eval", value => $result);
1226             '.$dbgstr.'
1227             }
1228             }
1229             ';
1230             }
1231              
1232             # don't free the eval ruleset here -- we need it in the compiled code!
1233              
1234             # nothing done in the loop, that means no rules
1235 198 100       707 return unless ($evalstr);
1236            
1237 121         990 $evalstr = <<"EOT";
1238             {
1239             package $package_name;
1240              
1241             sub ${methodname} {
1242             my (\$self, \@extraevalargs) = \@_;
1243              
1244             my \$testptr = \$self->{conf}->{$evalname}->{$priority};
1245             my \$prepend2desc = q#$prepend2desc#;
1246             my \$rulename;
1247             my \$result;
1248             $evalstr
1249             }
1250              
1251             1;
1252             }
1253             EOT
1254              
1255 121         205 undef &{$methodname};
  121         477  
1256              
1257 121         497 dbg("rules: run_eval_tests - compiling eval code: %s, priority %s",
1258             $testtype, $priority);
1259             # dbg("rules: eval code(3): %s", $evalstr);
1260 121         185 my $eval_result;
1261 121         168 { my $timer = $self->{main}->time_method('compile_eval');
  121         608  
1262 121 0   16   143044 $eval_result = eval($evalstr);
  25 50   25   89  
  25 100   12   102  
  25 50       127  
  12 50       25  
  12 100       22  
  12 50       19  
  12 50       24  
  30 100       75  
  30 0       54  
  30 50       115  
  12 50       31  
  30 0       91  
  25 50       245  
  25 50       68  
  13 0       25  
  1 50       2  
  12 50       17  
  25 0       60  
  13 50       29  
  12 50       21  
  25 0       45  
  25 50       55  
  12 50       27  
  25 0       41  
  25 50       91  
  16 50       39  
  25 0       86  
  25 50       245  
  25 50       146  
  0 0       0  
  18 50       43  
  18 100       64  
  30 0       103  
  18 50       67  
  12 50       21  
  12 0       27  
  12 50       20  
  12 50       26  
  12 0       26  
  12 50       56  
  12 50       27  
  12 0       36  
  12 50       212  
  12 50       60  
  0 0       0  
  0 50       0  
  0 50       0  
  12 0       204  
  0 50       0  
  12 50       48  
  12 0       46  
  12 50       26  
  12 50       27  
  12 0       24  
  12 50       25  
  12 50       25  
  12 0       33  
  12 50       34  
  12 100       64  
  12 0       30  
  12 50       25  
  12 50       232  
  12 0       58  
  0 50       0  
  0 50       0  
  0 0       0  
  12 50       39  
  0 50       0  
  12 0       23  
  12 50       24  
  12 50       26  
  12 0       89  
  12 50       31  
  12 50       52  
  12 0       26  
  12 50       51  
  12 50       220  
  12 0       78  
  0 50       0  
  0 50       0  
  0 0       0  
  12 50       54  
  0 50       0  
  12 0       22  
  12 50       24  
  12 50       27  
  12 0       31  
  12 50       29  
  12 50       51  
  12 0       40  
  12 50       30  
  12 50       204  
  12 0       64  
  0 50       0  
  0 50       0  
  0 0       0  
  12 50       50  
  0 50       0  
  12         26  
  12         23  
  12         28  
  12         30  
  12         46  
  12         62  
  12         40  
  12         28  
  12         230  
  12         69  
  0         0  
  0         0  
  0         0  
  12         44  
  0         0  
  12         21  
  12         39  
  12         20  
  12         27  
  12         28  
  12         45  
  12         31  
  12         23  
  12         208  
  12         54  
  0         0  
  0         0  
  0         0  
  12         51  
  0         0  
  12         27  
  12         32  
  12         25  
  12         40  
  12         25  
  12         45  
  12         29  
  12         25  
  12         205  
  12         51  
  0         0  
  0         0  
  0         0  
  12         44  
  0         0  
  12         26  
  12         24  
  12         19  
  12         28  
  12         25  
  12         45  
  12         30  
  12         27  
  12         202  
  12         57  
  0         0  
  0         0  
  0         0  
  12         43  
  0         0  
  12         21  
  12         24  
  12         30  
  12         28  
  12         26  
  12         44  
  12         28  
  12         69  
  12         264  
  12         67  
  0         0  
  0         0  
  0         0  
  12         41  
  5         22  
  12         30  
  12         28  
  12         21  
  12         29  
  12         25  
  12         49  
  12         31  
  12         26  
  12         276  
  12         48  
  0         0  
  0         0  
  0         0  
  12         48  
  0         0  
  12         20  
  12         29  
  12         22  
  12         28  
  12         24  
  12         74  
  12         30  
  12         58  
  12         218  
  12         54  
  0         0  
  0         0  
  0         0  
  12         44  
  0         0  
  12         20  
  12         22  
  12         19  
  12         44  
  12         28  
  12         43  
  12         44  
  12         25  
  12         216  
  12         53  
  0         0  
  0         0  
  0         0  
  12         51  
  0         0  
  12         22  
  12         27  
  12         17  
  12         29  
  12         95  
  12         59  
  12         30  
  12         27  
  12         205  
  12         51  
  0         0  
  0         0  
  0         0  
  12         42  
  0         0  
  12         20  
  12         34  
  12         23  
  12         27  
  12         27  
  12         58  
  12         32  
  12         25  
  12         213  
  12         45  
  0         0  
  0         0  
  0         0  
  12         43  
  0         0  
  12         21  
  12         24  
  12         25  
  12         24  
  12         30  
  12         45  
  12         28  
  12         30  
  12         223  
  12         60  
  0         0  
  0         0  
  0         0  
  12         94  
  0         0  
  12         24  
  12         37  
  12         19  
  12         28  
  12         28  
  12         40  
  12         27  
  12         36  
  12         244  
  12         72  
  0         0  
  0         0  
  0         0  
  12         40  
  0         0  
  12         23  
  12         31  
  12         22  
  12         28  
  12         32  
  12         47  
  12         30  
  12         28  
  12         222  
  12         54  
  0         0  
  0         0  
  0         0  
  12         48  
  0         0  
  12         23  
  12         21  
  12         21  
  12         26  
  12         27  
  12         44  
  12         34  
  12         30  
  12         218  
  12         68  
  0         0  
  0         0  
  0         0  
  12         47  
  1         5  
  12         30  
  12         23  
  12         24  
  12         34  
  12         30  
  12         45  
  12         28  
  12         24  
  12         224  
  12         75  
  0         0  
  0         0  
  0         0  
  12         40  
  0         0  
  12         22  
  12         24  
  12         21  
  12         30  
  12         28  
  12         56  
  12         36  
  12         33  
  12         215  
  12         50  
  0         0  
  0         0  
  0         0  
  12         160  
  0         0  
  12         24  
  12         22  
  12         23  
  12         28  
  12         33  
  12         43  
  12         33  
  12         36  
  12         230  
  12         57  
  0         0  
  0         0  
  0         0  
  12         40  
  0         0  
  12         20  
  12         22  
  12         22  
  12         25  
  12         36  
  12         42  
  12         37  
  12         38  
  12         219  
  12         74  
  0         0  
  0         0  
  0         0  
  12         39  
  0         0  
  12         31  
  12         26  
  12         19  
  12         24  
  12         28  
  12         44  
  12         41  
  12         23  
  12         217  
  12         61  
  0         0  
  0         0  
  0         0  
  12         53  
  0         0  
  12         32  
  12         27  
  12         23  
  12         43  
  12         29  
  12         44  
  12         40  
  12         28  
  12         202  
  12         51  
  0         0  
  0         0  
  0         0  
  12         54  
  0         0  
  12         35  
  12         26  
  12         22  
  12         28  
  12         51  
  12         43  
  12         31  
  12         21  
  12         216  
  12         49  
  0         0  
  0         0  
  0         0  
  12         42  
  0         0  
  12         21  
  12         24  
  12         33  
  12         33  
  12         26  
  12         47  
  12         31  
  12         29  
  12         235  
  12         49  
  0         0  
  0         0  
  0         0  
  12         38  
  0         0  
  12         26  
  12         23  
  12         24  
  12         32  
  12         30  
  12         64  
  12         38  
  12         25  
  12         214  
  12         44  
  0         0  
  0         0  
  0         0  
  12         36  
  0         0  
  12         24  
  12         22  
  12         24  
  12         33  
  12         21  
  12         42  
  12         29  
  12         26  
  12         208  
  12         67  
  0         0  
  0         0  
  0         0  
  12         52  
  0         0  
  12         21  
  12         23  
  12         43  
  12         27  
  12         48  
  12         43  
  12         29  
  12         32  
  12         223  
  12         57  
  0         0  
  0         0  
  0         0  
  12         43  
  0         0  
  12         28  
  12         21  
  12         23  
  12         28  
  12         23  
  12         48  
  12         27  
  12         34  
  12         211  
  12         46  
  0         0  
  0         0  
  0         0  
  12         239  
  0         0  
  12         45  
  12         53  
  12         28  
  12         23  
  12         22  
  12         22  
  12         27  
  12         33  
  12         34  
  12         58  
  12         35  
  12         37  
  12         213  
  12         64  
  0         0  
  0         0  
  0         0  
  12         218  
  0         0  
1263             }
1264 121 50       591 if (!$eval_result) {
1265 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
1266 0         0 warn "rules: failed to compile eval tests, skipping some: $eval_stat\n";
1267 0         0 $self->{rule_errors}++;
1268             }
1269             else {
1270 121         389 my $method = "${package_name}::${methodname}";
1271 121         384 push (@TEMPORARY_METHODS, $methodname);
1272             # dbg("rules: run_eval_tests - calling the just compiled %s", $method);
1273 121         709 my $t = Mail::SpamAssassin::Timeout->new({ deadline => $master_deadline });
1274             my $err = $t->run(sub {
1275 22     22   189 no strict "refs";
  22         49  
  22         15341  
1276 121     135   284 &{$method}($pms,@extraevalargs);
  121         3448  
1277 121         914 });
1278 121 0 33     663 if ($t->timed_out() && $master_deadline && time > $master_deadline) {
      33        
1279 0         0 info("check: exceeded time limit in $method, skipping further tests");
1280 0         0 $pms->{deadline_exceeded} = 1;
1281             }
1282             }
1283             }
1284              
1285             ###########################################################################
1286             # Helper Functions
1287              
1288             sub hash_line_for_rule {
1289 95     108 0 174 my ($self, $pms, $rulename) = @_;
1290             # using tainted subr. argument may taint the whole expression, avoid
1291 95         368 my $u = untaint_var($pms->{conf}->{source_file}->{$rulename});
1292 95         617 return sprintf("\n#line 1 \"%s, rule %s,\"", $u, $rulename);
1293             # return sprintf("\n#line 1 \"%s, rule %s,\"", $u, $rulename) .
1294             # "\ndbg(\"rules: will run %s\", q(".$rulename."));\n";
1295             }
1296              
1297             sub is_user_rule_sub {
1298 0     13 0 0 my ($self, $subname) = @_;
1299 0         0 my $package_name = __PACKAGE__;
1300 0 0       0 return 0 if (eval 'defined &'.$package_name.'::'.$subname);
1301 0         0 1;
1302             }
1303              
1304             sub start_rules_plugin_code {
1305 132     150 0 251 my ($self, $ruletype, $pri) = @_;
1306              
1307 132         193 my $evalstr = '';
1308 132 50       415 if ($self->{main}->have_plugin("start_rules")) {
1309 0         0 $evalstr .= '
1310              
1311             $self->{main}->call_plugins ("start_rules", { permsgstatus => $self,
1312             ruletype => \''.$ruletype.'\',
1313             priority => '.$pri.' });
1314              
1315             ';
1316             }
1317              
1318 132         434 return $evalstr;
1319             }
1320              
1321             sub hit_rule_plugin_code {
1322 95     95 0 232 my ($self, $pms, $rulename, $ruletype, $loop_break_directive,
1323             $matching_string_unavailable) = @_;
1324              
1325             # note: keep this in 'single quotes' to avoid the $ & performance hit,
1326             # unless specifically requested by the caller. Also split the
1327             # two chars, just to be paranoid and ensure that a buggy perl interp
1328             # doesn't impose that hit anyway (just in case)
1329 95         118 my $match;
1330 95 100       164 if ($matching_string_unavailable) {
1331 7         26 $match = '"<YES>"'; # nothing better to report, $& is not set by this rule
1332             } else {
1333             # simple, but suffers from 'user data interpreted as a boolean', Bug 6360
1334 88         131 $match = '(defined $'.'& ? $'.'& : "negative match")';
1335             }
1336              
1337 95         122 my $debug_code = '';
1338 95 50       222 if (exists($pms->{should_log_rule_hits})) {
1339 0         0 $debug_code = '
1340             dbg("rules: ran '.$ruletype.' rule '.$rulename.' ======> got hit: \"" . '.
1341             $match.' . "\"");
1342             ';
1343             }
1344              
1345 95         141 my $save_hits_code = '';
1346 95 50       201 if ($pms->{save_pattern_hits}) {
1347 0         0 $save_hits_code = '
1348             $self->{pattern_hits}->{q{'.$rulename.'}} = '.$match.';
1349             ';
1350             }
1351              
1352             # if we're not running "tflags multiple", break out of the matching
1353             # loop this way
1354 95         129 my $multiple_code = '';
1355 95 100 100     356 if ($loop_break_directive &&
      66        
1356             ($pms->{conf}->{tflags}->{$rulename}||'') !~ /\bmultiple\b/) {
1357 32         73 $multiple_code = $loop_break_directive.';';
1358             }
1359              
1360 95         431 return $debug_code.$save_hits_code.$multiple_code;
1361             }
1362              
1363             sub ran_rule_plugin_code {
1364 95     95 0 195 my ($self, $rulename, $ruletype) = @_;
1365              
1366 95 50       276 return '' unless $self->{main}->have_plugin("ran_rule");
1367              
1368             # The $self here looks odd, but since we are inserting this into eval'd code it
1369             # needs to be $self which in that case is actually the PerMsgStatus object
1370 0         0 return '
1371             $self->{main}->call_plugins ("ran_rule", { permsgstatus => $self, rulename => \''.$rulename.'\', ruletype => \''.$ruletype.'\' });
1372             ';
1373             }
1374              
1375             sub free_ruleset_source {
1376 132     132 0 299 my ($self, $pms, $type, $pri) = @_;
1377              
1378             # we can't do this, if we may need to recompile them again later
1379 132 50       466 return if $pms->{conf}->{allow_user_rules};
1380              
1381             # remove now-compiled rulesets
1382 132 50       430 if (exists $pms->{conf}->{$type.'_tests'}->{$pri}) {
1383 132         512 delete $pms->{conf}->{$type.'_tests'}->{$pri};
1384             }
1385             }
1386              
1387             ###########################################################################
1388              
1389             1;