Mailing List Archive

svn commit: rev 6573 - in incubator/spamassassin/trunk: . lib/Mail/SpamAssassin lib/Mail/SpamAssassin/Plugin t
Author: jm
Date: Sat Feb 7 17:44:52 2004
New Revision: 6573

Added:
incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/
incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Test.pm
incubator/spamassassin/trunk/t/plugin.t (contents, props changed)
Modified:
incubator/spamassassin/trunk/MANIFEST
incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
incubator/spamassassin/trunk/lib/Mail/SpamAssassin/PluginHandler.pm
Log:
Plugins:

- allow loading from @INC

- allow loading with relative paths

- added 'ifplugin' and 'endif' conditional-loading keywords for Conf

- added a Test plugin class

- added test for above (and for plugins in general)

- cleaned up a few more cases where Conf uses regexps instead of
$key and $value



Modified: incubator/spamassassin/trunk/MANIFEST
==============================================================================
--- incubator/spamassassin/trunk/MANIFEST (original)
+++ incubator/spamassassin/trunk/MANIFEST Sat Feb 7 17:44:52 2004
@@ -304,3 +304,5 @@
masses/rule-qa/corpus-tagtime
tools/desc_length.pl
t/data/nice/not_gtube.eml
+lib/Mail/SpamAssassin/Plugin/Test.pm
+t/plugin.t

Modified: incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
==============================================================================
--- incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm (original)
+++ incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm Sat Feb 7 17:44:52 2004
@@ -346,7 +346,8 @@
} # (eg. .utf8 or @euro)

$self->{currentfile} = '(no file)';
- my $skipfile = 0;
+ my @if_stack = ();
+ my $skip_parsing = 0;

foreach (split (/\n/, $_[1])) {
s/(?<!\\)#.*$//; # remove comments
@@ -359,8 +360,22 @@
# Versioning assertions
if (/^file\s+start\s+(.+)$/) { $self->{currentfile} = $1; next; }
if (/^file\s+end/) {
+ if (scalar @if_stack > 0) {
+ my $cond = pop @if_stack;
+
+ if ($cond->{type} eq 'ifplugin') {
+ warn "unclosed 'if' in ".
+ $self->{currentfile}.": ifplugin ".$cond->{plugin}."\n";
+ } else {
+ die "unknown 'if' type: ".$cond->{type}."\n";
+ }
+
+ $self->{errors}++;
+ @if_stack = ();
+ }
+
$self->{currentfile} = '(no file)';
- $skipfile = 0;
+ $skip_parsing = 0;
next;
}

@@ -376,6 +391,53 @@
$value =~ /^(.*)$/;
$value = $1;

+=head2 CONDITIONAL-PARSING OPTIONS
+
+=item ifplugin PluginModuleName
+
+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>.
+
+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
+the next file.
+
+For example:
+
+ loadplugin MyPlugin plugintest.pm
+
+ ifplugin MyPlugin
+ header MY_PLUGIN_FOO eval:check_for_foo()
+ score MY_PLUGIN_FOO 0.1
+ endif
+
+=cut
+
+ if ($key eq 'ifplugin') {
+ push (@if_stack, {
+ type => 'ifplugin',
+ plugin => $value,
+ skip_parsing => $skip_parsing
+ });
+
+ if ($self->{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;
+ }
+ next;
+ }
+
+ # and the endif statement:
+ if ($key eq 'endif') {
+ my $cond = pop @if_stack;
+ $skip_parsing = $cond->{skip_parsing};
+ next;
+ }
+
=head2 VERSION OPTIONS

=over 4
@@ -398,13 +460,13 @@
"$Mail::SpamAssassin::VERSION. Maybe you need to use ".
"the -C switch, or remove the old config files? ".
"Skipping this file";
- $skipfile = 1;
+ $skip_parsing = 1;
$self->{errors}++;
}
next;
}

- if ($skipfile) { next; }
+ if ($skip_parsing) { next; }

# note: no eval'd code should be loaded before the SECURITY line below.
###########################################################################
@@ -1786,8 +1848,8 @@

=cut

- if (/^bayes_sql_override_username\s+(.*)$/) {
- $self->{bayes_sql_override_username} = $1; next;
+ if ($key eq 'bayes_sql_override_username') {
+ $self->{bayes_sql_override_username} = $value; next;
}

##############
@@ -2586,8 +2648,8 @@

=cut

- if (/^user_scores_ldap_username\s+(.*?)\s*$/) {
- $self->{user_scores_ldap_username} = $1; next;
+ if ($key eq 'user_scores_ldap_username') {
+ $self->{user_scores_ldap_username} = $value; next;
}

=item user_scores_ldap_password
@@ -2596,35 +2658,32 @@

=cut

- if (/^user_scores_ldap_password\s+(.*?)\s*$/) {
- $self->{user_scores_ldap_password} = $1; next;
+ if ($key eq 'user_scores_ldap_password') {
+ $self->{user_scores_ldap_password} = $value; next;
}

-=item loadplugin PluginModuleName /path/to/module.pm
+=item loadplugin PluginModuleName [/path/to/module.pm]

Load a SpamAssassin plugin module. The C<PluginModuleName> is the perl module
-name, used to create the plugin object itself. C</path/to/module.pm> is the
-file to load, containing the module's perl code; if it's specified as a
-relative path, it's considered to be relative to the current configuration
-file.
+name, used to create the plugin object itself.
+
+C</path/to/module.pm> is the file to load, containing the module's perl code;
+if it's specified as a relative path, it's considered to be relative to the
+current configuration file. If it is omitted, the module will be loaded
+using perl's search path (the C<@INC> array).

See C<Mail::SpamAssassin::Plugin> for more details on writing plugins.

=cut

- # leave as RE right now
- if (/^loadplugin\s+(\S+)\s+(\S+)$/) {
- my $mod = $1;
- my $path = $2;
-
- if (!File::Spec->file_name_is_absolute ($path)) {
- my ($vol, $dirs, $file) = File::Spec->splitpath ($self->{currentfile});
- $path = File::Spec->catpath ($vol, $dirs, $path);
- dbg ("loadplugin: fixed relative path: $path");
- }
- $self->load_plugin ($mod, $path); next;
+ if ($key eq 'loadplugin') { # single-arg variant
+ $self->load_plugin ($value); next;
+ }
+ if (/^loadplugin\s+(\S+)\s+(\S+)$/) { # two-arg variant
+ $self->load_plugin ($1, $2); next;
}

+
###########################################################################

failed_line:
@@ -2861,6 +2920,11 @@
sub load_plugin {
my ($self, $package, $path) = @_;
$self->{main}->{plugins}->load_plugin ($package, $path);
+}
+
+sub load_plugin_succeeded {
+ my ($self, $plugin, $package, $path) = @_;
+ $self->{plugins_loaded}->{$package} = 1;
}

sub register_eval_rule {

Added: incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Test.pm
==============================================================================
--- (empty file)
+++ incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Test.pm Sat Feb 7 17:44:52 2004
@@ -0,0 +1,41 @@
+=head1
+
+To try this out, write these lines to /etc/mail/spamassassin/plugintest.cf:
+
+ loadplugin Mail::SpamAssassin::Plugin::Test
+ header MY_TEST_PLUGIN eval:check_test_plugin()
+
+=cut
+
+package Mail::SpamAssassin::Plugin::Test;
+
+use Mail::SpamAssassin::Plugin;
+use vars qw(@ISA);
+@ISA = qw(Mail::SpamAssassin::Plugin);
+
+# constructor: register the eval rule
+sub new {
+ my $class = shift;
+ my $mailsaobject = shift;
+
+ # some boilerplate...
+ $class = ref($class) || $class;
+ my $self = $class->SUPER::new($mailsaobject);
+ bless ($self, $class);
+
+ # the important bit!
+ $self->register_eval_rule ("check_test_plugin");
+
+ print "registered Mail::SpamAssassin::Plugin::Test: $self\n";
+ return $self;
+}
+
+# and the eval rule itself
+sub check_test_plugin {
+ my ($self, $permsgstatus) = @_;
+ print "Mail::SpamAssassin::Plugin::Test eval test called: $self\n";
+ # ... hard work goes here...
+ return 1;
+}
+
+1;

Modified: incubator/spamassassin/trunk/lib/Mail/SpamAssassin/PluginHandler.pm
==============================================================================
--- incubator/spamassassin/trunk/lib/Mail/SpamAssassin/PluginHandler.pm (original)
+++ incubator/spamassassin/trunk/lib/Mail/SpamAssassin/PluginHandler.pm Sat Feb 7 17:44:52 2004
@@ -26,6 +26,7 @@

use strict;
use bytes;
+use File::Spec;

use vars qw{
@ISA $VERSION
@@ -54,19 +55,34 @@
sub load_plugin {
my ($self, $package, $path) = @_;

- dbg ("plugin: loading $path");
+ my $ret;
+ if ($path) {
+ dbg ("plugin: loading $package from $path");
+
+ if (!File::Spec->file_name_is_absolute ($path)) {
+ my ($vol, $dirs, $file) = File::Spec->splitpath ($self->{currentfile});
+ $path = File::Spec->catpath ($vol, $dirs, $path);
+ dbg ("plugin: fixed relative path: $path");
+ }
+ $ret = do $path;
+ }
+ else {
+ dbg ("plugin: loading $package from \@INC");
+ $ret = eval qq{ require $package; };
+ }

- if (!do $path) {
+ if (!$ret) {
if ($@) { warn "failed to parse plugin $path: $@\n"; }
elsif ($!) { warn "failed to load plugin $path: $!\n"; }
}

my $plugin = eval $package.q{->new ($self->{main}); };

- if ($@ || !$plugin) { warn "failed to create plugin $package: $@\n"; }
+ if ($@ || !$plugin) { warn "failed to create instance of plugin $package: $@\n"; }

if ($plugin) {
$self->{main}->{plugins}->register_plugin ($plugin);
+ $self->{main}->{conf}->load_plugin_succeeded ($plugin, $package, $path);
}
}


Added: incubator/spamassassin/trunk/t/plugin.t
==============================================================================
--- (empty file)
+++ incubator/spamassassin/trunk/t/plugin.t Sat Feb 7 17:44:52 2004
@@ -0,0 +1,36 @@
+#!/usr/bin/perl
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("plugin");
+use Test; BEGIN { plan tests => 6 };
+
+# ---------------------------------------------------------------------------
+
+%patterns = (
+
+q{ GTUBE }, 'gtube',
+q{ MY_TEST_PLUGIN }, 'plugin_called',
+q{ registered Mail::SpamAssassin::Plugin::Test }, 'registered',
+q{ Mail::SpamAssassin::Plugin::Test eval test called }, 'test_called',
+
+);
+
+%anti_patterns = (
+
+q{ SHOULD_NOT_BE_CALLED }, 'should_not_be_called'
+
+);
+
+tstlocalrules ("
+ loadplugin Mail::SpamAssassin::Plugin::Test
+ ifplugin FooPlugin
+ header SHOULD_NOT_BE_CALLED eval:doesnt_exist()
+ endif
+ ifplugin Mail::SpamAssassin::Plugin::Test
+ header MY_TEST_PLUGIN eval:check_test_plugin()
+ endif
+");
+
+ok (sarun ("-L -t < data/spam/gtube.eml", \&patterns_run_cb));
+ok_all_patterns();
+