Mailing List Archive

svn commit: rev 6250 - in incubator/spamassassin/trunk: . lib/Mail lib/Mail/SpamAssassin
Author: jm
Date: Thu Jan 22 22:01:21 2004
New Revision: 6250

Added:
incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin.pm
incubator/spamassassin/trunk/lib/Mail/SpamAssassin/PluginHandler.pm
Modified:
incubator/spamassassin/trunk/MANIFEST
incubator/spamassassin/trunk/MANIFEST.SKIP
incubator/spamassassin/trunk/lib/Mail/SpamAssassin.pm
incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
incubator/spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm
Log:
plugin support, using new loadplugin configuration command

Modified: incubator/spamassassin/trunk/MANIFEST
==============================================================================
--- incubator/spamassassin/trunk/MANIFEST (original)
+++ incubator/spamassassin/trunk/MANIFEST Thu Jan 22 22:01:21 2004
@@ -240,3 +240,5 @@
tools/split_corpora
tools/test_extract
tools/triplets.pl
+lib/Mail/SpamAssassin/Plugin.pm
+lib/Mail/SpamAssassin/PluginHandler.pm

Modified: incubator/spamassassin/trunk/MANIFEST.SKIP
==============================================================================
--- incubator/spamassassin/trunk/MANIFEST.SKIP (original)
+++ incubator/spamassassin/trunk/MANIFEST.SKIP Thu Jan 22 22:01:21 2004
@@ -111,3 +111,4 @@
tasks/.*
build/2.60_change_summary
build/replace_license_blocks
+sa-learn

Modified: incubator/spamassassin/trunk/lib/Mail/SpamAssassin.pm
==============================================================================
--- incubator/spamassassin/trunk/lib/Mail/SpamAssassin.pm (original)
+++ incubator/spamassassin/trunk/lib/Mail/SpamAssassin.pm Thu Jan 22 22:01:21 2004
@@ -113,6 +113,7 @@
use Mail::SpamAssassin::PerMsgStatus;
use Mail::SpamAssassin::MsgParser;
use Mail::SpamAssassin::Bayes;
+use Mail::SpamAssassin::PluginHandler;

use File::Basename;
use File::Path;
@@ -310,6 +311,7 @@
$DEBUG->{rulesrun}=64;

$self->{conf} ||= new Mail::SpamAssassin::Conf ($self);
+ $self->{plugins} = Mail::SpamAssassin::PluginHandler->new ($self);

$self->{save_pattern_hits} ||= 0;

@@ -1038,11 +1040,13 @@
my $text = join ('',<IN>);
close IN;

+ $self->{conf}->{main} = $self;
$self->{conf}->parse_scores_only ($text);
if ($self->{conf}->{allow_user_rules}) {
dbg("finishing parsing!");
$self->{conf}->finish_parsing();
}
+ delete $self->{conf}->{main}; # to allow future GC'ing
}

###########################################################################
@@ -1232,8 +1236,10 @@
warn "No configuration text or files found! Please check your setup.\n";
}

+ $self->{conf}->{main} = $self;
$self->{conf}->parse_rules ($self->{config_text});
$self->{conf}->finish_parsing ();
+ delete $self->{conf}->{main}; # to allow future GC'ing

delete $self->{config_text};

@@ -1453,6 +1459,14 @@
closedir SA_CF_DIR;

return map { "$dir/$_" } sort { $a cmp $b } @cfs; # sort numerically
+}
+
+###########################################################################
+
+sub call_plugins {
+ my $self = shift;
+ my $subname = shift;
+ return $self->{plugins}->callback ($subname, @_);
}

###########################################################################

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 Thu Jan 22 22:01:21 2004
@@ -201,6 +201,8 @@
$self->{rawbody_evals} = { };
$self->{meta_tests} = { };

+ $self->{eval_plugins} = { };
+
# testing stuff
$self->{regression_tests} = { };

@@ -1986,7 +1988,16 @@
$self->{num_check_received} = $1+0; next;
}

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

+ if ($self->{main}->call_plugins ("parse_config", {
+ line => $_,
+ user_config => $scoresonly
+ }))
+ {
+ # a plugin dealt with it successfully.
+ next;
+ }

###########################################################################
# SECURITY: no eval'd code should be loaded before this line.
@@ -2643,6 +2654,20 @@
# user_scores_sql_table here. All just take \S+ and set the string of the
# same name on $self.

+=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.
+
+See C<Mail::SpamAssassin::Plugin> for more details on writing plugins.
+
+=cut
+
+ if (/^loadplugin\s+(\S+)\s+(\S+)$/) {
+ $self->load_plugin ($1, $2); next;
+ }
+
###########################################################################

failed_line:
@@ -2866,6 +2891,18 @@
}

return 0;
+}
+
+###########################################################################
+
+sub load_plugin {
+ my ($self, $package, $path) = @_;
+ $self->{main}->{plugins}->load_plugin ($package, $path);
+}
+
+sub register_eval_rule {
+ my ($self, $pluginobj, $nameofsub) = @_;
+ $self->{eval_plugins}->{$nameofsub} = $pluginobj;
}

###########################################################################

Modified: incubator/spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm
==============================================================================
--- incubator/spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm (original)
+++ incubator/spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm Thu Jan 22 22:01:21 2004
@@ -2174,6 +2174,17 @@
my ($function, @args) = @{$test};
unshift(@args, @extraevalargs);

+ # check to make sure the function is defined
+ if (!$self->can ($function)) {
+ my $pluginobj = $self->{conf}->{eval_plugins}->{$function};
+ if ($pluginobj) {
+ # we have a plugin for this. eval its function
+ $self->register_plugin_eval_glue ($pluginobj, $function);
+ } else {
+ dbg ("no method found for eval test $function");
+ }
+ }
+
eval {
$result = $self->$function(@args);
};
@@ -2191,6 +2202,31 @@
} else {
#dbg("Ran run_eval_test rule $rulename but did not get hit", "rulesrun", 32) if $debugenabled;
}
+ }
+}
+
+sub register_plugin_eval_glue {
+ my ($self, $pluginobj, $function) = @_;
+
+ dbg ("registering glue method for $function ($pluginobj)");
+ my $evalstr = <<"ENDOFEVAL";
+{
+ package Mail::SpamAssassin::PerMsgStatus;
+
+ sub $function {
+ my (\$self) = shift;
+ my \$plugin = \$self->{conf}->{eval_plugins}->{$function};
+ return \$plugin->$function (\$self, \@_);
+ }
+
+ 1;
+}
+ENDOFEVAL
+ eval $evalstr;
+
+ if ($@) {
+ warn "Failed to run header SpamAssassin tests, skipping some: $@\n";
+ $self->{rule_errors}++;
}
}


Added: incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin.pm
==============================================================================
--- (empty file)
+++ incubator/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin.pm Thu Jan 22 22:01:21 2004
@@ -0,0 +1,309 @@
+# <@LICENSE>
+# ====================================================================
+# The Apache Software License, Version 1.1
+#
+# Copyright (c) 2000 The Apache Software Foundation. All rights
+# reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in
+# the documentation and/or other materials provided with the
+# distribution.
+#
+# 3. The end-user documentation included with the redistribution,
+# if any, must include the following acknowledgment:
+# "This product includes software developed by the
+# Apache Software Foundation (http://www.apache.org/)."
+# Alternately, this acknowledgment may appear in the software itself,
+# if and wherever such third-party acknowledgments normally appear.
+#
+# 4. The names "Apache" and "Apache Software Foundation" must
+# not be used to endorse or promote products derived from this
+# software without prior written permission. For written
+# permission, please contact apache@apache.org.
+#
+# 5. Products derived from this software may not be called "Apache",
+# nor may "Apache" appear in their name, without prior written
+# permission of the Apache Software Foundation.
+#
+# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+# ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+# ====================================================================
+#
+# This software consists of voluntary contributions made by many
+# individuals on behalf of the Apache Software Foundation. For more
+# information on the Apache Software Foundation, please see
+# <http://www.apache.org/>.
+#
+# Portions of this software are based upon public domain software
+# originally written at the National Center for Supercomputing Applications,
+# University of Illinois, Urbana-Champaign.
+# </@LICENSE>
+
+=head1 NAME
+
+Mail::SpamAssassin::Plugin - SpamAssassin plugin base class
+
+=head1 SYNOPSIS
+
+ package MyPlugin;
+
+ use Mail::SpamAssassin::Plugin;
+ use vars qw(@ISA);
+ @ISA = qw(Mail::SpamAssassin::Plugin);
+
+ sub new {
+ my $class = shift;
+ my $mailsaobject = shift;
+
+ # the usual perlobj boilerplate to create a subclass object
+ $class = ref($class) || $class;
+ my $self = $class->SUPER::new($mailsaobject);
+ bless ($self, $class);
+
+ # then register an eval rule
+ $self->register_eval_rule ("check_for_foo");
+
+ # and return the new plugin object
+ return $self;
+ }
+
+ ...methods...
+
+ 1;
+
+=head1 DESCRIPTION
+
+This is the base class for SpamAssassin plugins; all plugins must be objects
+that implement this class.
+
+This class provides no-op stub methods for all the callbacks that a plugin
+can receive. It is expected that your plugin will override one or more
+of these stubs to perform its actions.
+
+SpamAssassin implements a plugin chain; each callback event is passed to each
+of the registered plugin objects in turn. Any plugin can call
+C<$plugin->inhibit_further_callbacks()> to block delivery of that event to
+later plugins in the chain. This is useful if the plugin has handled the
+event, and there will be no need for later plugins to handle it as well.
+
+The following methods can be overridden by subclasses to handle events
+that SpamAssassin will call back to:
+
+=head1 INTERFACE
+
+=over 4
+
+=cut
+
+package Mail::SpamAssassin::Plugin;
+use Mail::SpamAssassin;
+
+use strict;
+use bytes;
+
+use vars qw{
+ @ISA $VERSION
+};
+
+@ISA = qw();
+$VERSION = 'bogus';
+
+###########################################################################
+
+=item $plugin = MyPluginClass->new ($mailsaobject)
+
+Constructor. Plugins that need to register themselves will need to
+define their own; the default super-class constructor will work fine
+for plugins that just override a method.
+
+Note that subclasses must provide the C<$mailsaobject> to the
+superclass constructor, like so:
+
+ my $self = $class->SUPER::new($mailsaobject);
+
+=cut
+
+sub new {
+ my $class = shift;
+ my $mailsaobject = shift;
+ $class = ref($class) || $class;
+
+ if (!defined $mailsaobject) {
+ die "plugin: usage: Mail::SpamAssassin::Plugin::new(class,mailsaobject)";
+ }
+
+ my $self = {
+ main => $mailsaobject,
+ _inhibit_further_callbacks => 0
+ };
+ bless ($self, $class);
+ $self;
+}
+
+=item $plugin->parse_config ( { options ... } )
+
+Parse a configuration line that hasn't already been handled. C<options>
+is a reference to a hash containing these options:
+
+=over 4
+
+=item line
+
+The line of configuration text to parse. This has leading and trailing
+whitespace, and comments, removed.
+
+=item user_config
+
+A boolean: C<1> if reading a user's configuration, C<0> if reading the
+system-wide configuration files.
+
+=back
+
+If the configuration line was a setting that is handled by this plugin, the
+method implementation should call C<$plugin->inhibit_further_callbacks()> and
+return C<1>.
+
+If the setting is not handled by this plugin, the method should return C<0> so
+that a later plugin may handle it, or so that SpamAssassin can output a warning
+message to the user if no plugin understands it.
+
+Note that it is suggested that configuration be stored on the
+C<Mail::SpamAssassin::Conf> object in use, instead of the plugin object itself.
+That can be found as C<$plugin->{main}->{conf}>.
+
+=cut
+
+sub parse_config {
+ my ($self, $opts) = @_;
+ # implemented by subclasses, no-op by default
+ return 0;
+}
+
+=item $plugin->finish ()
+
+Called when the C<Mail::SpamAssassin> object is destroyed.
+
+=cut
+
+sub finish {
+ my ($self) = @_;
+ # implemented by subclasses, no-op by default
+}
+
+###########################################################################
+
+=back
+
+=head1 HELPER APIS
+
+These methods provide an API for plugins to register themselves
+to receive specific events, or control the callback chain behaviour.
+
+=over 4
+
+=item $plugin->register_eval_rule ($nameofevalsub)
+
+Plugins that implement an eval test will need to call this, so that
+SpamAssassin calls into the object when that eval test is encountered.
+
+For example,
+
+ $plugin->register_eval_rule ('check_for_foo')
+
+will cause C<$plugin->check_for_foo()> to be called for this
+SpamAssassin rule:
+
+ header FOO_RULE eval:check_for_foo()
+
+Note that eval rules are passed the following arguments:
+
+=over 4
+
+=item The plugin object itself
+
+=item The C<Mail::SpamAssassin::PerMsgStatus> object calling the rule
+
+=item any and all arguments specified in the configuration file
+
+=back
+
+In other words, the eval test method should look something like this:
+
+ sub check_for_foo {
+ my ($self, $permsgstatus, ...arguments...) = @_;
+ ...code returning 0 or 1
+ }
+
+Note that the headers can be accessed using the C<get()> method on the
+C<Mail::SpamAssassin::PerMsgStatus> object, and the body by
+C<get_decoded_stripped_body_text_array()> and other similar methods.
+Similarly, the C<Mail::SpamAssassin::Conf> object holding the current
+configuration may be accessed through C<$permsgstatus->{main}->{conf}>.
+
+The eval rule should return C<1> for a hit, or C<0> if the rule
+is not hit.
+
+State for a single message being scanned should be stored on the C<$checker>
+object, not on the C<$self> object, since C<$self> persists between scan
+operations.
+
+=cut
+
+sub register_eval_rule {
+ my ($self, $nameofsub) = @_;
+ $self->{main}->{conf}->register_eval_rule ($self, $nameofsub);
+}
+
+=item $plugin->inhibit_further_callbacks()
+
+Tells the plugin handler to inhibit calling into other plugins in the plugin
+chain for the current callback. Frequently used when parsing configuration
+settings using C<parse_config()>.
+
+=cut
+
+sub inhibit_further_callbacks {
+ my ($self) = @_;
+ $self->{_inhibit_further_callbacks} = 1;
+}
+
+=item dbg ($message)
+
+Output a debugging message C<$message>, if the SpamAssassin object is running
+with debugging turned on.
+
+=cut
+
+sub dbg { Mail::SpamAssassin::dbg (@_); }
+
+1;
+
+=back
+
+=head1 SEE ALSO
+
+C<Mail::SpamAssassin>
+
+C<Mail::SpamAssassin::PerMsgStatus>
+
+http://bugzilla.spamassassin.org/show_bug.cgi?id=2163
+
+=cut

Added: incubator/spamassassin/trunk/lib/Mail/SpamAssassin/PluginHandler.pm
==============================================================================
--- (empty file)
+++ incubator/spamassassin/trunk/lib/Mail/SpamAssassin/PluginHandler.pm Thu Jan 22 22:01:21 2004
@@ -0,0 +1,162 @@
+# <@LICENSE>
+# ====================================================================
+# The Apache Software License, Version 1.1
+#
+# Copyright (c) 2000 The Apache Software Foundation. All rights
+# reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in
+# the documentation and/or other materials provided with the
+# distribution.
+#
+# 3. The end-user documentation included with the redistribution,
+# if any, must include the following acknowledgment:
+# "This product includes software developed by the
+# Apache Software Foundation (http://www.apache.org/)."
+# Alternately, this acknowledgment may appear in the software itself,
+# if and wherever such third-party acknowledgments normally appear.
+#
+# 4. The names "Apache" and "Apache Software Foundation" must
+# not be used to endorse or promote products derived from this
+# software without prior written permission. For written
+# permission, please contact apache@apache.org.
+#
+# 5. Products derived from this software may not be called "Apache",
+# nor may "Apache" appear in their name, without prior written
+# permission of the Apache Software Foundation.
+#
+# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+# ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+# ====================================================================
+#
+# This software consists of voluntary contributions made by many
+# individuals on behalf of the Apache Software Foundation. For more
+# information on the Apache Software Foundation, please see
+# <http://www.apache.org/>.
+#
+# Portions of this software are based upon public domain software
+# originally written at the National Center for Supercomputing Applications,
+# University of Illinois, Urbana-Champaign.
+# </@LICENSE>
+
+=head1 NAME
+
+Mail::SpamAssassin::PluginHandler - SpamAssassin plugin handler
+
+=cut
+
+package Mail::SpamAssassin::PluginHandler;
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::Plugin;
+
+use strict;
+use bytes;
+
+use vars qw{
+ @ISA $VERSION
+};
+
+@ISA = qw();
+
+$VERSION = 'bogus'; # avoid CPAN.pm picking up version strings later
+
+###########################################################################
+
+sub new {
+ my $class = shift;
+ my $main = shift;
+ $class = ref($class) || $class;
+ my $self = {
+ plugins => [ ],
+ main => $main
+ };
+ bless ($self, $class);
+ $self;
+}
+
+###########################################################################
+
+sub load_plugin {
+ my ($self, $package, $path) = @_;
+
+ dbg ("plugin: loading $path");
+
+ if (!do $path) {
+ 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) {
+ $self->{main}->{plugins}->register_plugin ($plugin);
+ }
+}
+
+sub register_plugin {
+ my ($self, $plugin) = @_;
+ $plugin->{main} = $self->{main};
+ push (@{$self->{plugins}}, $plugin);
+ dbg ("plugin: registered $plugin");
+}
+
+###########################################################################
+
+sub callback {
+ my $self = shift;
+ my $subname = shift;
+ my $ret;
+
+ foreach my $plugin (@{$self->{plugins}}) {
+ $plugin->{_inhibit_further_callbacks} = 0;
+
+ dbg ("plugin: calling $subname on $plugin");
+ my $methodref = $plugin->can ($subname);
+ $ret = &$methodref ($plugin, @_);
+
+ if ($plugin->{_inhibit_further_callbacks}) {
+ dbg ("plugin: $plugin inhibited further callbacks");
+ last;
+ }
+ }
+
+ return $ret;
+}
+
+###########################################################################
+
+sub finish {
+ my $self = shift;
+ foreach my $plugin (@{$self->{plugins}}) {
+ $plugin->finish();
+ delete $plugin->{main};
+ }
+ delete $self->{plugins};
+ delete $self->{main};
+}
+
+###########################################################################
+
+sub dbg { Mail::SpamAssassin::dbg (@_); }
+
+1;