Merge pull request #233 from jaredj/greylist-error-handling
Skip greylisting when we can't talk to greylist DB
This commit is contained in:
commit
573f7c5bf4
@ -180,15 +180,23 @@ sub flush {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sub dir {
|
sub dir {
|
||||||
my ( $self, @candidate_dirs ) = @_;
|
my ( $self, @arg ) = @_;
|
||||||
return $self->{dir} if $self->{dir} and ! @candidate_dirs;
|
return $self->{dir} if $self->{dir} and ! @arg;
|
||||||
push @candidate_dirs, ( $self->qphome . '/var/db', $self->qphome . '/config' );
|
for my $d ( $self->candidate_dirs(@arg) ) {
|
||||||
for my $d ( @candidate_dirs ) {
|
|
||||||
next if ! $self->validate_dir($d);
|
next if ! $self->validate_dir($d);
|
||||||
return $self->{dir} = $d; # first match wins
|
return $self->{dir} = $d; # first match wins
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub candidate_dirs {
|
||||||
|
my ( $self, @arg ) = @_;
|
||||||
|
return @{ $self->{candidate_dirs} } if $self->{candidate_dirs} && ! @arg;
|
||||||
|
$self->{candidate_dirs} = \@arg if @arg;
|
||||||
|
push @{ $self->{candidate_dirs} },
|
||||||
|
$self->qphome . '/var/db', $self->qphome . '/config';
|
||||||
|
return @{ $self->{candidate_dirs} };
|
||||||
|
}
|
||||||
|
|
||||||
sub validate_dir {
|
sub validate_dir {
|
||||||
my ( $self, $d ) = @_;
|
my ( $self, $d ) = @_;
|
||||||
return 0 if ! $d;
|
return 0 if ! $d;
|
||||||
|
@ -347,10 +347,24 @@ sub _register_standard_hooks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub db_args {
|
||||||
|
my ( $self, %arg ) = @_;
|
||||||
|
$self->validate_db_args(@_);
|
||||||
|
$self->{db_args} = \%arg if %arg;
|
||||||
|
$self->{db_args}{name} ||= $self->plugin_name;
|
||||||
|
$self->{db_args}{cnx_timeout} ||= 1;
|
||||||
|
return %{ $self->{db_args} };
|
||||||
|
}
|
||||||
|
|
||||||
sub db {
|
sub db {
|
||||||
my ( $self, %arg ) = @_;
|
my ( $self, %arg ) = @_;
|
||||||
$arg{name} ||= $self->plugin_name;
|
$self->validate_db_args(@_);
|
||||||
return $self->{db} ||= Qpsmtpd::DB->new(%arg);
|
return $self->{db} ||= Qpsmtpd::DB->new( $self->db_args(%arg) );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub validate_db_args {
|
||||||
|
(my $self, undef, my @args) = @_;
|
||||||
|
die "Invalid db arguments\n" if @args % 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
@ -111,6 +111,9 @@ Location of redis server where the greylisting DB will be stored.
|
|||||||
Redis can be used as a scalable and clusterable alternative
|
Redis can be used as a scalable and clusterable alternative
|
||||||
to a simple DBM file. For more information, see http://redis.io
|
to a simple DBM file. For more information, see http://redis.io
|
||||||
|
|
||||||
|
When Redis is in use, this plugin will wait up to 1 second to connect;
|
||||||
|
when Redis is unavailable, clients will not be greylisted.
|
||||||
|
|
||||||
=head2 per_recipient <bool>
|
=head2 per_recipient <bool>
|
||||||
|
|
||||||
Flag to indicate whether to use per-recipient configs.
|
Flag to indicate whether to use per-recipient configs.
|
||||||
@ -211,9 +214,10 @@ sub register {
|
|||||||
$config->{reject} = $config->{mode} =~ /testonly|off/i ? 0 : 1;
|
$config->{reject} = $config->{mode} =~ /testonly|off/i ? 0 : 1;
|
||||||
}
|
}
|
||||||
$self->{_args} = $config;
|
$self->{_args} = $config;
|
||||||
$self->init_db() or return;
|
$self->init_db();
|
||||||
$self->register_hooks();
|
$self->register_hooks();
|
||||||
$self->prune_db();
|
eval { $self->prune_db(); };
|
||||||
|
$self->log(LOGERROR, "Unable to prune greylist DB: $@") if $@;
|
||||||
if ($self->{_args}{upgrade}) {
|
if ($self->{_args}{upgrade}) {
|
||||||
$self->convert_db();
|
$self->convert_db();
|
||||||
}
|
}
|
||||||
@ -233,22 +237,20 @@ sub register_hooks {
|
|||||||
|
|
||||||
sub init_db {
|
sub init_db {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
return $self->init_redis if $self->{_args}{redis};
|
if ( $self->{_args}{redis} ) {
|
||||||
return $self->init_dbm;
|
$self->init_redis;
|
||||||
|
} else {
|
||||||
|
$self->init_dbm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sub init_redis {
|
sub init_redis {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
eval {
|
$self->db_args(
|
||||||
$self->db(
|
|
||||||
name => 'greylist',
|
name => 'greylist',
|
||||||
class => 'Qpsmtpd::DB::Redis',
|
class => 'Qpsmtpd::DB::Redis',
|
||||||
server => $self->parse_redis_server,
|
server => $self->parse_redis_server,
|
||||||
) or die 'Unknown error';
|
);
|
||||||
};
|
|
||||||
return 1 if ! $@;
|
|
||||||
$self->log(LOGCRIT, "Unable to connect to redis, GREYLISTING DISABLED: $@");
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub parse_redis_server {
|
sub parse_redis_server {
|
||||||
@ -263,7 +265,7 @@ sub init_dbm {
|
|||||||
$self->db(
|
$self->db(
|
||||||
name => 'greylist',
|
name => 'greylist',
|
||||||
class => 'Qpsmtpd::DB::File::DBM'
|
class => 'Qpsmtpd::DB::File::DBM'
|
||||||
) or return 0;
|
);
|
||||||
my $cdir = $self->{_args}{db_dir};
|
my $cdir = $self->{_args}{db_dir};
|
||||||
$cdir = $1 if $cdir and $cdir =~ m{^([-a-zA-Z0-9./_]+)$};
|
$cdir = $1 if $cdir and $cdir =~ m{^([-a-zA-Z0-9./_]+)$};
|
||||||
# greylisting-specific hints for where to store the greylist DB
|
# greylisting-specific hints for where to store the greylist DB
|
||||||
@ -271,11 +273,10 @@ sub init_dbm {
|
|||||||
$self->db->nfs_locking( $self->{_args}{nfslock} );
|
$self->db->nfs_locking( $self->{_args}{nfslock} );
|
||||||
|
|
||||||
# Work around old DBM filename
|
# Work around old DBM filename
|
||||||
return 1 if -f "$db_dir/greylist.dbm";
|
|
||||||
my $oldname = 'denysoft_greylist';
|
my $oldname = 'denysoft_greylist';
|
||||||
return 1 if ! -f "$db_dir/$oldname.dbm";
|
if ( ! -f "$db_dir/greylist.dbm" && -f "$db_dir/$oldname.dbm" ) {
|
||||||
$self->db->name($oldname);
|
$self->db->name($oldname);
|
||||||
return 1;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sub load_exclude_files {
|
sub load_exclude_files {
|
||||||
@ -386,16 +387,25 @@ sub greylist {
|
|||||||
|
|
||||||
|
|
||||||
my $key = $self->get_greylist_key($sender, $rcpt) or return DECLINED;
|
my $key = $self->get_greylist_key($sender, $rcpt) or return DECLINED;
|
||||||
|
my @result;
|
||||||
|
eval {
|
||||||
|
$self->db->lock;
|
||||||
|
@result = $self->greylist_result($key, $config);
|
||||||
|
$self->db->unlock;
|
||||||
|
};
|
||||||
|
return $self->errcode($@) if $@;
|
||||||
|
return @result;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub greylist_result {
|
||||||
|
my ($self, $key, $config) = @_;
|
||||||
my $fmt = "%s:%d:%d:%d";
|
my $fmt = "%s:%d:%d:%d";
|
||||||
|
|
||||||
$self->db->lock or return DECLINED;
|
|
||||||
my $value = $self->db->get($key);
|
my $value = $self->db->get($key);
|
||||||
if ( ! $value ) {
|
if ( ! $value ) {
|
||||||
# new IP or entry timed out - record new
|
# new IP or entry timed out - record new
|
||||||
$self->db->set( $key, sprintf $fmt, $self->now, 1, 0, 0 );
|
$self->db->set( $key, sprintf $fmt, $self->now, 1, 0, 0 );
|
||||||
$self->log(LOGWARN, "fail: initial DENYSOFT, unknown");
|
$self->log(LOGWARN, "fail: initial DENYSOFT, unknown");
|
||||||
return $self->cleanup_and_return();
|
return $self->failcode();
|
||||||
}
|
}
|
||||||
|
|
||||||
my ( $ts, $new, $black, $white ) = split /:/, $value;
|
my ( $ts, $new, $black, $white ) = split /:/, $value;
|
||||||
@ -407,7 +417,7 @@ sub greylist {
|
|||||||
if ( $self->now - $ts < $config->{white_timeout} ) {
|
if ( $self->now - $ts < $config->{white_timeout} ) {
|
||||||
$self->db->set( $key, sprintf $fmt, $self->now, $new, $black, ++$white );
|
$self->db->set( $key, sprintf $fmt, $self->now, $new, $black, ++$white );
|
||||||
$self->log(LOGINFO, "pass: white, $white deliveries");
|
$self->log(LOGINFO, "pass: white, $white deliveries");
|
||||||
return $self->cleanup_and_return(DECLINED);
|
return DECLINED;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$self->log(LOGINFO, "key $key has timed out (white)");
|
$self->log(LOGINFO, "key $key has timed out (white)");
|
||||||
@ -419,26 +429,29 @@ sub greylist {
|
|||||||
$self->db->set( $key, sprintf $fmt, $ts, $new, ++$black, 0 );
|
$self->db->set( $key, sprintf $fmt, $ts, $new, ++$black, 0 );
|
||||||
$self->log(LOGWARN,
|
$self->log(LOGWARN,
|
||||||
"fail: black DENYSOFT - $black deferred connections");
|
"fail: black DENYSOFT - $black deferred connections");
|
||||||
return $self->cleanup_and_return();
|
return $self->failcode();
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->log(LOGWARN, "pass: timed out (grey)");
|
$self->log(LOGWARN, "pass: timed out (grey)");
|
||||||
return $self->cleanup_and_return(DECLINED);
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
# This exists purely to be overridden for testing
|
# This exists purely to be overridden for testing
|
||||||
sub now { time() }
|
sub now { time() }
|
||||||
|
|
||||||
sub cleanup_and_return {
|
sub failcode {
|
||||||
my ($self, $return_val) = @_;
|
my ($self) = @_;
|
||||||
|
|
||||||
$self->db->unlock;
|
|
||||||
return $return_val if defined $return_val; # explicit override
|
|
||||||
return DECLINED
|
return DECLINED
|
||||||
if defined $self->{_args}{reject} && !$self->{_args}{reject};
|
if defined $self->{_args}{reject} && !$self->{_args}{reject};
|
||||||
return DENYSOFT, $DENYMSG;
|
return DENYSOFT, $DENYMSG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub errcode {
|
||||||
|
my ($self, $err) = @_;
|
||||||
|
$self->log(LOGERROR, "UNABLE TO OBTAIN GREYLIST RESULT:$err");
|
||||||
|
return DECLINED;
|
||||||
|
}
|
||||||
|
|
||||||
sub get_greylist_key {
|
sub get_greylist_key {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
my $sender = shift || $self->qp->transaction->sender;
|
my $sender = shift || $self->qp->transaction->sender;
|
||||||
|
@ -26,6 +26,8 @@ sub register_tests {
|
|||||||
$self->register_test('test_init_redis');
|
$self->register_test('test_init_redis');
|
||||||
$self->register_test('test_init_dbm');
|
$self->register_test('test_init_dbm');
|
||||||
$self->register_test('test_parse_redis_server');
|
$self->register_test('test_parse_redis_server');
|
||||||
|
$self->register_test('test_failcode');
|
||||||
|
$self->register_test('test_errcode');
|
||||||
}
|
}
|
||||||
|
|
||||||
sub test_load_exclude_files {
|
sub test_load_exclude_files {
|
||||||
@ -333,19 +335,27 @@ sub rc {
|
|||||||
sub test_init_redis {
|
sub test_init_redis {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
delete $self->{db};
|
delete $self->{db};
|
||||||
$self->{_args}{redis} = 'bogusserverasdfqwerty.:6379';
|
$self->{_args}{redis} = 'testredis';
|
||||||
ok( ! $self->init_redis, 'init_redis() fails on bogus server' );
|
$self->init_db;
|
||||||
eval { Qpsmtpd::DB::Redis->new };
|
is( keyvals($self->db_args),
|
||||||
return if $@;
|
'class=Qpsmtpd::DB::Redis;cnx_timeout=1;'
|
||||||
$self->{_args}{redis} = 'localhost';
|
. 'name=greylist;server=testredis:6379',
|
||||||
ok( $self->init_redis, 'init_redis() succeeds when redis is up' );
|
'init_redis() sets redis args' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub keyvals {
|
||||||
|
my ( %h ) = @_;
|
||||||
|
return join ";", map { "$_=$h{$_}" } sort keys %h;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub test_init_dbm {
|
sub test_init_dbm {
|
||||||
my ($self) = @_;
|
my ($self) = @_;
|
||||||
delete $self->{db};
|
delete $self->{db};
|
||||||
delete $self->{_args}{redis};
|
delete $self->{_args}{redis};
|
||||||
ok( $self->init_db, 'init_db() works for DBM' );
|
$self->init_db;
|
||||||
|
is( $self->db->name, 'greylist', 'init_dbm() sets correct db name' );
|
||||||
|
is( $self->db->path, 't/config/greylist.dbm', 'init_dbm() sets correct path' );
|
||||||
|
is( ref $self->db, 'Qpsmtpd::DB::File::DBM', 'init_dbm() gives DBM object' );
|
||||||
}
|
}
|
||||||
|
|
||||||
sub test_parse_redis_server {
|
sub test_parse_redis_server {
|
||||||
@ -358,3 +368,24 @@ sub test_parse_redis_server {
|
|||||||
'parse_redis_server(): add default port' );
|
'parse_redis_server(): add default port' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub test_failcode {
|
||||||
|
my ($self) = @_;
|
||||||
|
$self->{_args}{reject} = 0;
|
||||||
|
is( $self->rc( $self->failcode() ), 'DECLINED',
|
||||||
|
'failcode(): reject disabled' );
|
||||||
|
delete $self->{_args}{reject};
|
||||||
|
is( $self->rc( $self->failcode() ), 'DENYSOFT: This mail is temporarily denied',
|
||||||
|
'failcode(): reject enabled' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub test_errcode {
|
||||||
|
my ($self) = @_;
|
||||||
|
delete $self->qp->{_logged};
|
||||||
|
is( $self->rc( $self->errcode('test error') ),
|
||||||
|
'DECLINED',
|
||||||
|
'errcode(): proper return code' );
|
||||||
|
is( join("\n", @{ $self->qp->{_logged} || [] }),
|
||||||
|
'LOGERROR:greylistingUNABLE TO OBTAIN GREYLIST RESULT:test error',
|
||||||
|
'errocode(): logged error' );
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ __get_keys();
|
|||||||
__size();
|
__size();
|
||||||
__flush();
|
__flush();
|
||||||
__qphome();
|
__qphome();
|
||||||
|
__candidate_dirs();
|
||||||
__validate_dir();
|
__validate_dir();
|
||||||
__dir();
|
__dir();
|
||||||
__untie_gotcha();
|
__untie_gotcha();
|
||||||
@ -108,6 +109,15 @@ sub __qphome {
|
|||||||
is( $db->qphome, 't', 'qphome()' );
|
is( $db->qphome, 't', 'qphome()' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub __candidate_dirs {
|
||||||
|
is( join('|', $db->candidate_dirs), 't/var/db|t/config',
|
||||||
|
'candidate_dirs() default ' );
|
||||||
|
is( join('|', $db->candidate_dirs('foo')), 'foo|t/var/db|t/config',
|
||||||
|
'candidate_dirs() passed args plus defaults' );
|
||||||
|
is( join('|', $db->candidate_dirs), 'foo|t/var/db|t/config',
|
||||||
|
'candidate_dirs() cached values' );
|
||||||
|
}
|
||||||
|
|
||||||
sub __validate_dir {
|
sub __validate_dir {
|
||||||
is( $db->validate_dir(), 0, 'validate_dir(): false on no input' );
|
is( $db->validate_dir(), 0, 'validate_dir(): false on no input' );
|
||||||
is( $db->validate_dir(undef), 0, 'validate_dir(): false on undef' );
|
is( $db->validate_dir(undef), 0, 'validate_dir(): false on undef' );
|
||||||
|
@ -9,11 +9,42 @@ use Test::Qpsmtpd;
|
|||||||
|
|
||||||
use_ok('Qpsmtpd::Plugin');
|
use_ok('Qpsmtpd::Plugin');
|
||||||
|
|
||||||
|
__validate_db_args();
|
||||||
|
__db_args();
|
||||||
__db();
|
__db();
|
||||||
__register_hook();
|
__register_hook();
|
||||||
|
|
||||||
done_testing();
|
done_testing();
|
||||||
|
|
||||||
|
sub __validate_db_args {
|
||||||
|
my $plugin = FakePlugin->new;
|
||||||
|
eval { $plugin->validate_db_args($plugin, testkey => 1) };
|
||||||
|
is( $@, '', 'validate_db_args() does not die on valid data' );
|
||||||
|
eval { $plugin->validate_db_args($plugin, 'bogus') };
|
||||||
|
is( $@, "Invalid db arguments\n", 'validate_db_args() dies on invalid data' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub __db_args {
|
||||||
|
my $plugin = FakePlugin->new;
|
||||||
|
is( keyvals($plugin->db_args),
|
||||||
|
'cnx_timeout=1;name=___MockHook___',
|
||||||
|
'default db args populated' );
|
||||||
|
is( keyvals($plugin->db_args( arg1 => 1 )),
|
||||||
|
'arg1=1;cnx_timeout=1;name=___MockHook___',
|
||||||
|
'passed args in addition to defaults' );
|
||||||
|
is( keyvals($plugin->db_args( name => 'bob', arg2 => 2 )),
|
||||||
|
'arg2=2;cnx_timeout=1;name=bob',
|
||||||
|
'passed args override defaults' );
|
||||||
|
is( keyvals($plugin->db_args),
|
||||||
|
'arg2=2;cnx_timeout=1;name=bob',
|
||||||
|
'get previous args' );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub keyvals {
|
||||||
|
my ( %h ) = @_;
|
||||||
|
return join ";", map { "$_=$h{$_}" } sort keys %h;
|
||||||
|
}
|
||||||
|
|
||||||
sub __db {
|
sub __db {
|
||||||
my $plugin = FakePlugin->new;
|
my $plugin = FakePlugin->new;
|
||||||
my $db = $plugin->db( class => 'FakeDB', name => 'testfoo' );
|
my $db = $plugin->db( class => 'FakeDB', name => 'testfoo' );
|
||||||
|
Loading…
Reference in New Issue
Block a user