# <@LICENSE> # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to you under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at: # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # =head1 NAME Mail::SpamAssassin::Plugin::Shortcircuit - short-circuit evaluation for certain rules =head1 SYNOPSIS loadplugin Mail::SpamAssassin::Plugin::Shortcircuit report Content analysis details: (_SCORE_ points, _REQD_ required, s/c _SCTYPE_) add_header all Status "_YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ shortcircuit=_SCTYPE_ autolearn=_AUTOLEARN_ version=_VERSION_" =head1 DESCRIPTION This plugin implements simple, test-based shortcircuiting. Shortcircuiting a test will force all other pending rules to be skipped, if that test is hit. Recomended usage is to use C to set rules with strong S/O values (ie. 1.0) to be run first, and make instant spam or ham classification based on that. =cut package Mail::SpamAssassin::Plugin::Shortcircuit; use Mail::SpamAssassin::Plugin; use Mail::SpamAssassin::Logger; use strict; use warnings; use bytes; use vars qw(@ISA); @ISA = qw(Mail::SpamAssassin::Plugin); sub new { my $class = shift; my $mailsaobject = shift; $class = ref($class) || $class; my $self = $class->SUPER::new($mailsaobject); bless ($self, $class); $self->set_config($mailsaobject->{conf}); return $self; } sub set_config { my($self, $conf) = @_; my @cmds = (); =head1 CONFIGURATION SETTINGS The following configuration settings are used to control shortcircuiting: =item shortcircuit SYMBOLIC_TEST_NAME {ham|spam|on|off} Shortcircuiting a test will force all other pending rules to be skipped, if that test is hit. Recomended usage is to use C to set rules with strong S/O values (ie. 1.0) to be run first, and make instant spam or ham classification based on that. To override a test that uses shortcircuiting, you can set the classification type to C. =over 4 =item on Shortcircuits the rest of the tests, but does not make a strict classification of spam or ham. Rather, it uses the default score for the rule being shortcircuited. This would allow you, for example, to define a rule such as =over 4 body TEST /test/ describe TEST test rule that scores barely over spam threshold score TEST 5.5 priority TEST -100 shortcircuit TEST on =back The result of a message hitting the above rule would be a final score of 5.5, as opposed to 100 (default) if it were classified as spam. =item off Disables shortcircuiting on said rule. =item spam Shortcircuit the rule using a set of defaults; override the default score of this rule with the score from C, set the C tflag, and set priority to C<-100>. In other words, equivalent to: =over 4 shortcircuit TEST on priority TEST -100 score TEST 100 tflags TEST noautolearn =back =item ham Shortcircuit the rule using a set of defaults; override the default score of this rule with the score from C, set the C and C tflags, and set priority to C<-100>. In other words, equivalent to: =over 4 shortcircuit TEST on priority TEST -100 score TEST -100 tflags TEST noautolearn nice =back =back =cut push (@cmds, { setting => 'shortcircuit', code => sub { my ($self, $key, $value, $line) = @_; my ($rule,$type); unless (defined $value && $value !~ /^$/) { return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; } if ($value =~ /^(\S+)\s+(\S+)$/) { $rule=$1; $type=$2; } else { return $Mail::SpamAssassin::Conf::INVALID_VALUE; } if ($type =~ m/^(?:spam|ham)$/) { dbg("shortcircuit: adding $rule using abbreviation $type"); # set the defaults: $self->{shortcircuit}->{$rule} = $type; $self->{priority}->{$rule} = -100; my $tf = $self->{tflags}->{$rule}; $self->{tflags}->{$rule} = ($tf ? $tf." " : "") . ($type eq 'ham' ? "nice " : "") . "noautolearn"; } elsif ($type eq "on") { $self->{shortcircuit}->{$rule} = "on"; } elsif ($type eq "off") { delete $self->{shortcircuit}->{$rule}; } else { return $Mail::SpamAssassin::Conf::INVALID_VALUE; } } }); =item shortcircuit_spam_score n.nn (default: 100) When shortcircuit is used on a rule, and the shortcircuit classification type is set to C, this value should be applied in place of the default score for that rule. =cut push (@cmds, { setting => 'shortcircuit_spam_score', default => 100, type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC }); =item shortcircuit_ham_score n.nn (default: -100) When shortcircuit is used on a rule, and the shortcircuit classification type is set to C, this value should be applied in place of the default score for that rule. =cut push (@cmds, { setting => 'shortcircuit_ham_score', default => -100, type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC }); $conf->{parser}->register_commands(\@cmds); } =head1 TAGS The following tags are added to the set available for use in reports, headers etc.: _SC_ shortcircuit status (classification and rule name) _SCRULE_ rulename that caused the shortcircuit _SCTYPE_ shortcircuit classification ("spam", "ham", "default", "none") =cut sub hit_rule { my ($self, $params) = @_; my $scan = $params->{permsgstatus}; my $rule = $params->{rulename}; # don't s/c if we're linting return if ($scan->{lint_rules}); # don't s/c if we're in compile_now() return if ($self->{am_compiling}); my $sctype = $scan->{conf}->{shortcircuit}->{$rule}; return unless $sctype; my $conf = $scan->{conf}; my $score = $params->{score}; $scan->{shortcircuit_rule} = $rule; if ($sctype eq 'on') { # guess by rule score dbg("shortcircuit: s/c due to $rule, using score of $score"); $scan->{shortcircuit_type} = ($score < 0 ? 'ham' : 'spam'); } else { $scan->{shortcircuit_type} = $sctype; if ($sctype eq 'ham') { $score = $conf->{shortcircuit_ham_score}; } else { $score = $conf->{shortcircuit_spam_score}; } dbg("shortcircuit: s/c $sctype due to $rule, using score of $score"); } # bug 5256: if we short-circuit, don't do auto-learning $scan->{disable_auto_learning} = 1; # I don't think we really need to do these... # undef $scan->{test_names_hit}; # reset rule hits # $scan->{score} = 0; # reset score # $scan->{tag_data}->{REPORT} = ''; # reset tag data # $scan->{tag_data}->{SUMMARY} = ''; # reset tag data } sub parsed_metadata { my ($self, $params) = @_; my $scan = $params->{permsgstatus}; $scan->set_tag ('SC', sub { my $rule = $scan->{shortcircuit_rule}; my $type = $scan->{shortcircuit_type}; return "$rule ($type)" if ($rule); return "no"; }); $scan->set_tag ('SCRULE', sub { my $rule = $scan->{shortcircuit_rule}; return ($rule || "none"); }); $scan->set_tag ('SCTYPE', sub { my $type = $scan->{shortcircuit_type}; return ($type || "no"); }); $scan->set_spamd_result_item (sub { "shortcircuit=".$scan->get_tag("SCTYPE"); }); } sub have_shortcircuited { my ($self, $params) = @_; return (exists $params->{permsgstatus}->{shortcircuit_type}) ? 1 : 0; } sub compile_now_start { my ($self, $params) = @_; $self->{am_compiling} = 1; } sub compile_now_finish { my ($self, $params) = @_; delete $self->{am_compiling}; } 1; =head1 SEE ALSO C =cut