SpamAssassin filter for RT 3.0.10

I got SpamAssassin working via the mail filter in
RT::Interface::email::Filter::SpamAssassin. The filter needed a
complete renovation; perhaps it came from some much earlier RT version?

The appended patch works with SpamAssassin 2.63, only tested on RT 3.0.10/
Solaris 9/MySQL/Perl5.8.3/FastCGI/etc.

There was another tricky bug: apparently SpamAssassin munges the $_
global variable when a message rings the spam bell, such that its value
in the Gateway function’s loop around @RT::MailPlugins gets unset. This
clears the plugin entry so it breaks on the next incoming email. I know
the functions/operators that set $_ are supposed have local effects but
apparently something doesn’t. Switching to an explicit loop variable
fixed the symptom, so that patch is included.

I also added a couple of new features:

  • It logs the message-id header of a message it drops on the floor at
    the ā€œinfoā€ level so you can match it with MTA logs to diagnose cases
    of vanished mail.
  • Adds the ā€œX-RT-SpamAssassin-Scoreā€ header so a scrip can sort the
    spammiest messages into a separate queue.

I know the spam filter could be implemented in the MTA or in procmail
driving the rt-mailgate, but this looked easier to maintain. If there aren’t
any objections I’ll submit it as a patch.

-- Larry

*** lib/RT/Interface/Email.pm.orig Fri Jan 2 17:55:55 2004
— lib/RT/Interface/Email.pm Mon May 3 19:52:05 2004
*** 500,521 ****

  # Since this needs loading, no matter what

! for (@RT::MailPlugins) {
my $Code;
my $NewAuthStat;
! if ( ref($) eq ā€œCODEā€ ) {
! $Code = $
;
}
else {
! $_ = ā€œRT::Interface::email::blush:" unless /^RT::Interface::email::/;
! eval "require $
;ā€;
if ($@) {
! die ("Couldn’t load module $: $@");
next;
}
no strict ā€˜refs’;
! if ( !defined( $Code = *{ $
. ā€œ::GetCurrentUserā€ }{CODE} ) ) {
! die (ā€œNo GetCurrentUser code found in $_ moduleā€);
next;
}
}
— 500,522 ----

  # Since this needs loading, no matter what

! my $pi;
! foreach $pi (@RT::MailPlugins) {
my $Code;
my $NewAuthStat;
! if ( ref($pi) eq ā€œCODEā€ ) {
! $Code = $pi;
}
else {
! $pi = ā€œRT::Interface::email::$piā€ unless $pi =~ m/^RT::Interface::email::/;
! eval ā€œrequire $pi;ā€;
if ($@) {
! die (ā€œCouldn’t load module $pi: $@ā€);
next;
}
no strict ā€˜refs’;
! if ( !defined( $Code = *{ $pi . ā€œ::GetCurrentUserā€ }{CODE} ) ) {
! die (ā€œNo GetCurrentUser code found in $pi moduleā€);
next;
}
}
*** lib/RT/Interface/Email/Filter/SpamAssassin.pm.orig Fri Jan 2 17:55:55 2004
— lib/RT/Interface/Email/Filter/SpamAssassin.pm Mon May 3 20:15:40 2004
*** 27,41 ****
my $spamtest = Mail::SpamAssassin->new();

sub GetCurrentUser {
! my $item = shift;
! my $status = $spamtest->check ($item);
! return (undef, 0) unless $status->is_spam();
! eval { $status->rewrite_mail() };
! if ($status->get_hits > $status->get_required_hits()*1.5) {
# Spammy indeed
! return (undef, -1);
}
! return (undef, 0);
}

=head1 NAME
— 27,48 ----
my $spamtest = Mail::SpamAssassin->new();

sub GetCurrentUser {
! my $result = 0;
! my %args = ( Message => undef,
! @_ );
! my $status = $spamtest->check_message_text($args{ā€˜Message’}->as_string());
! if ($status->is_spam() &&
! $status->get_hits() > $status->get_required_hits()*1.5) {
# Spammy indeed
! my $mid = $args{ā€˜Message’}->head()->get(ā€˜Message-Id’);
! my $hits = $status->get_hits();
! $RT::Logger->info(ā€œFilter::SpamAssassin: Dropping message $mid because spam score = $hitsā€);
! $result = -1;
! } else {
! $args{ā€˜Message’}->head()->replace(ā€˜X-RT-SpamAssassin-Score’, sprintf("%.3f", $status->get_hits()));
}
! $status->finish();
! return (undef, $result);
}

=head1 NAME

There was another tricky bug: apparently SpamAssassin munges the $_
global variable when a message rings the spam bell, such that its value
in the Gateway function’s loop around @RT::MailPlugins gets unset. This
clears the plugin entry so it breaks on the next incoming email. I know
the functions/operators that set $_ are supposed have local effects but
apparently something doesn’t. Switching to an explicit loop variable
fixed the symptom, so that patch is included.

Here’s an example to share a legit use of local:

$_=3;
{
 local $_;
 something_that_modifies_dollar_underbar();
}
# $_ == 3