spamassassin updates

refactored into small subs with unit tests.
parse SA header with split instead of regexp (more reliable)
store SA results in a 'spamassassin' transaction note
add strict and warnings pragma
renamed reject_threshold -> reject (backwards compatible)
added relayclient skip option and POD. Skips SA processing when relayclient is set
added MULTIPLE RECIPIENT BEHAVIOR topic to POD
This commit is contained in:
Matt Simerson 2012-05-06 02:01:00 -04:00 committed by Robert
parent 5285774285
commit d644c24c83
2 changed files with 540 additions and 143 deletions

View File

@ -11,6 +11,10 @@ from the SpamAssassin package. F<http://www.spamassassin.org>
SpamAssassin 2.6 or newer is required. SpamAssassin 2.6 or newer is required.
Stores the results in a note named spamassassin (for other plugins). The note
is a hashref with whatever fields are defined in your spamassassin config.
These are the common ones: score,required,autolearn,tests,version
=head1 CONFIG =head1 CONFIG
Configured in the plugins file without any parameters, the Configured in the plugins file without any parameters, the
@ -24,11 +28,11 @@ The format goes like
Options being those listed below and the values being parameters to Options being those listed below and the values being parameters to
the options. Confused yet? :-) It looks like this in practice: the options. Confused yet? :-) It looks like this in practice:
spamassassin reject_threshold 7 leave_old_headers keep spamassassin reject 7 leave_old_headers keep
=over 4 =over 4
=item reject_threshold [threshold] =item reject [threshold]
Set the threshold where the plugin will reject the mail. Some Set the threshold where the plugin will reject the mail. Some
mail servers are so useless that they ignore 55x responses not coming mail servers are so useless that they ignore 55x responses not coming
@ -75,175 +79,328 @@ what you are doing, you can also leave them intact (parameter 'keep').
The username to pass to spamd, if different from the user qpsmtpd runs as. The username to pass to spamd, if different from the user qpsmtpd runs as.
=item relayclient skip
What special treatment is offered to connection with relay permission? Relay
permissions are granted when the connecting IP is listed in the relayclients
file and/or when the user has authenticated. The only valid option at present
is 'skip', which skips SA scoring.
If SpamAssasin has certain network tests enabled, users may get elevated spam
scores because their dynamic IP space is properly listed on DUL blocking lists.
If the user is authenticated or coming from a trusted IP, odds are we don't
want to be reject their messages. Especially when running qpsmtpd on port 587.
=back =back
With both of the first options the configuration line will look like the following With both of the first options the configuration line will look like the following
spamasssasin reject_threshold 18 munge_subject_threshold 8 spamasssasin reject 18 munge_subject_threshold 8
=head1 MULTIPLE RECIPIENT BEHAVIOR
This plugin supports per-user SpamAssassin preferences. When per-user SA prefs
are enabled (by setting spamd_user = vpopmail), the message recipient is used
as the spamd username. If SpamAssassin has per-user preferences enabled, it
will consult the users spam preferences when scoring the message.
When a message has multiple recipients, we do not change the spamd username.
The message is still scored by SA, but per-user preferences are not
consulted. To aid in debugging, messages with multiple recipents will
have an X-Spam-User header inserted. Admins and savvy users can look for
that header to confirm the reason their personal prefs were not consulted.
To get per-user SA prefs to work for messages with multiple recipients, the
LDA should be configured to check for the presence of the X-Spam-User header.
If the X-Spam-User header is present, the LDA should submit the message to
spamd for re-processing with the recipients address.
=head1 TODO =head1 TODO
Make the "subject munge string" configurable Make the "subject munge string" configurable
=head1 CHANGES
2012.04.02 - Matt Simerson
* refactored for ease of maintenance
* added support for per-user SpamAssassin preferences
* updated get_spam_results so that score=N.N works (as well as hits=N.N)
* rewrote the X-Spam-* header additions so that SA generated headers are
not discarded. Admin can alter SA headers with add_header in their SA
config. Subverting their changes there is unexpected. Making them read
code to figure out why is an unnecessary hurdle.
* added assemble_message, so we can calc content size which spamd wants
=cut =cut
use strict;
use warnings;
use Qpsmtpd::Constants;
use Qpsmtpd::DSN; use Qpsmtpd::DSN;
use Socket qw(:DEFAULT :crlf); use Socket qw(:DEFAULT :crlf);
use IO::Handle; use IO::Handle;
sub register { sub register {
my ($self, $qp, @args) = @_; my ($self, $qp, %args) = @_;
$self->log(LOGERROR, "Bad parameters for the spamassassin plugin") $self->log(LOGERROR, "Bad parameters for the spamassassin plugin") if @_ % 2;
if @_ % 2;
%{$self->{_args}} = @args; $self->{_args} = { %args };
$self->register_hook("data_post", "check_spam_reject") # backwards compatibility with previous config syntax
if $self->{_args}->{reject_threshold}; if ( ! defined $self->{_args}{reject} && defined $self->{_args}{reject_threshold} ) {
$self->{_args}{reject} = $self->{_args}{reject_threshold};
$self->register_hook("data_post", "check_spam_munge_subject") };
if $self->{_args}->{munge_subject_threshold};
$self->register_hook('data_post', 'check_spam_reject');
$self->register_hook('data_post', 'check_spam_munge_subject');
} }
sub hook_data_post { # check_spam sub hook_data_post {
my ($self, $transaction) = @_; my ($self, $transaction) = @_;
$self->log(LOGDEBUG, "check_spam"); if ( $transaction->data_size > 500_000 ) {
return (DECLINED) if $transaction->data_size > 500_000; $self->log(LOGINFO, "skip: too large (".$transaction->data_size.")");
return (DECLINED);
};
if ( $self->{_args}{relayclient} && $self->{_args}{relayclient} eq 'skip'
&& $self->qp->connection->relay_client() ) {
$self->log(LOGINFO, "skip: relayclient" );
return (DECLINED);
};
my $SPAMD = $self->connect_to_spamd() or return (DECLINED);
my $username = $self->select_spamd_username( $transaction );
my $message = $self->assemble_message($transaction);
my $length = length $message;
$self->print_to_spamd( $SPAMD, $message, $length, $username );
shutdown($SPAMD, 1); # close our side of the socket (tell spamd we're done)
my $headers = $self->parse_spamd_response( $SPAMD ) or return (DECLINED);
$self->insert_spam_headers( $transaction, $headers, $username );
return (DECLINED);
};
sub select_spamd_username {
my ($self, $transaction) = @_;
my $username = $self->{_args}{spamd_user} || getpwuid($>);
my $recipient_count = scalar $transaction->recipients;
if ( $recipient_count > 1 ) {
$self->log(LOGDEBUG, "Message has $recipient_count recipients");
return $username;
};
if ( $username eq 'vpopmail' ) {
# use the recipients email address as username. This enables per-user SA prefs
$username = ($transaction->recipients)[0]->address;
}
else {
$self->log(LOGDEBUG, "skipping per-user SA prefs");
};
return $username;
};
sub parse_spamd_response {
my ( $self, $SPAMD ) = @_;
my $line0 = <$SPAMD>; # get the first protocol line
if ( $line0 !~ /EX_OK/ ) {
$self->log(LOGERROR, "invalid response from spamd: $line0");
return;
};
my (%new_headers, $last_header);
while (<$SPAMD>) {
s/[\r\n]//g;
if ( m/^(X-Spam-.*?): (.*)?/ ) {
$new_headers{$1} = $2 || '';
$last_header = $1;
next;
}
if ( $last_header && m/^(\s+.*)/ ) { # a folded line, append to last
$new_headers{$last_header} .= CRLF . "\t" . $1;
next;
}
$last_header = undef;
}
close $SPAMD;
$self->log(LOGDEBUG, "finished reading from spamd");
return scalar keys %new_headers ? \%new_headers : undef;
};
sub insert_spam_headers {
my ( $self, $transaction, $new_headers, $username ) = @_;
my $recipient_count = scalar $transaction->recipients;
$self->_cleanup_spam_header($transaction, 'X-Spam-User'); # always clean up
if ( $recipient_count > 1 ) { # add for multiple recipients
$transaction->header->add('X-Spam-User', $username . ", $recipient_count recipients", 0);
};
foreach my $name ( keys %$new_headers ) {
next if $name eq 'X-Spam-Prev-Subject'; # might exist if SA rewrote subject
if ( $name eq 'X-Spam-Report' ) {
next; # Mail::Header mangles this prefolded header
# $self->log(LOGDEBUG, $new_headers->{$name} );
};
if ( $name eq 'X-Spam-Status' ) {
$self->parse_spam_header( $new_headers->{$name} );
};
$new_headers->{$name} =~ s/\015//; # hack for outlook
$self->_cleanup_spam_header($transaction, $name);
$transaction->header->add($name, $new_headers->{$name}, 0);
};
}
sub assemble_message {
my ($self, $transaction) = @_;
$transaction->body_resetpos;
my $message = "X-Envelope-From: "
. $transaction->sender->format . "\n"
. $transaction->header->as_string . "\n\n";
while (my $line = $transaction->body_getline) { $message .= $line; };
$message = join(CRLF, split/\n/, $message);
return $message . CRLF;
};
sub connect_to_spamd {
my $self = shift;
my $socket = $self->{_args}{spamd_socket};
my $SPAMD;
if ( $socket && $socket =~ /\// ) { # file path
$SPAMD = $self->connect_to_spamd_socket( $socket );
}
else {
$SPAMD = $self->connect_to_spamd_tcpip( $socket );
};
return if ! $SPAMD;
$SPAMD->autoflush(1);
return $SPAMD;
};
sub connect_to_spamd_socket {
my ( $self, $socket ) = @_;
if ( ! $socket || $socket !~ /^([\w\/.-]+)$/ ) { # Unix Domain Socket
$self->log(LOGERROR, "not a valid path");
return;
};
socket(my $SPAMD, PF_UNIX, SOCK_STREAM, 0) or do {
$self->log(LOGERROR, "Could not open socket: $!");
return;
};
my $paddr = sockaddr_un( $socket );
connect($SPAMD, $paddr) or do {
$self->log(LOGERROR, "Could not connect to spamd socket: $!");
return;
};
$self->log(LOGDEBUG, "connected to spamd");
return $SPAMD;
};
sub connect_to_spamd_tcpip {
my ( $self, $socket ) = @_;
my $remote = 'localhost'; my $remote = 'localhost';
my $port = 783; my $port = 783;
if (defined $self->{_args}->{spamd_socket}
&& $self->{_args}->{spamd_socket} =~ /^([\w.-]+):(\d+)$/) { if (defined $socket && $socket =~ /^([\w.-]+):(\d+)$/) {
$remote = $1; $remote = $1;
$port = $2; $port = $2;
} }
if ($port =~ /\D/) { $port = getservbyname($port, 'tcp') } if ($port =~ /\D/) { $port = getservbyname($port, 'tcp') };
die "No port" unless $port; if ( ! $port ) {
my $iaddr = inet_aton($remote) or $self->log(LOGERROR, "No spamd port, check your spamd_socket config.");
$self->log(LOGERROR, "Could not resolve host: $remote") and return (DECLINED); return;
};
my $iaddr = inet_aton($remote) or do {
$self->log(LOGERROR, "Could not resolve host: $remote");
return;
};
my $paddr = sockaddr_in($port, $iaddr); my $paddr = sockaddr_in($port, $iaddr);
my $proto = getprotobyname('tcp'); my $proto = getprotobyname('tcp');
if ($self->{_args}->{spamd_socket} and
$self->{_args}->{spamd_socket} =~ /^([\w\/.-]+)$/ ) { # connect to Unix Domain Socket
my $spamd_socket = $1;
socket(SPAMD, PF_UNIX, SOCK_STREAM, 0) socket(my $SPAMD, PF_INET, SOCK_STREAM, $proto) or do {
or $self->log(LOGERROR, "Could not open socket: $!") and return (DECLINED); $self->log(LOGERROR, "Could not open socket: $!");
return;
};
$paddr = sockaddr_un($spamd_socket); connect($SPAMD, $paddr) or do {
} $self->log(LOGERROR, "Could not connect to spamd: $!");
else { return;
socket(SPAMD, PF_INET, SOCK_STREAM, $proto) };
or $self->log(LOGERROR, "Could not open socket: $!") and return (DECLINED);
}
connect(SPAMD, $paddr) $self->log(LOGDEBUG, "connected to spamd");
or $self->log(LOGERROR, "Could not connect to spamassassin daemon: $!") and return DECLINED; return $SPAMD;
$self->log(LOGDEBUG, "check_spam: connected to spamd"); };
SPAMD->autoflush(1); sub print_to_spamd {
my ( $self, $SPAMD, $message, $length, $username ) = @_;
$transaction->body_resetpos; print $SPAMD "HEADERS SPAMC/1.4" . CRLF;
my $username = $self->{_args}->{spamd_user} || getpwuid($>); print $SPAMD "Content-length: $length" . CRLF;
print $SPAMD "User: $username" . CRLF;
print $SPAMD CRLF;
print $SPAMD $message or $self->log(LOGWARN, "Could not print to spamd: $!");
print SPAMD "SYMBOLS SPAMC/1.3" . CRLF;
print SPAMD "User: $username" . CRLF;
# Content-Length:
print SPAMD CRLF;
# or CHECK or REPORT or SYMBOLS
print SPAMD "X-Envelope-From: ", $transaction->sender->format, CRLF
or $self->log(LOGWARN, "Could not print to spamd: $!");
print SPAMD join CRLF, split /\n/, $transaction->header->as_string
or $self->log(LOGWARN, "Could not print to spamd: $!");
print SPAMD CRLF
or $self->log(LOGWARN, "Could not print to spamd: $!");
while (my $line = $transaction->body_getline) {
chomp $line;
print SPAMD $line, CRLF
or $self->log(LOGWARN, "Could not print to spamd: $!");
}
print SPAMD CRLF;
shutdown(SPAMD, 1);
$self->log(LOGDEBUG, "check_spam: finished sending to spamd"); $self->log(LOGDEBUG, "check_spam: finished sending to spamd");
my $line0 = <SPAMD>; # get the first protocol lines out };
if ($line0) {
$line0 =~ s/\r?\n$//;
$self->log(LOGDEBUG, "check_spam: spamd: $line0");
$self->_cleanup_spam_header($transaction, 'X-Spam-Check-By');
$transaction->header->add("X-Spam-Check-By", $self->qp->config('me'), 0);
}
my ($flag, $hits, $required);
while (<SPAMD>) {
s/\r?\n$//;
$self->log(LOGDEBUG, "check_spam: spamd: $_");
#warn "GOT FROM SPAMD1: $_";
last unless m/\S/;
if (m{Spam: (True|False) ; (-?\d+\.\d) / (-?\d+\.\d)}) {
($flag, $hits, $required) = ($1, $2, $3);
}
}
my $tests = <SPAMD>|| '';
close SPAMD;
$tests =~ s/\015//; # hack for outlook
$flag = $flag eq 'True' ? 'Yes' : 'No';
$self->log(LOGDEBUG, "check_spam: finished reading from spamd");
$self->_cleanup_spam_header($transaction, 'X-Spam-Flag');
$self->_cleanup_spam_header($transaction, 'X-Spam-Status');
$self->_cleanup_spam_header($transaction, 'X-Spam-Level');
$transaction->header->add('X-Spam-Flag', 'YES', 0) if ($flag eq 'Yes');
$transaction->header->add('X-Spam-Status',
"$flag, hits=$hits required=$required\n" .
"\ttests=$tests", 0);
my $length = int($hits);
$length = 1 if $length < 1;
$length = 50 if $length > 50;
$transaction->header->add('X-Spam-Level', '*' x $length, 0);
$self->log(LOGNOTICE, "check_spam: $flag, hits=$hits, required=$required, " .
"tests=$tests");
return (DECLINED);
}
sub check_spam_reject { sub check_spam_reject {
my ($self, $transaction) = @_; my ($self, $transaction) = @_;
$self->log(LOGDEBUG, "check_spam_reject: reject_threshold=" . $self->{_args}->{reject_threshold}); my $sa_results = $self->get_spam_results($transaction) or do {
my $score = $self->get_spam_score($transaction) or return DECLINED; $self->log(LOGNOTICE, "skip: no spamassassin results");
$self->log(LOGDEBUG, "check_spam_reject: score=$score"); return DECLINED;
};
my $score = $sa_results->{score} or do {
$self->log(LOGERROR, "skip: error getting spamassassin score");
return DECLINED;
};
my $reject = $self->{_args}{reject} or do {
$self->log(LOGERROR, "skip: reject threshold not set, default pass ($score)");
return DECLINED;
};
if ( $score < $reject ) {
$self->log(LOGINFO, "pass, $score < $reject");
return DECLINED;
};
# default of media_unsupported is DENY, so just change the message # default of media_unsupported is DENY, so just change the message
return Qpsmtpd::DSN->media_unsupported("spam score exceeded threshold") $self->log(LOGINFO, "deny, $score > $reject");
if $score >= $self->{_args}->{reject_threshold}; return Qpsmtpd::DSN->media_unsupported("spam score exceeded threshold");
$self->log(LOGDEBUG, "check_spam_reject: passed");
return DECLINED;
} }
sub check_spam_munge_subject { sub check_spam_munge_subject {
my ($self, $transaction) = @_; my ($self, $transaction) = @_;
my $score = $self->get_spam_score($transaction) or return DECLINED;
return DECLINED unless $score >= $self->{_args}->{munge_subject_threshold}; my $qp_num = $self->{_args}{munge_subject_threshold};
my $sa = $self->get_spam_results($transaction) or return DECLINED;
my $required = $sa->{required} || $qp_num or do {
$self->log(LOGDEBUG, "skipping munge, no user or qpsmtpd pref set");
return DECLINED;
};
return DECLINED unless $sa->{score} > $required;
my $subject_prefix = $self->qp->config('subject_prefix') || '*** SPAM ***'; my $subject_prefix = $self->qp->config('subject_prefix') || '*** SPAM ***';
my $subject = $transaction->header->get('Subject') || ''; my $subject = $transaction->header->get('Subject') || '';
@ -252,19 +409,57 @@ sub check_spam_munge_subject {
return DECLINED; return DECLINED;
} }
sub get_spam_score { sub get_spam_results {
my ($self, $transaction) = @_; my ($self, $transaction) = @_;
my $status = $transaction->header->get('X-Spam-Status') or return;
my ($score) = ($status =~ m/hits=(-?\d+\.\d+)/)[0]; if ( defined $transaction->notes('spamassassin') ) {
return $score; return $transaction->notes('spamassassin');
};
my $header = $transaction->header->get('X-Spam-Status') or return;
my $r = $self->parse_spam_header( $header );
$self->log(LOGDEBUG, "$r->{is_spam}, $r->{score}");
$transaction->notes('spamassassin', $r);
return $r;
} }
sub parse_spam_header {
my ($self, $string) = @_;
# the X-Spam-Score header contents vary based on the settings in
# the spamassassin *.cf files. Rather than parse via regexp, split
# on the consistent whitespace and = delimiters. More reliable and
# likely faster.
my @parts = split(/\s+/, $string);
my $is_spam = shift @parts;
chomp @parts;
chop $is_spam; # remove trailing ,
my %r;
foreach ( @parts ) {
my ($key,$val) = split(/=/, $_);
$r{$key} = $val;
}
$r{is_spam} = $is_spam;
# backwards compatibility for SA versions < 3
if ( defined $r{hits} && ! defined $r{score} ) {
$r{score} = delete $r{hits};
};
return \%r;
};
sub _cleanup_spam_header { sub _cleanup_spam_header {
my ($self, $transaction, $header_name) = @_; my ($self, $transaction, $header_name) = @_;
my $action = lc($self->{_args}->{leave_old_headers}) || 'rename'; my $action = 'rename';
if ( $self->{_args}->{leave_old_headers} ) {
$action = lc($self->{_args}->{leave_old_headers});
};
return unless $action eq 'drop' or $action eq 'rename'; return unless $action eq 'drop' || $action eq 'rename';
my $old_header_name = $header_name; my $old_header_name = $header_name;
$old_header_name = ($old_header_name =~ s/^X-//) ? "X-Old-$old_header_name" : "Old-$old_header_name"; $old_header_name = ($old_header_name =~ s/^X-//) ? "X-Old-$old_header_name" : "Old-$old_header_name";

202
t/plugin_tests/spamassassin Normal file
View File

@ -0,0 +1,202 @@
#!perl -w
use strict;
use warnings;
use Mail::Header;
use Qpsmtpd::Address;
use Qpsmtpd::Constants;
my @sample_headers = (
'No, score=-5.4 required=4.0 autolearn=ham',
'No, score=-8.2 required=4.0 autolearn=ham',
'No, score=-102.3 required=4.0 autolearn=disabled',
'No, score=-0.1 required=5.0 tests=AWL,BAYES_00,FREEMAIL_FROM,HTML_MESSAGE,RCVD_IN_DNSWL_NONE,RDNS_NONE autolearn=no version=3.3.2',
'No, score=4.4 required=5.0 autolearn=no',
'Yes, score=14.3 required=5.0 autolearn=no',
'Yes, score=18.3 required=5.0 autolearn=spam',
'Yes, score=26.6 required=4.0 autolearn=unavailable',
'No, score=-1.7 required=4.0 autolearn=unavailable version=3.3.2',
'No, hits=-1.0 required=4.0 autolearn=unavailable version=3.3.2',
);
sub register_tests {
my $self = shift;
$self->register_test('test_connect_to_spamd', 4);
$self->register_test('test_parse_spam_header', 10);
$self->register_test('test_get_spam_results', 19);
$self->register_test('test_check_spam_munge_subject', 4);
$self->register_test('test_check_spam_reject', 2);
}
sub test_connect_to_spamd {
my $self = shift;
my $transaction = $self->qp->transaction;
$transaction->add_recipient( Qpsmtpd::Address->new( '<user@example.com>' ) );
my $username = $self->select_spamd_username( $transaction );
my $message = $self->test_message();
my $length = length $message;
# Try a unix socket
$self->{_args}{spamd_socket} = '/var/run/spamd/spamd.socket';
my $SPAMD = $self->connect_to_spamd();
if ( $SPAMD ) {
ok( $SPAMD, "connect_to_spamd, socket");
$self->print_to_spamd( $SPAMD, $message, $length, $username );
shutdown($SPAMD, 1); # close our side of the socket (tell spamd we're done)
my $headers = $self->parse_spamd_response( $SPAMD );
#warn Data::Dumper::Dumper($headers);
ok( $headers, "connect_to_spamd, socket response\n");
}
else {
ok( 1 == 1, "connect_to_spamd, socket connect FAILED");
ok( 1 == 1, "connect_to_spamd, socket response FAILED");
};
# Try a TCP/IP connection
$self->{_args}{spamd_socket} = '127.0.0.1:783';
$SPAMD = $self->connect_to_spamd();
if ( $SPAMD ) {
ok( $SPAMD, "connect_to_spamd, tcp/ip");
#warn Data::Dumper::Dumper($SPAMD);
$self->print_to_spamd( $SPAMD, $message, $length, $username );
shutdown($SPAMD, 1); # close our side of the socket (tell spamd we're done)
my $headers = $self->parse_spamd_response( $SPAMD );
#warn Data::Dumper::Dumper($headers);
ok( $headers, "connect_to_spamd, tcp/ip response\n");
}
else {
ok( 1 == 1, "connect_to_spamd, tcp/ip connect FAILED");
ok( 1 == 1, "connect_to_spamd, tcp/ip response FAILED");
};
};
sub test_check_spam_reject {
my $self = shift;
my $transaction = $self->qp->transaction;
$self->setup_headers();
# message scored a 10, should pass
$self->{_args}{reject} = 12;
$transaction->notes('spamassassin', { score => 10 } );
my $r = $self->check_spam_reject($transaction);
cmp_ok( DECLINED, '==', $r, "check_spam_reject, $r");
# message scored a 15, should fail
$self->{_args}{reject} = 12;
$transaction->notes('spamassassin', { score => 15 } );
($r) = $self->check_spam_reject($transaction);
cmp_ok( DENY, '==', $r, "check_spam_reject, $r");
};
sub test_check_spam_munge_subject {
my $self = shift;
my $transaction = $self->qp->transaction;
$self->setup_headers();
my $subject = 'DSPAM smells better than SpamAssassin';
$self->{_args}{munge_subject_threshold} = 5;
$transaction->notes('spamassassin', { score => 6 } );
$transaction->header->add('Subject', $subject);
$self->check_spam_munge_subject($transaction);
my $r = $transaction->header->get('Subject'); chomp $r;
cmp_ok($r, 'eq', "*** SPAM *** $subject", "check_spam_munge_subject +");
$transaction->header->delete('Subject'); # cleanup
$self->{_args}{munge_subject_threshold} = 5;
$transaction->notes('spamassassin', { score => 3 } );
$transaction->header->add('Subject', $subject);
$self->check_spam_munge_subject($transaction);
$r = $transaction->header->get('Subject'); chomp $r;
cmp_ok($r, 'eq', $subject, "check_spam_munge_subject -");
$transaction->header->delete('Subject'); # cleanup
$transaction->notes('spamassassin', { score => 3, required => 4 } );
$transaction->header->add('Subject', $subject);
$self->check_spam_munge_subject($transaction);
$r = $transaction->header->get('Subject'); chomp $r;
cmp_ok($r, 'eq', $subject, "check_spam_munge_subject -");
$transaction->header->delete('Subject'); # cleanup
$transaction->notes('spamassassin', { score => 5, required => 4 } );
$transaction->header->add('Subject', $subject);
$self->check_spam_munge_subject($transaction);
$r = $transaction->header->get('Subject'); chomp $r;
cmp_ok($r, 'eq', "*** SPAM *** $subject", "check_spam_munge_subject +");
};
sub test_get_spam_results {
my $self = shift;
my $transaction = $self->qp->transaction;
$self->setup_headers();
foreach my $h ( @sample_headers ) {
$transaction->notes('spamassassin', undef); # empty cache
$transaction->header->delete('X-Spam-Status'); # delete previous header
$transaction->header->add('X-Spam-Status', $h);
my $r_ref = $self->get_spam_results($transaction);
if ( $h =~ /hits=/ ) {
$r_ref->{hits} = delete $r_ref->{score}; # SA v2 compat
};
my $r2 = _reassemble_header($r_ref);
cmp_ok( $h, 'eq', $r2, "get_spam_results ($h)" );
# this time it should be cached
$r_ref = $self->get_spam_results($transaction);
next if $h =~ /hits=/; # caching is broken for SA v2 headers
$r2 = _reassemble_header($r_ref);
cmp_ok( $h, 'eq', $r2, "get_spam_results ($h)" );
};
};
sub test_parse_spam_header {
my $self = shift;
foreach my $h ( @sample_headers ) {
my $r_ref = $self->parse_spam_header($h);
if ( $h =~ /hits=/ ) {
$r_ref->{hits} = delete $r_ref->{score}; # SA v2 compat
};
my $r2 = _reassemble_header($r_ref);
cmp_ok( $h, 'eq', $r2, "parse_spam_header ($h)" );
};
};
sub setup_headers {
my $self = shift;
my $transaction = $self->qp->transaction;
my $header = Mail::Header->new(Modify => 0, MailFrom => "COERCE");
$transaction->header( $header );
};
sub test_message {
return <<'EO_MESSAGE'
To: Fictitious User <fict@example.com>
From: No Such <such@example.com>
Subject: jose can you see, by the dawns early light?
What so proudly we.
EO_MESSAGE
};
sub _reassemble_header {
my $info_ref = shift;
my $string = $info_ref->{'is_spam'};
$string .= ",";
foreach ( qw/ hits score required tests autolearn version / ) {
next if ! defined $info_ref->{$_};
$string .= " $_=$info_ref->{$_}";
};
return $string;
};