File Coverage

blib/lib/Mail/SpamAssassin/Plugin/Check.pm
Criterion Covered Total %
statement 980 1195 82.0
branch 232 530 43.7
condition 46 113 40.7
subroutine 95 100 95.0
pod 3 31 9.6
total 1356 1969 68.8


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