4f9af75e48
Previously the greylist plugin tried to use redis and fell back to DBM. This means that if a system already had an established DBM database, but happened to have Redis running, the existing DBM db would be abandoned for a new Redis DB. This would inevitably lead to more delays for legitimate mail, and possibly lost mail. This adds a 'redis_server' argument which enables Redis and sets the location of the redis server; if it is not explicitly set, DBM is used instead. If the redis server is unavailable, rather than failing to start QP, we instead fail to register the plugin.
313 lines
10 KiB
Perl
313 lines
10 KiB
Perl
#!perl -w
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Qpsmtpd::Address;
|
|
use Qpsmtpd::Constants;
|
|
|
|
my $test_email = 'user@example.com';
|
|
|
|
sub register_tests {
|
|
my $self = shift;
|
|
|
|
$self->register_test("test_load_exclude_files");
|
|
$self->register_test('test_data_handler');
|
|
$self->register_test('test_get_greylist_key');
|
|
$self->register_test('test_exclude');
|
|
$self->register_test("test_greylist_geoip");
|
|
$self->register_test("test_greylist_p0f_genre");
|
|
$self->register_test("test_greylist_p0f_distance");
|
|
$self->register_test("test_greylist_p0f_link");
|
|
$self->register_test("test_greylist_p0f_uptime");
|
|
$self->register_test('test_exclude_file_match');
|
|
$self->register_test('test_greylist');
|
|
$self->register_test('test_init_redis');
|
|
$self->register_test('test_init_dbm');
|
|
$self->register_test('test_parse_redis_server');
|
|
}
|
|
|
|
sub test_load_exclude_files {
|
|
my ( $self ) = @_;
|
|
delete $self->{$_} for qw( _exclude_ip _exclude_hostname exclude_re );
|
|
$self->load_exclude_files();
|
|
ok( $self->{_exclude_ip}{'194.7.234.142'},
|
|
'Excluded IPs populated by load_exclude_files()' );
|
|
ok( $self->{_exclude_hostname}{'yahoo.com'},
|
|
'Excluded hostnames populated by load_exclude_files()' );
|
|
ok( ( grep { $_ eq qr/^mta[12].siol.net$/ } @{ $self->{_exclude_re} || [] } ),
|
|
'Excluded REs populated by load_exlude_files()' );
|
|
}
|
|
|
|
sub test_exclude_file_match {
|
|
my ( $self ) = @_;
|
|
my @test_data = (
|
|
{
|
|
ip => 192.168.1.1,
|
|
hostname => 'mta1234.siol.net',
|
|
expected => 0,
|
|
descr => 'miss',
|
|
},
|
|
{
|
|
ip => '194.7.234.142',
|
|
hostname => 'mta1234.siol.net',
|
|
expected => 1,
|
|
descr => 'IP match',
|
|
},
|
|
{
|
|
ip => 192.168.1.1,
|
|
hostname => 'postini.com',
|
|
expected => 1,
|
|
descr => 'Hostname match',
|
|
},
|
|
{
|
|
ip => 192.168.1.1,
|
|
hostname => 'mta2.siol.net',
|
|
expected => 1,
|
|
descr => 'Regex match',
|
|
},
|
|
);
|
|
for my $t ( @test_data ) {
|
|
$self->connection->remote_ip( $t->{ip} );
|
|
$self->connection->remote_host( $t->{hostname} );
|
|
is( $self->exclude_file_match(), $t->{expected}, "exclude_file_match(): $t->{descr}" );
|
|
}
|
|
}
|
|
|
|
sub test_data_handler {
|
|
my $self = shift;
|
|
my $transaction = $self->qp->transaction;
|
|
|
|
my ($code, $mess) = $self->data_handler( $transaction );
|
|
cmp_ok( $code, '==', DECLINED, "no note" );
|
|
|
|
$transaction->notes('greylist', 1);
|
|
|
|
($code, $mess) = $self->data_handler( $transaction );
|
|
cmp_ok( $code, '==', DECLINED, "no recipients");
|
|
|
|
my $address = Qpsmtpd::Address->new( "<$test_email>" );
|
|
$transaction->recipients( $address );
|
|
|
|
$transaction->notes('whitelistrcpt', 2);
|
|
($code, $mess) = $self->data_handler( $transaction );
|
|
cmp_ok( $code, '==', DENYSOFT, "missing recipients");
|
|
|
|
$transaction->notes('whitelistrcpt', 1);
|
|
($code, $mess) = $self->data_handler( $transaction );
|
|
cmp_ok( $code, '==', DECLINED, "missing recipients");
|
|
}
|
|
|
|
sub test_get_greylist_key {
|
|
my $self = shift;
|
|
|
|
$self->{_args}{sender} = 0;
|
|
$self->{_args}{recipient} = 0;
|
|
$self->{_args}{remote_ip} = 0;
|
|
|
|
my $test_ip = '192.168.1.1';
|
|
|
|
my $address = Qpsmtpd::Address->new( "<$test_email>" );
|
|
$self->qp->transaction->sender( $address );
|
|
$self->qp->transaction->add_recipient( $address );
|
|
$self->qp->connection->remote_ip($test_ip);
|
|
|
|
my $key = $self->get_greylist_key();
|
|
ok( ! $key, "db key empty: -");
|
|
|
|
$self->{_args}{remote_ip} = 1;
|
|
$key = $self->get_greylist_key( $address, $address );
|
|
cmp_ok( $key, 'eq', '3232235777', "db key: $key");
|
|
|
|
$self->{_args}{sender} = 1;
|
|
$key = $self->get_greylist_key( $address, $address );
|
|
cmp_ok( $key, 'eq', "3232235777:$test_email", "db key: $key");
|
|
|
|
$self->{_args}{recipient} = 1;
|
|
$key = $self->get_greylist_key( $address, $address );
|
|
cmp_ok( $key, 'eq', "3232235777:$test_email:$test_email", "db key: $key");
|
|
}
|
|
|
|
sub test_exclude {
|
|
my ( $self ) = @_;
|
|
|
|
$self->connection->relay_client(1);
|
|
ok( $self->exclude(), "Relay client results in exclude() hit" );
|
|
$self->connection->relay_client(0);
|
|
ok( ! $self->exclude(), "Non-relay client results in exclude() miss" );
|
|
|
|
my $old_p0f = $self->connection->notes('p0f');
|
|
$self->connection->notes('p0f'=> { genre => 'windows' } );
|
|
delete $self->{_args}{p0f};
|
|
ok( ! $self->exclude(), 'no p0f args = no exclusion' );
|
|
$self->{_args}{'p0f'} = 'genre,Lindows';
|
|
ok( $self->exclude(), 'p0f miss = exclusion' );
|
|
$self->{_args}{'p0f'} = 'genre,Windows';
|
|
ok( ! $self->exclude(), 'p0f hit = no exclusion' );
|
|
$self->connection->notes( p0f => $old_p0f );
|
|
}
|
|
|
|
sub test_greylist_geoip {
|
|
my $self = shift;
|
|
|
|
$self->{_args}{'geoip'} = 'US,UK,HU';
|
|
|
|
my @valid = qw/ US us UK hu /;
|
|
my @invalid = qw/ PK RU ru /;
|
|
|
|
foreach my $cc ( @valid ) {
|
|
$self->connection->notes('geoip_country', $cc );
|
|
ok( $self->geoip_match(), "match + ($cc)");
|
|
ok( $self->exclude(), "match + ($cc) results in exclude() hit");
|
|
}
|
|
|
|
foreach my $cc ( @invalid ) {
|
|
$self->connection->notes('geoip_country', $cc );
|
|
ok( ! $self->geoip_match(), "bad - ($cc)");
|
|
ok( ! $self->exclude(), "miss - ($cc) results in exclude() miss");
|
|
}
|
|
}
|
|
|
|
sub test_greylist_p0f_genre {
|
|
my $self = shift;
|
|
|
|
$self->{_args}{'p0f'} = 'genre,Linux';
|
|
$self->connection->notes('p0f'=> { genre => 'windows', link => 'dsl' } );
|
|
ok( ! $self->p0f_match(), 'p0f genre miss');
|
|
|
|
$self->{_args}{'p0f'} = 'genre,Windows';
|
|
$self->connection->notes('p0f'=> { genre => 'windows', link => 'dsl' } );
|
|
ok( $self->p0f_match(), 'p0f genre hit');
|
|
}
|
|
|
|
sub test_greylist_p0f_distance {
|
|
my $self = shift;
|
|
|
|
$self->{_args}{'p0f'} = 'distance,8';
|
|
$self->connection->notes('p0f'=> { distance=>9 } );
|
|
ok( $self->p0f_match(), 'p0f distance hit');
|
|
|
|
$self->{_args}{'p0f'} = 'distance,8';
|
|
$self->connection->notes('p0f'=> { distance=>7 } );
|
|
ok( ! $self->p0f_match(), 'p0f distance miss');
|
|
}
|
|
|
|
sub test_greylist_p0f_link {
|
|
my $self = shift;
|
|
|
|
$self->{_args}{'p0f'} = 'link,dsl';
|
|
$self->connection->notes('p0f'=> { link=>'DSL' } );
|
|
ok( $self->p0f_match(), 'p0f link hit');
|
|
ok( ! $self->exclude(), 'p0f link hit results in exclude() miss' );
|
|
|
|
$self->{_args}{'p0f'} = 'link,dsl';
|
|
$self->connection->notes('p0f'=> { link=>'Ethernet' } );
|
|
ok( ! $self->p0f_match(), 'p0f link miss');
|
|
ok( $self->exclude(), 'p0f link miss results in exclude() hit' );
|
|
}
|
|
|
|
sub test_greylist_p0f_uptime {
|
|
my $self = shift;
|
|
|
|
$self->{_args}{'p0f'} = 'uptime,100';
|
|
$self->connection->notes('p0f'=> { uptime=> 99 } );
|
|
ok( $self->p0f_match(), 'p0f uptime hit');
|
|
|
|
$self->{_args}{'p0f'} = 'uptime,100';
|
|
$self->connection->notes('p0f'=> { uptime=>500 } );
|
|
ok( ! $self->p0f_match(), 'p0f uptime miss');
|
|
}
|
|
|
|
my $mocktime;
|
|
sub test_greylist {
|
|
my ( $self ) = @_;
|
|
$self->{_args} = {
|
|
remote_ip => 1,
|
|
sender => 0,
|
|
recipient => 0,
|
|
reject => 1,
|
|
black_timeout => 50 * 60, # 50m
|
|
grey_timeout => 3 * 3600 + 20 * 60, # 3h:20m
|
|
white_timeout => 36 * 3600 * 24, # 36 days
|
|
p0f => 0,
|
|
geoip => 0,
|
|
};
|
|
$self->connection->remote_host('example.com');
|
|
$self->connection->remote_ip('1.2.3.4');
|
|
my $sender = Qpsmtpd::Address->new( "<$test_email>" );
|
|
my $rcpt = Qpsmtpd::Address->new( "<$test_email>" );
|
|
for my $test_class (@Qpsmtpd::DB::child_classes) {
|
|
delete $self->{db};
|
|
eval { $self->db( class => $test_class ) };
|
|
if ( $@ ) {
|
|
warn "Unable to test greylisting against $test_class: $@";
|
|
next;
|
|
}
|
|
$self->db->lock;
|
|
$self->db->flush;
|
|
$self->db->unlock;
|
|
my $start = time() - 40 * 3600 * 24; # 40 days ago
|
|
$mocktime = $start;
|
|
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
|
'DENYSOFT: This mail is temporarily denied',
|
|
'Initial connection attempt greylisted' );
|
|
$mocktime = $start + 60 * 49;
|
|
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
|
'DENYSOFT: This mail is temporarily denied',
|
|
'Greylisted 49 minutes later' );
|
|
$mocktime = $start + 60 * 51;
|
|
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
|
'DECLINED',
|
|
'Allowed 51 minutes later' );
|
|
$mocktime = $start + 60 * 52 + 36 * 3600 * 24;
|
|
$self->prune_db;
|
|
is( $self->rc( $self->greylist( $self->transaction, $sender, $rcpt ) ),
|
|
'DENYSOFT: This mail is temporarily denied',
|
|
're-greylisted 36 days later' );
|
|
$self->db->lock;
|
|
$self->db->flush;
|
|
$self->db->unlock;
|
|
}
|
|
}
|
|
|
|
{
|
|
no warnings qw( redefine );
|
|
sub now { $mocktime || time() }
|
|
}
|
|
|
|
sub rc {
|
|
my ( $self, $r, $msg ) = @_;
|
|
return '' if ! defined $r;
|
|
return return_code($r) if ! defined $msg;
|
|
return return_code($r) . ": $msg";
|
|
}
|
|
|
|
sub test_init_redis {
|
|
my ($self) = @_;
|
|
delete $self->{db};
|
|
$self->{_args}{redis_server} = 'bogusserverasdfqwerty.:6379';
|
|
ok( ! $self->init_redis, 'init_redis() fails on bogus server' );
|
|
eval { Qpsmtpd::DB::Redis->new };
|
|
return if $@;
|
|
$self->{_args}{redis_server} = 'localhost';
|
|
ok( $self->init_redis, 'init_redis() succeeds when redis is up' );
|
|
}
|
|
|
|
sub test_init_dbm {
|
|
my ($self) = @_;
|
|
delete $self->{db};
|
|
delete $self->{_args}{redis_server};
|
|
ok( $self->init_db, 'init_db() works for DBM' );
|
|
}
|
|
|
|
sub test_parse_redis_server {
|
|
my ($self) = @_;
|
|
$self->{_args}{redis_server} = 'asdf:1234';
|
|
is( $self->parse_redis_server, 'asdf:1234',
|
|
'parse_redis_server(): leave provided port alone' );
|
|
$self->{_args}{redis_server} = 'qwerty';
|
|
is( $self->parse_redis_server, 'qwerty:6379',
|
|
'parse_redis_server(): add default port' );
|
|
}
|
|
|