Mailing List Archive

svn commit: rev 36276 - in spamassassin/trunk: . lib/Mail/SpamAssassin lib/Mail/SpamAssassin/Conf t
Author: jm
Date: Wed Aug 11 23:53:18 2004
New Revision: 36276

Added:
spamassassin/trunk/t/ifversion.t
Modified:
spamassassin/trunk/MANIFEST
spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm
spamassassin/trunk/lib/Mail/SpamAssassin/Constants.pm
spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm
spamassassin/trunk/t/plugin.t
Log:
bug 3674: add new 'if version' command and generalise the 'if' conditional

Modified: spamassassin/trunk/MANIFEST
==============================================================================
--- spamassassin/trunk/MANIFEST (original)
+++ spamassassin/trunk/MANIFEST Wed Aug 11 23:53:18 2004
@@ -363,3 +363,4 @@
masses/compare-models
masses/config.set0
masses/config.set1
+t/ifversion.t

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm Wed Aug 11 23:53:18 2004
@@ -2922,26 +2922,60 @@
Include configuration lines from C<filename>. Relative paths are considered
relative to the current configuration file or user preferences file.

-=item ifplugin PluginModuleName
+=item if (conditional perl expression)
+
+Used to support conditional interpretation of the configuration file. Lines
+between this and a corresponding C<endif> line, will be ignored unless the
+conditional expression evaluates as true (in the perl sense; that is, defined
+and non-0).
+
+The conditional accepts a limited subset of perl for security -- just enough to
+perform basic arithmetic comparisons. The following input is accepted:
+
+=over 4
+
+=item numbers, whitespace, arithmetic operations and grouping
+
+Namely these characters and ranges:
+
+ ( ) - + * / _ . , < = > ! ~ 0-9 whitespace
+
+=item version

-Used to support conditional interpretation, based on whether a plugin module
-has been loaded successfully or not. Lines between this and a corresponding
-C<endif> line, will be ignored unless the named plugin module has been loaded
-using C<loadplugin>.
+This will be replaced with the version number of the currently-running
+SpamAssassin engine. Note: The version used is in the internal SpamAssassin
+version format which is C<x.yyyzzz>, where x is major version, y is minor
+version, and z is maintenance version. So 3.0.0 is C<3.000000>, and 3.4.80 is
+C<3.004080>.

-Note that if the end of a configuration file is reached while still inside a
-C<ifplugin> scope, a warning will be issued, but parsing will restart on
+=item plugin(Name::Of::Plugin)
+
+This is a function call that returns C<1> if the plugin named
+C<Name::Of::Plugin> is loaded, or C<undef> otherwise.
+
+=back
+
+If the end of a configuration file is reached while still inside a
+C<if> scope, a warning will be issued, but parsing will restart on
the next file.

For example:

+ if (version > 3.000000)
+ header MY_FOO ...
+ endif
+
loadplugin MyPlugin plugintest.pm

- ifplugin MyPlugin
+ if plugin (MyPlugin)
header MY_PLUGIN_FOO eval:check_for_foo()
score MY_PLUGIN_FOO 0.1
endif

+=item ifplugin PluginModuleName
+
+An alias for C<if plugin(PluginModuleName)>.
+
=item require_version n.n.n

Indicates that the entire file, from this line on, requires a certain
@@ -2949,9 +2983,9 @@
of SpamAssassin tries to read the configuration from this file, it will
output a warning instead, and ignore it.

-Note: The version must be in the internal SpamAssassin version format
-which is: x.yyyzzz. x is major version, y is minor version, and z is
-maintenance version. So 3.0.0 is 3.000000, and 3.4.80 is 3.004080.
+Note: The version used is in the internal SpamAssassin version format which is
+C<x.yyyzzz>, where x is major version, y is minor version, and z is maintenance
+version. So 3.0.0 is C<3.000000>, and 3.4.80 is C<3.004080>.

=cut


Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm Wed Aug 11 23:53:18 2004
@@ -244,10 +244,11 @@
if (scalar @if_stack > 0) {
my $cond = pop @if_stack;

- if ($cond->{type} eq 'ifplugin') {
+ if ($cond->{type} eq 'if') {
warn "unclosed 'if' in ".
- $self->{currentfile}.": ifplugin ".$cond->{plugin}."\n";
- } else {
+ $self->{currentfile}.": if ".$cond->{conditional}."\n";
+ }
+ else {
die "unknown 'if' type: ".$cond->{type}."\n";
}

@@ -275,18 +276,14 @@
}

if ($key eq 'ifplugin') {
- push (@if_stack, {
- type => 'ifplugin',
- plugin => $value,
- skip_parsing => $skip_parsing
- });
-
- if ($conf->{plugins_loaded}->{$value}) {
- # leave $skip_parsing as-is; we may not be parsing anyway in this block.
- # in other words, support nested 'if's and 'require_version's
- } else {
- $skip_parsing = 1;
- }
+ $self->handle_conditional ($key, "plugin ($value)",
+ \@if_stack, \$skip_parsing);
+ next;
+ }
+
+ if ($key eq 'if') {
+ $self->handle_conditional ($key, $value,
+ \@if_stack, \$skip_parsing);
next;
}

@@ -389,6 +386,58 @@
$self->set_default_scores();

delete $self->{scoresonly};
+}
+
+sub handle_conditional {
+ my ($self, $key, $value, $if_stack_ref, $skip_parsing_ref) = @_;
+ my $conf = $self->{conf};
+
+ my $lexer = ARITH_EXPRESSION_LEXER;
+ my @tokens = ($value =~ m/($lexer)/g);
+
+ my $eval = '';
+ my $bad = 0;
+ foreach my $token (@tokens) {
+ if ($token =~ /^(\W+|[\-\+\d\.]+)$/) {
+ $eval .= $1." "; # note: untaints!
+ }
+ elsif ($token eq 'plugin') {
+ # replace with method call
+ $eval .= "\$self->cond_clause_plugin_loaded";
+ }
+ elsif ($token eq 'version') {
+ $eval .= $Mail::SpamAssassin::VERSION." ";
+ }
+ elsif ($token =~ /^(\w[\w\:]+)$/) { # class name
+ $eval .= "\"$1\" "; # note: untaints!
+ }
+ else {
+ $bad++; warn "unparseable chars in 'if $value': '$token'\n";
+ }
+ }
+
+ if ($bad) {
+ $conf->{errors}++;
+ return -1;
+ }
+
+ push (@{$if_stack_ref}, {
+ type => 'if',
+ conditional => $value,
+ skip_parsing => $$skip_parsing_ref
+ });
+
+ if (eval $eval) {
+ # leave $skip_parsing as-is; we may not be parsing anyway in this block.
+ # in other words, support nested 'if's and 'require_version's
+ } else {
+ $$skip_parsing_ref = 1;
+ }
+}
+
+# functions supported in the "if" eval:
+sub cond_clause_plugin_loaded {
+ return $_[0]->{conf}->{plugins_loaded}->{$_[1]};
}

# Let's do some linting here ...

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Constants.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Constants.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Constants.pm Wed Aug 11 23:53:18 2004
@@ -36,7 +36,7 @@
@SA_VARS = qw(
META_TEST_MIN_PRIORITY HARVEST_DNSBL_PRIORITY MBX_SEPARATOR
MAX_BODY_LINE_LENGTH MAX_HEADER_KEY_LENGTH MAX_HEADER_VALUE_LENGTH
- MAX_HEADER_LENGTH
+ MAX_HEADER_LENGTH ARITH_EXPRESSION_LEXER
);

%EXPORT_TAGS = (
@@ -189,5 +189,22 @@
use constant MAX_HEADER_VALUE_LENGTH => 8192;
# maximum byte length of entire header
use constant MAX_HEADER_LENGTH => 65536;
+
+# used for meta rules and "if" conditionals in Conf::Parser
+use constant ARITH_EXPRESSION_LEXER => qr/(?:
+ [\-\+\d\.]+| # A Number
+ \w[\w\:]+| # Rule or Class Name
+ [\(\)]| # Parens
+ \|\|| # Boolean OR
+ \&\&| # Boolean AND
+ \^| # Boolean XOR
+ !| # Boolean NOT
+ >=?| # GT or EQ
+ <=?| # LT or EQ
+ ==| # EQ
+ !=| # NEQ
+ [\+\-\*\/]| # Mathematical Operator
+ [\?:] # ? : Operator
+ )/ox;

1;

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm Wed Aug 11 23:53:18 2004
@@ -2005,22 +2005,8 @@
my $token;

# Lex the rule into tokens using a rather simple RE method ...
- my @tokens =
- $rule =~ m/(
- \w+| # Rule Name
- [\(\)]| # Parens
- \|\|| # Boolean OR
- \&\&| # Boolean AND
- \^| # Boolean XOR
- !| # Boolean NOT
- >=?| # GT or EQ
- <=?| # LT or EQ
- ==| # EQ
- !=| # NEQ
- [\+\-\*\/]| # Mathematical Operator
- [\?:]| # ? : Operator
- \d+ # A Number
- )/gx;
+ my $lexer = ARITH_EXPRESSION_LEXER;
+ my @tokens = ($rule =~ m/$lexer/g);

# Set the rule blank to start
$meta{$rulename} = "";
@@ -2107,7 +2093,7 @@
eval $evalstr;

if ($@) {
- warn "Failed to run header SpamAssassin tests, skipping some: $@\n";
+ warn "Failed to run meta SpamAssassin tests, skipping some: $@\n";
$self->{rule_errors}++;
}
else {

Added: spamassassin/trunk/t/ifversion.t
==============================================================================
--- (empty file)
+++ spamassassin/trunk/t/ifversion.t Wed Aug 11 23:53:18 2004
@@ -0,0 +1,33 @@
+#!/usr/bin/perl
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("ifversion");
+use Test; BEGIN { plan tests => 4 };
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+
+q{ GTUBE }, 'gtube',
+q{ SHOULD_BE_CALLED }, 'should_be_called'
+
+);
+
+%anti_patterns = (
+
+q{ SHOULD_NOT_BE_CALLED }, 'should_not_be_called'
+
+);
+
+tstlocalrules ("
+ if (version > 9.99999)
+ body SHOULD_NOT_BE_CALLED /./
+ endif
+ if (version <= 9.99999)
+ body SHOULD_BE_CALLED /./
+ endif
+");
+
+ok (sarun ("-L -t < data/spam/gtube.eml", \&patterns_run_cb));
+ok_all_patterns();
+

Modified: spamassassin/trunk/t/plugin.t
==============================================================================
--- spamassassin/trunk/t/plugin.t (original)
+++ spamassassin/trunk/t/plugin.t Wed Aug 11 23:53:18 2004
@@ -26,7 +26,7 @@
ifplugin FooPlugin
header SHOULD_NOT_BE_CALLED eval:doesnt_exist()
endif
- ifplugin Mail::SpamAssassin::Plugin::Test
+ if plugin(Mail::SpamAssassin::Plugin::Test)
header MY_TEST_PLUGIN eval:check_test_plugin()
endif
");