?revision_form?Rev ?revision_input??revision_submit??revision_endform?
Rev 3 |
Go to most recent revision |
Blame |
Compare with Previous |
Last modification |
View Log
| RSS feed
package Greylisting
;
#
# $Id: Greylisting.pm,v 1.4 2006/01/11 17:17:28 marcmerlin Exp $
#
# General Greylisting Plugin, written by Marc MERLIN <marc_soft@merlins.org>
# (Kristopher Austin gets the credit for the original port to an SA 3.0 plugin)
#
# This was originally written to implement greylisting in SA-Exim, although
# I have tried to make it more general and allow for reuse in other MTAs
# (although they will need to
# 1) be running SA at SMTP time
# 2) Provide the list of rcpt to and env from in some headers for SA to read
# 3) Provide the IP of the connecting host )
#
# This rule should get a negative score so that if we've already seen the
# greylisting tuplet before, we lower the score, which hopefully brings us from
# a tempreject to an accept (at least that's how sa-exim does it)
#
# -- Marc 2004/01/19
use strict
;
use Mail
::SpamAssassin::Plugin;
our @ISA = qw(Mail
::SpamAssassin::Plugin);
sub new
{
my ($class, $mailsa) = @_;
$class = ref($class) || $class;
my $self = $class->SUPER::new($mailsa);
bless ($self, $class);
$self->register_eval_rule ("greylisting");
return $self;
}
sub check_end
{
my ($self, $permsgstatus) = @_;
if (not $self->{'rangreylisting'})
{
Mail
::SpamAssassin::Plugin::dbg("GREYLISTING: greylisting didn't run since the configuration wasn't setup to call us");
}
}
# Greylisting happens depending on the SA score, so we want to run it last,
# which is why we give it a high priority
sub greylisting
{
my ($self, $permsgstatus, $optionhash) = @_;
my $connectip;
my $envfrom;
my $rcptto;
my @rcptto;
my $iswhitelisted=0;
my $err;
my $mesgid = $permsgstatus->get('Message-Id')."\n";
my $mesgidfn;
my $tuplet;
my $sascore = $permsgstatus->get_score();
my $dontcheckscore;
my %option;
Mail
::SpamAssassin::Plugin::dbg("GREYLISTING: called function");
$optionhash =~ s/;/,/g;
# This is safe, right? (users shouldn't be able to set it in their config)
%option=eval $optionhash;
$self->{'rangreylisting'}=1;
foreach my $reqoption (qw ( method greylistsecs dontgreylistthreshold
connectiphdr envfromhdr rcpttohdr greylistnullfrom greylistfourthbyte
))
{
die "Greylist option $reqoption missing from SA config" unless (defined $option{$reqoption});
}
$dontcheckscore = $option{'dontgreylistthreshold'};
# No newlines, thank you (yes, you need this twice apparently)
chomp ($mesgid);
chomp ($mesgid);
# Newline in the middle mesgids, are you serious? Get rid of them here
$mesgid =~ s/\012/|/g;
# For stuff that we know is spam, don't greylist the host
# (that might help later spam with a lower score to come in)
if ($sascore >= $dontcheckscore)
{
Mail
::SpamAssassin::Plugin::dbg("GREYLISTING: skipping greylisting on $mesgid, since score is already $sascore and you configured greylisting not to bother with anything above $dontcheckscore");
return 0;
}
else
{
Mail
::SpamAssassin::Plugin::dbg("GREYLISTING: running greylisting on $mesgid, since score is too low ($sascore) and you configured greylisting to greylist anything under $dontcheckscore");
}
if (not $connectip = $permsgstatus->get($option{'connectiphdr'}))
{
warn "Couldn't get Connecting IP header $option{'connectiphdr'} for message $mesgid, skipping greylisting call\n";
return 0;
}
chomp($connectip);
# Clean up input (for security, if you use files/dirs)
$connectip =~ /([\d.:]+)/;
$connectip = ($1 or "");
# Account for a null envelope from
if (not defined ($envfrom = $permsgstatus->get($option{'envfromhdr'})))
{
warn "Couldn't get Envelope From header $option{'envfromhdr'} for message $mesgid, skipping greylisting call\n";
return 0;
}
chomp($envfrom);
# Clean up input (for security, if you use files/dirs)
$envfrom =~ s#/#-#g;
if (not $envfrom)
{
$envfrom="<>";
return 0 if (not $option{'greylistnullfrom'});
}
if (not $rcptto = $permsgstatus->get($option{'rcpttohdr'}))
{
warn "Couldn't get Rcpt To header $option{'rcpttohdr'} for message $mesgid, skipping greylisting call\n";
return 0;
}
chomp($rcptto);
# Clean up input (for security, if you use files/dirs)
$rcptto =~ s#/#-#g;
@rcptto = split(/, /, $rcptto);
umask 0007
;
foreach $rcptto (@rcptto)
{
# The dir method is easy to fiddle with and expire records in (with
# a find | rm) but it's probably more I/O extensive than a real DB
# and suffers from directory size problems if a specific IP is sending
# generating tens of thousands of tuplets. -- Marc
# That said, I prefer formats I can easily tinker with, and not having
# to worry about buggy locking and so forth
if ($option{'method'} eq "dir")
{
my $tmpvar;
# The clean strings are hardcoded because it's hard to do a variable
# substitution within a tr (and using the eval solution is too
# resource expensive)
# envfrom could be cleaned outside of the loop, but the other method
# options might now want that
$envfrom =~ tr/!#%()*+,-.0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~/_/c;
# clean variables to run properly under -T
$envfrom =~ /(.+)/;
$tmpvar = ($1 or "");
# work around bug in perl untaint in perl 5.8
$envfrom=undef;
$envfrom=$tmpvar;
$rcptto =~ tr/!#%()*+,-.0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~/_/c;
$rcptto =~ /(.+)/;
$tmpvar = ($1 or "");
$rcptto=undef;
$rcptto=$tmpvar;
die "greylist option dir not passed, even though method was set to dir" unless ($option{'dir'});
# connectip is supposed to be untainted now, but I was still getting
# some insecure dependecy error messages sometimes (perl 5.8 problem apparently)
$connectip =~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/;
my ($ipbyte1, $ipbyte2, $ipbyte3, $ipbyte4) = ($1, $2, $3, $4);
my $ipdir1 = "$option{'dir'}/$ipbyte1";
my $ipdir2 = "$ipdir1/$ipbyte2";
my $ipdir3 = "$ipdir2/$ipbyte3";
my $ipdir4;
my $tupletdir;
$ipdir4 = "$ipdir3";
$ipdir4 .= "/$ipbyte4" if ($option{'greylistfourthbyte'});
$tupletdir = "$ipdir4/$envfrom";
$tuplet = "$tupletdir/$rcptto";
# make directory whether it's there or not (faster than test and set)
mkdir $ipdir1;
mkdir $ipdir2;
mkdir $ipdir3;
mkdir $ipdir4;
mkdir $tupletdir;
if (not -e
$tuplet)
{
# If the tuplets aren't there, we create them and continue in
# case there are other ones (one of them might be whitelisted
# already)
$err="creating $tuplet";
open (TUPLET
, ">$tuplet") or goto greylisterror
;
print TUPLET
time."\n";
print TUPLET
"Status: Greylisted\n";
print TUPLET
"Last Message-Id: $mesgid\n";
print TUPLET
"Whitelisted Count: 0\n";
print TUPLET
"Query Count: 1\n";
print TUPLET
"SA Score: $sascore\n";
$err="closing first-written $tuplet";
close TUPLET
or goto greylisterror
;
}
else
{
my $time;
my $status;
my $whitelistcount;
my $querycount;
# Take into account race condition of expiring deletes and us
# running
$err="reading $tuplet";
open (TUPLET
, "<$tuplet") or goto greylisterror
;
$err="Couldn't read time";
defined ($time=<TUPLET>) or goto greylisterror
;
chomp ($time);
$err="Couldn't read status";
defined ($status=<TUPLET>) or goto greylisterror
;
chomp ($status);
$err="Couldn't extract Status from $status";
$status =~ s/^Status: // or goto greylisterror
;
# Skip Mesg-Id
$err="Couldn't skip Mesg-Id";
defined ($_=<TUPLET>) or goto greylisterror
;
$err="Couldn't read whitelistcount";
defined ($whitelistcount=<TUPLET>) or goto greylisterror
;
chomp ($whitelistcount);
$err="Couldn't extract Whitelisted Count from $whitelistcount";
$whitelistcount =~ s/^Whitelisted Count: // or goto greylisterror
;
$err="Couldn't read querycount";
defined ($querycount=<TUPLET>) or goto greylisterror
;
chomp ($querycount);
$err="Couldn't extract Query Count from $querycount";
$querycount =~ s/^Query Count: // or goto greylisterror
;
close (TUPLET
);
$querycount++;
if ((time - $time) > $option{'greylistsecs'})
{
$status="Whitelisted";
$whitelistcount++;
}
$err="re-writing $tuplet";
open (TUPLET
, ">$tuplet") or goto greylisterror
;
print TUPLET
"$time\n";
print TUPLET
"Status: $status\n";
print TUPLET
"Last Message-Id: $mesgid\n";
print TUPLET
"Whitelisted Count: $whitelistcount\n";
print TUPLET
"Query Count: $querycount\n";
print TUPLET
"SA Score: $sascore\n";
$err="closing re-written $tuplet";
close TUPLET
or goto greylisterror
;
# We continue processing the other recipients, to setup or
# update their counters
if ($status eq "Whitelisted")
{
$iswhitelisted=1;
}
}
}
elsif ($option{'method'} eq "file")
{
warn "codeme (file greylisting)\n";
}
elsif ($option{'method'} eq "db")
{
warn "codeme (db greylisting)\n";
}
}
Mail
::SpamAssassin::Plugin::dbg("GREYLISTING: computed greylisting on tuplet, saved info in $tuplet and whitelist status is $iswhitelisted");
return $iswhitelisted;
greylisterror
:
warn "Reached greylisterror: $err / $!";
# delete tuplet since it apparently had issues but don't check for errors
# in case it was a permission denied on write
unlink ($tuplet);
return $iswhitelisted;
}
1;