=head1 NAME Mail::SpamAssassin::Plugin::Reuse - For reusing old rule hits during a mass-check =head1 SYNOPSIS loadplugin Mail::SpamAssassin::Plugin::Reuse ifplugin Mail::SpamAssassin::Plugin::Reuse reuse NETWORK_RULE [ NETWORK_RULE_OLD_NAME ] endif =head1 DESCRIPTION The purpose of this plugin is to work in conjunction with B to map rules hit in input messages to rule hits in the mass-check output. =cut package Mail::SpamAssassin::Plugin::Reuse; # use bytes; use strict; use warnings; use Mail::SpamAssassin::Conf; use Mail::SpamAssassin::Logger; our @ISA = qw(Mail::SpamAssassin::Plugin); # constructor sub new { my $invocant = shift; my $samain = shift; # some boilerplate... my $class = ref($invocant) || $invocant; my $self = $class->SUPER::new($samain); bless ($self, $class); $self->set_config($samain->{conf}); # make sure we run last (or close) of the finish_parsing_start since # we need all other rules to be defined $self->register_method_priority("finish_parsing_start", 100); return $self; } sub set_config { my ($self, $conf) = @_; my @cmds; # reuse CURRENT_NAME ADDITIONAL_NAMES_IN_INPUT ... # e.g. # reuse NET_TEST_V1 NET_TEST_V0 push (@cmds, { setting => 'reuse', code => sub { my ($conf, $key, $value, $line) = @_; if ($value !~ /\s*(\w+)(?:\s+(\w+(?:\s+\w+)*))?\s*$/) { return $Mail::SpamAssassin::Conf::INVALID_VALUE; } my $new_name = $1; my @old_names = ($new_name); if ($2) { push @old_names, split (' ', $2); } dbg("reuse: read rule, old: @old_names new: $new_name"); foreach my $old (@old_names) { push @{$conf->{reuse_tests}->{$new_name}}, $old; } }}); $conf->{parser}->register_commands(\@cmds); } sub finish_parsing_start { my ($self, $opts) = @_; my $conf = $opts->{conf}; dbg("reuse: finish_parsing_start called"); return 0 if (!exists $conf->{reuse_tests}); foreach my $rule_name (keys %{$conf->{reuse_tests}}) { # If the rule does not exist, add a new EMPTY test, set default score if (!exists $conf->{tests}->{$rule_name}) { dbg("reuse: $rule_name does not exist, adding empty test"); $conf->{parser}->add_test($rule_name, undef, $Mail::SpamAssassin::Conf::TYPE_EMPTY_TESTS); } if (!exists $conf->{scores}->{$rule_name}) { my $set_score = ($rule_name =~/^T_/) ? 0.01 : 1.0; $set_score = -$set_score if ( ($conf->{tflags}->{$rule_name}||'') =~ /\bnice\b/ ); foreach my $ss (0..3) { $conf->{scoreset}->[$ss]->{$rule_name} = $set_score; } } # Figure out when to add any hits -- grab priority and "stage" my $priority = $conf->{priority}->{$rule_name} || 0; my $stage = $self->_get_stage_from_rule($opts->{conf}, $rule_name); $conf->{reuse_tests_order}->{$rule_name} = [ $priority, $stage ]; } } sub check_start { my ($self, $opts) = @_; my $pms = $opts->{permsgstatus}; # Can we reuse? my $msg = $pms->get_message(); unless (exists $msg->{metadata}->{reuse_tests_hit}) { dbg("reuse: no old test hits passed in"); return 0; } my $old_hash = $msg->{metadata}->{reuse_tests_hit}; # now go through the rules and priorities and figure out which ones # need to be disabled foreach my $rule (keys %{$pms->{conf}->{reuse_tests}}) { dbg("reuse: looking at rule $rule"); my ($priority, $stage) = @{$pms->{conf}->{reuse_tests_order}->{$rule}}; # score set could change after check_start but before we add hits, # so we need to disable the rule in all sets foreach my $ss (0..3) { if (exists $pms->{conf}->{scoreset}->[$ss]->{$rule}) { dbg("reuse: disabling rule $rule in score set $ss"); $pms->{reuse_old_scores}->{$rule}->[$ss] = $pms->{conf}->{scoreset}->[$ss]->{$rule}; $pms->{conf}->{scoreset}->[$ss]->{$rule} = 0; } } # now, check for hits OLD: foreach my $old_test (@{$pms->{conf}->{reuse_tests}->{$rule}}) { dbg("reuse: looking for rule $old_test"); if ($old_hash->{$old_test}) { push @{$pms->{reuse_hits_to_add}->{"$priority $stage"}}, $rule; dbg("reuse: rule $rule hit, will add at priority $priority, stage " . "$stage"); last OLD; } } } } sub check_end { my ($self, $opts) = @_; my $pms = $opts->{permsgstatus}; foreach my $disabled_rule (keys %{$pms->{reuse_old_scores}}) { foreach my $ss (0..3) { next unless exists $pms->{conf}->{scoreset}->[$ss]->{$disabled_rule}; $pms->{conf}->{scoreset}->[$ss]->{$disabled_rule} = $pms->{reuse_old_scores}->{$disabled_rule}->[$ss]; } } delete $pms->{reuse_old_scores}; } sub start_rules { my ($self, $opts) = @_; return $self->_add_hits($opts->{permsgstatus}, $opts->{priority}, $opts->{ruletype}); } sub _add_hits { my ($self, $pms, $priority, $stage) = @_; return unless exists $pms->{reuse_hits_to_add}->{"$priority $stage"}; return if exists $pms->{reuse_hits_done}->{"$priority $stage"}; foreach my $rule (@{$pms->{reuse_hits_to_add}->{"$priority $stage"}}) { # Add hit even if rule was originally disabled my $ss = $pms->{conf}->get_score_set(); $pms->{conf}->{scores}->{$rule} = $pms->{reuse_old_scores}->{$rule}->[$ss] || 0.001; dbg("reuse: registering hit for $rule: score: " . $pms->{conf}->{scores}->{$rule}); $pms->got_hit($rule); $pms->{conf}->{scores}->{$rule} = 0; } $pms->{reuse_hits_done}->{"$priority $stage"} = 1; } my %type_to_stage = ( $Mail::SpamAssassin::Conf::TYPE_HEAD_TESTS => "head", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS => "eval", $Mail::SpamAssassin::Conf::TYPE_BODY_TESTS => "body", $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS => "eval", $Mail::SpamAssassin::Conf::TYPE_FULL_TESTS => "full", $Mail::SpamAssassin::Conf::TYPE_FULL_EVALS => "eval", $Mail::SpamAssassin::Conf::TYPE_RAWBODY_TESTS => "rawbody", $Mail::SpamAssassin::Conf::TYPE_RAWBODY_EVALS => "eval", $Mail::SpamAssassin::Conf::TYPE_URI_TESTS => "uri", $Mail::SpamAssassin::Conf::TYPE_URI_EVALS => "eval", $Mail::SpamAssassin::Conf::TYPE_META_TESTS => "meta", $Mail::SpamAssassin::Conf::TYPE_RBL_EVALS => "eval", ); sub _get_stage_from_rule { my ($self, $conf, $rule) = @_; my $type = $conf->{test_types}->{$rule}; if ($type && $type == $Mail::SpamAssassin::Conf::TYPE_EMPTY_TESTS) { # this is a "fake" rule... see if the rule "text"/"definition" is # the name of the "parent" rule" my $parent = $conf->{tests}->{$rule}; if ($parent) { $type = $conf->{test_types}->{$parent}; } } if ($type && exists $type_to_stage{$type}) { return $type_to_stage{$type}; } else { # Run before the meta rules run so that they can use these hits as # inputs. return "meta"; } }