|
|
@@ -19,6 +19,7 @@ use strict; |
|
|
|
use Mail::SpamAssassin;
|
|
|
|
use Mail::SpamAssassin::Plugin;
|
|
|
|
use Mail::SpamAssassin::PerMsgStatus;
|
|
|
|
use Mail::SpamAssassin::Logger;
|
|
|
|
use IO::Socket;
|
|
|
|
use IO::File;
|
|
|
|
|
|
|
@@ -27,7 +28,6 @@ our @ISA = qw(Mail::SpamAssassin::Plugin); |
|
|
|
|
|
|
|
# Convenience variables and pseudo-constants
|
|
|
|
my $CRLF = "\x0d\x0a";
|
|
|
|
my $DefaultMaxTempFileSize = 64 * 1024;
|
|
|
|
|
|
|
|
# translation table for SNF rule codes
|
|
|
|
my $rule_code_xlat = {
|
|
|
@@ -75,11 +75,309 @@ sub new { |
|
|
|
$self->{Temp_Dir} = '/tmp/snf4sa';
|
|
|
|
|
|
|
|
# Maximum email message size (including headers).
|
|
|
|
$self->{SNF_MaxTempFileSize} = $DefaultMaxTempFileSize;
|
|
|
|
$self->{SNF_MaxTempFileSize} = 64 * 1024;
|
|
|
|
|
|
|
|
# Key for GBUdb maximum weight.
|
|
|
|
$self->{GBUdb_MaxWeightKey} = "gbudb_max_weight";
|
|
|
|
|
|
|
|
# Key for SNFServer code in configuration file.
|
|
|
|
$self->{SNF_CodeKey} = "snf_result";
|
|
|
|
|
|
|
|
# Key for SA score increment in configuration file.
|
|
|
|
$self->{SA_DeltaScoreKey} = "sa_score";
|
|
|
|
|
|
|
|
# Key for short circuit in configuration file.
|
|
|
|
$self->{SA_ShortCircuitYesKey} = "short_circuit_yes";
|
|
|
|
|
|
|
|
# Key for no short circuit in configuration file.
|
|
|
|
$self->{SA_ShortCircuitNoKey} = "short_circuit_no";
|
|
|
|
|
|
|
|
return $self;
|
|
|
|
}
|
|
|
|
|
|
|
|
# DEBUG/TEST.
|
|
|
|
#sub extract_metadata {
|
|
|
|
#
|
|
|
|
# my ($self, $opts) = @_;
|
|
|
|
#
|
|
|
|
# print "***********************\n";
|
|
|
|
# print "extract_metadata called\n";
|
|
|
|
# print "***********************\n";
|
|
|
|
#
|
|
|
|
# $opts->{msg}->put_metadata("X-Extract-Metadata:", "Test header");
|
|
|
|
#
|
|
|
|
#}
|
|
|
|
# END OF DEBUG/TEST.
|
|
|
|
|
|
|
|
sub have_shortcircuited {
|
|
|
|
|
|
|
|
my ($self, $permsgstatus) = @_;
|
|
|
|
|
|
|
|
# print "************************************\n";
|
|
|
|
# print "****have_shortcircuited returning 0\n";
|
|
|
|
# print "************************************\n";
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub parse_config {
|
|
|
|
|
|
|
|
my ($self, $options) = @_;
|
|
|
|
|
|
|
|
# DEBUG.
|
|
|
|
#print "parse_confg. key: $options->{key}\n";
|
|
|
|
#print "parse_config. line: $options->{line}\n";
|
|
|
|
#print "parse_config. value: $options->{value}\n";
|
|
|
|
#END OF DEBUG.
|
|
|
|
|
|
|
|
# Process GBUdb_max_weight.
|
|
|
|
if (lc($options->{key}) eq $self->{GBUdb_MaxWeightKey}) {
|
|
|
|
|
|
|
|
# GBUdb maximum weight.
|
|
|
|
my $tempValue = $options->{value};
|
|
|
|
# Test that the value was a number.
|
|
|
|
#$self->log_debug("Found $self->{GBUdb_MaxWeightKey} . " value: $options->{value}, tempValue: $tempValue\n"; # DEBUG.
|
|
|
|
|
|
|
|
if ($tempValue =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/) {
|
|
|
|
|
|
|
|
# Value was a number. Load and return success.
|
|
|
|
$options->{conf}->{gbuDbMaxWeight} = $tempValue;
|
|
|
|
$self->inhibit_further_callbacks();
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
$self->log_debug("Invalid value for $self->{GBUdb_MaxWeightKey} " .
|
|
|
|
$tempValue);
|
|
|
|
|
|
|
|
}
|
|
|
|
} elsif (lc($options->{key}) eq $self->{SNF_CodeKey}) {
|
|
|
|
|
|
|
|
# Relationship between SNFServer code and SA score delta.
|
|
|
|
my $snf = $self->parse_snf_sa_mapping($options);
|
|
|
|
if (defined($snf)) {
|
|
|
|
my @codes = @{$snf->{snfCode}};
|
|
|
|
print "snf->{snfCode}: @codes\n";
|
|
|
|
print "snf->{deltaScore}: $snf->{deltaScore}\n";
|
|
|
|
print "snf->{shortCircuit}: $snf->{shortCircuit}\n";
|
|
|
|
|
|
|
|
# Save configuration.
|
|
|
|
|
|
|
|
# Successfully parsed.
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Wasn't handled.
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Parse a snf_result configuration line.
|
|
|
|
#
|
|
|
|
# Input--
|
|
|
|
#
|
|
|
|
# $line--String containing the snf_result line without the first word.
|
|
|
|
#
|
|
|
|
# Returns has reference with the following fields (if no error)--
|
|
|
|
#
|
|
|
|
# snfCode--Array of SNFServer result codes that this configuration
|
|
|
|
# line specifies.
|
|
|
|
#
|
|
|
|
# deltaScore--SA score increment for the codes in @snfCode.
|
|
|
|
#
|
|
|
|
# shortCircuit--True if a SNFServer code in @snfCode is to
|
|
|
|
# short-circuit the message scan, false otherwise.
|
|
|
|
#
|
|
|
|
# If the line cannot be parsed, the return value is undef.
|
|
|
|
#
|
|
|
|
sub parse_snf_sa_mapping
|
|
|
|
{
|
|
|
|
my ($self, $options) = @_;
|
|
|
|
|
|
|
|
my $value = $options->{value};
|
|
|
|
|
|
|
|
my $ret_hash = {
|
|
|
|
snfCode => undef,
|
|
|
|
deltaScore => undef,
|
|
|
|
shortCircuit => undef
|
|
|
|
};
|
|
|
|
|
|
|
|
# SNFServer codes found.
|
|
|
|
my @snfCode = ();
|
|
|
|
|
|
|
|
# Remove leading and trailing whitespace.
|
|
|
|
$value =~ s/^\s+//;
|
|
|
|
$value =~ s/\s+$//;
|
|
|
|
|
|
|
|
# Convert to lower case.
|
|
|
|
$value = lc($value);
|
|
|
|
|
|
|
|
# Split up by white space.
|
|
|
|
my @specVal = split(/\s+/, $value);
|
|
|
|
|
|
|
|
if (0 == @specVal) {
|
|
|
|
|
|
|
|
# No separate words.
|
|
|
|
$self->log_debug("No separate words found in configuration line '" .
|
|
|
|
$options->{line} . "'");
|
|
|
|
return undef;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Convert each SNFServer result specification into an integer.
|
|
|
|
my $lastSpec;
|
|
|
|
for ($lastSpec = 0; $lastSpec < @specVal; $lastSpec++) {
|
|
|
|
|
|
|
|
# Check for next keyword.
|
|
|
|
if ($specVal[$lastSpec] eq $self->{SA_DeltaScoreKey}) {
|
|
|
|
|
|
|
|
# We've completed the processing of the SNFServer result
|
|
|
|
# codes.
|
|
|
|
last;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Get the code values.
|
|
|
|
my @codeVal = $self->get_code_values($specVal[$lastSpec]);
|
|
|
|
if (0 == @codeVal) {
|
|
|
|
|
|
|
|
# No code values were obtained.
|
|
|
|
$self->log_debug("Couldn't parse all the SNFServer code values " .
|
|
|
|
"in configuration line '" .
|
|
|
|
$options->{line} . "'");
|
|
|
|
return undef;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Add to the list of codes.
|
|
|
|
@snfCode = (@snfCode, @codeVal);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Sort the SNFServer result codes and remove duplicates.
|
|
|
|
@snfCode = sort { $a <=> $b } @snfCode;
|
|
|
|
my $prev = -1;
|
|
|
|
my @temp = grep($_ != $prev && ($prev = $_), @snfCode);
|
|
|
|
$ret_hash->{snfCode} = \@temp;
|
|
|
|
|
|
|
|
# The $specVal[$lastSpec] is $self->{SA_DeltaScoreKey}. Return if
|
|
|
|
# there aren't enough parameters.
|
|
|
|
$lastSpec++;
|
|
|
|
if ($lastSpec >= @specVal) {
|
|
|
|
|
|
|
|
# Not enough parameters.
|
|
|
|
$self->log_debug("Not enough parameters in configuration line '" .
|
|
|
|
$options->{line} . "'");
|
|
|
|
return undef;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Extract the SA delta score.
|
|
|
|
$ret_hash->{deltaScore} = $specVal[$lastSpec];
|
|
|
|
if (!($ret_hash->{deltaScore} =~
|
|
|
|
/^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/)) {
|
|
|
|
|
|
|
|
# SA delta score isn't a number.
|
|
|
|
$self->log_debug("Value after '" . $self->{SA_DeltaScoreKey} .
|
|
|
|
"' ($specVal[$lastSpec]) must be a number " .
|
|
|
|
"in configuration line '" .
|
|
|
|
$options->{line} . "'");
|
|
|
|
return undef;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Get short circuit spec.
|
|
|
|
$lastSpec++;
|
|
|
|
$ret_hash->{shortCircuit} = 0;
|
|
|
|
if ( ($lastSpec + 1) == @specVal) {
|
|
|
|
|
|
|
|
# A parameter was specified.
|
|
|
|
my $shortCircuitSpec = $specVal[$lastSpec];
|
|
|
|
if ($self->{SA_ShortCircuitYesKey} eq $shortCircuitSpec) {
|
|
|
|
|
|
|
|
# Specified short-circuit evaluation.
|
|
|
|
$ret_hash->{shortCircuit} = 1;
|
|
|
|
|
|
|
|
} elsif ($self->{SA_ShortCircuitNoKey} ne $shortCircuitSpec) {
|
|
|
|
|
|
|
|
# Invalid short-circuit specification.
|
|
|
|
$self->log_debug("Invalid short-circuit specification: '" .
|
|
|
|
$specVal[$lastSpec] .
|
|
|
|
"' in configuration line '" . $options->{line} .
|
|
|
|
"'. Must be '$self->{SA_ShortCircuitYesKey}' " .
|
|
|
|
" or '$self->{SA_ShortCircuitNoKey}'.");
|
|
|
|
return undef;
|
|
|
|
|
|
|
|
}
|
|
|
|
} elsif ($lastSpec != @specVal) {
|
|
|
|
|
|
|
|
# Too many parameters were specified.
|
|
|
|
$self->log_debug("Too many parameters were specified in " .
|
|
|
|
"configuration line '" . $options->{line} . "'");
|
|
|
|
return undef;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return $ret_hash;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
sub get_code_values
|
|
|
|
{
|
|
|
|
my ($self, $specElement) = @_;
|
|
|
|
|
|
|
|
my @snfCode = ();
|
|
|
|
|
|
|
|
# Split the specification.
|
|
|
|
my @codeVal = split(/-/, $specElement);
|
|
|
|
|
|
|
|
#$self->log_debug("snf4sa: get_code_values. specElement: $specElement. codeVal: @codeVal"); # DEBUG
|
|
|
|
|
|
|
|
if (1 == @codeVal) {
|
|
|
|
|
|
|
|
if ($specElement =~ /^\d+$/) {
|
|
|
|
|
|
|
|
# Found a single code.
|
|
|
|
$snfCode[0] = 1 * $specElement;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} elsif (2 == @codeVal) {
|
|
|
|
|
|
|
|
# Check range.
|
|
|
|
if ( ($codeVal[0] =~ /^\d+$/) && ($codeVal[1] =~ /^\d+$/) ) {
|
|
|
|
|
|
|
|
# Found a range of codes.
|
|
|
|
$codeVal[0] = 1 * $codeVal[0];
|
|
|
|
$codeVal[1] = 1 * $codeVal[1];
|
|
|
|
if ($codeVal[0] <= $codeVal[1]) {
|
|
|
|
|
|
|
|
# Add these SNF codes.
|
|
|
|
for (my $i = $codeVal[0]; $i <= $codeVal[1]; $i++) {
|
|
|
|
|
|
|
|
push(@snfCode, $i);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return @snfCode;
|
|
|
|
}
|
|
|
|
|
|
|
|
# Output a debug message.
|
|
|
|
#
|
|
|
|
# Input--
|
|
|
|
#
|
|
|
|
# $message--String containing the message to output.
|
|
|
|
#
|
|
|
|
sub log_debug
|
|
|
|
{
|
|
|
|
my ($self, $message) = @_;
|
|
|
|
dbg("snf4sa: $message");
|
|
|
|
}
|
|
|
|
|
|
|
|
# Check the message with SNFServer.
|
|
|
|
sub snf4sa_sacheck {
|
|
|
|
|
|
|
|
my ($self, $permsgstatus, $fulltext) = @_;
|
|
|
@@ -121,6 +419,24 @@ sub snf4sa_sacheck { |
|
|
|
# xci_scan connects to SNFServer with XCI to scan the message
|
|
|
|
my $SNF_XCI_Return = $self->xci_scan( $filename );
|
|
|
|
|
|
|
|
#print "header:\n\n$SNF_XCI_Return->{header}"; # DEBUG
|
|
|
|
#print "\nEnd of header\n\n"; # DEBUG
|
|
|
|
|
|
|
|
# Initialize the change in the SA score.
|
|
|
|
my $deltaScore = 0.0;
|
|
|
|
|
|
|
|
# Perform GBUdb processing.
|
|
|
|
if (defined($permsgstatus->{main}->{conf}->{gbuDbMaxWeight})) {
|
|
|
|
|
|
|
|
#print "gbudbMaxWeight: $permsgstatus->{main}->{conf}->{gbuDbMaxWeight}\n\n"; # DEBUG.
|
|
|
|
|
|
|
|
# Calculate the contribution to the scrore from the GBUdb results.
|
|
|
|
$deltaScore +=
|
|
|
|
$self->calc_GBUdb($SNF_XCI_Return->{header},
|
|
|
|
$permsgstatus->{main}->{conf}->{gbuDbMaxWeight});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Remove the temp file, we are done with it.
|
|
|
|
unlink($filename);
|
|
|
|
|
|
|
@@ -154,9 +470,68 @@ sub snf4sa_sacheck { |
|
|
|
# Add the header.
|
|
|
|
$permsgstatus->set_tag("SNFRESULTTAG", "$rc ($rcx)");
|
|
|
|
|
|
|
|
#all SA cares about is whether a 0 or 1 comes back. 0 = good 1=spam.
|
|
|
|
return $testscore;
|
|
|
|
# Submit the score.
|
|
|
|
if ($deltaScore) {
|
|
|
|
|
|
|
|
$permsgstatus->got_hit("SNF4SA", "", score => $deltaScore);
|
|
|
|
for my $set (0..3) {
|
|
|
|
$permsgstatus->{scoreset}->[$set]->{"SNF4SA"} =
|
|
|
|
sprintf("%0.3f", $deltaScore);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
# Always return zero, since the score was submitted via got_hit()
|
|
|
|
# above.
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
sub calc_GBUdb
|
|
|
|
{
|
|
|
|
my ( $self, $headers, $weight ) = @_;
|
|
|
|
|
|
|
|
# Split the header into lines.
|
|
|
|
my @headerLine = split(/\n/, $headers);
|
|
|
|
|
|
|
|
# Find the line containing the GBUdb results.
|
|
|
|
my $line;
|
|
|
|
foreach $line (@headerLine) {
|
|
|
|
|
|
|
|
# Search for the tag.
|
|
|
|
if ($line =~ /^X-GBUdb-Analysis:/) {
|
|
|
|
|
|
|
|
# GBUdb analysis was done. Extract the values.
|
|
|
|
my $ind0 = index($line, "c=");
|
|
|
|
my $ind1 = index($line, " ", $ind0 + 2);
|
|
|
|
if (-1 == $ind0) {
|
|
|
|
|
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
my $c = 1.0 * substr($line, $ind0 + 2, $ind1 - $ind0 - 2);
|
|
|
|
print "calc_GBUdb. line: $line\n"; # DEBUG
|
|
|
|
print "calc_GBUdb. c: $c, ind0: $ind0, ind1: $ind1\n"; # DEBUG
|
|
|
|
$ind0 = index($line, "p=");
|
|
|
|
$ind1 = index($line, " ", $ind0 + 2);
|
|
|
|
if (-1 == $ind0) {
|
|
|
|
|
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
my $p = 1.0 * substr($line, $ind0 + 2, $ind1 - $ind0 - 2);
|
|
|
|
|
|
|
|
print "calc_GBUdb. p: $p, ind0: $ind0, ind1: $ind1\n"; # DEBUG
|
|
|
|
|
|
|
|
# Calculate and return the score.
|
|
|
|
my $score = ($p * $c);
|
|
|
|
$score *= $score * $weight;
|
|
|
|
if ($p < 0.0) {
|
|
|
|
$score *= -1.0;
|
|
|
|
}
|
|
|
|
print "calc_GBUdb. score: $score\n"; # DEBUG
|
|
|
|
return $score;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sub abort
|