Merge pull request #15 from msimerson/early
consolidated chunks of code duplicated 4x into log_and_deny and log_and_...
This commit is contained in:
commit
4e2ae484a2
2
config.sample/dnsbl_allow
Normal file
2
config.sample/dnsbl_allow
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# test entry for dnsbl plugin
|
||||||
|
192.168.99.5
|
@ -58,7 +58,8 @@ sub hook_mail {
|
|||||||
@badmailfrom = @{$self->{_badmailfrom_config}};
|
@badmailfrom = @{$self->{_badmailfrom_config}};
|
||||||
};
|
};
|
||||||
|
|
||||||
return DECLINED if $self->is_immune( $sender, \@badmailfrom );
|
return DECLINED if $self->is_immune();
|
||||||
|
return DECLINED if $self->is_immune_sender( $sender, \@badmailfrom );
|
||||||
|
|
||||||
my $host = lc $sender->host;
|
my $host = lc $sender->host;
|
||||||
my $from = lc($sender->user) . '@' . $host;
|
my $from = lc($sender->user) . '@' . $host;
|
||||||
@ -104,7 +105,7 @@ sub hook_rcpt {
|
|||||||
return (DENY, $note);
|
return (DENY, $note);
|
||||||
}
|
}
|
||||||
|
|
||||||
sub is_immune {
|
sub is_immune_sender {
|
||||||
my ($self, $sender, $badmf ) = @_;
|
my ($self, $sender, $badmf ) = @_;
|
||||||
|
|
||||||
if ( ! scalar @$badmf ) {
|
if ( ! scalar @$badmf ) {
|
||||||
|
@ -49,12 +49,15 @@ use Qpsmtpd::DSN;
|
|||||||
sub hook_rcpt {
|
sub hook_rcpt {
|
||||||
my ($self, $transaction, $recipient, %param) = @_;
|
my ($self, $transaction, $recipient, %param) = @_;
|
||||||
|
|
||||||
return (DECLINED) if $self->qp->connection->relay_client();
|
return (DECLINED) if $self->is_immune();
|
||||||
|
|
||||||
my ($host, $to) = $self->get_host_and_to( $recipient )
|
my ($host, $to) = $self->get_host_and_to( $recipient )
|
||||||
or return (DECLINED);
|
or return (DECLINED);
|
||||||
|
|
||||||
my @badrcptto = $self->qp->config("badrcptto") or return (DECLINED);
|
my @badrcptto = $self->qp->config("badrcptto") or do {
|
||||||
|
$self->log(LOGINFO, "skip: empty config");
|
||||||
|
return (DECLINED);
|
||||||
|
};
|
||||||
|
|
||||||
for my $line (@badrcptto) {
|
for my $line (@badrcptto) {
|
||||||
$line =~ s/^\s+//g; # trim leading whitespace
|
$line =~ s/^\s+//g; # trim leading whitespace
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#!perl -w
|
#!perl -w
|
||||||
|
|
||||||
=head1 NAME
|
=head1 NAME
|
||||||
|
|
||||||
check_earlytalker - Check that the client doesn't talk before we send the SMTP banner
|
check_earlytalker - Check that the client doesn't talk before we send the SMTP banner
|
||||||
@ -16,9 +17,7 @@ on all mail/rcpt commands in the transaction.
|
|||||||
|
|
||||||
=head1 CONFIGURATION
|
=head1 CONFIGURATION
|
||||||
|
|
||||||
=over 4
|
=head2 wait [integer]
|
||||||
|
|
||||||
=item wait [integer]
|
|
||||||
|
|
||||||
The number of seconds to delay the initial greeting to see if the connecting
|
The number of seconds to delay the initial greeting to see if the connecting
|
||||||
host speaks first. The default is 1. Do not select a value that is too high,
|
host speaks first. The default is 1. Do not select a value that is too high,
|
||||||
@ -27,32 +26,40 @@ greeting timeouts -- these are known to range as low as 30 seconds, and may
|
|||||||
in some cases be configured lower by mailserver admins. Network transit time
|
in some cases be configured lower by mailserver admins. Network transit time
|
||||||
must also be allowed for.
|
must also be allowed for.
|
||||||
|
|
||||||
=item action [string: deny, denysoft, log]
|
=head2 reject <boolean>
|
||||||
|
|
||||||
What to do when matching an early-talker -- the options are I<deny>,
|
Do we reject/deny connections to early talkers?
|
||||||
I<denysoft> or I<log>.
|
|
||||||
|
|
||||||
If I<log> is specified, the connection will be allowed to proceed as normal,
|
check_earlytalker reject [ 0 | 1 ]
|
||||||
and only a warning will be logged.
|
|
||||||
|
|
||||||
The default is I<denysoft>.
|
Default: I<reject 1>
|
||||||
|
|
||||||
=item defer-reject [boolean]
|
=head2 reject_type [ temp | perm ]
|
||||||
|
|
||||||
|
What type of rejection to send. A temporary rejection tells the remote server to try again later. A permanent error tells it to give up permanently.
|
||||||
|
|
||||||
|
Default I<reject_type temp>.
|
||||||
|
|
||||||
|
=head2 defer-reject [boolean]
|
||||||
|
|
||||||
When an early-talker is detected, if this option is set to a true value, the
|
When an early-talker is detected, if this option is set to a true value, the
|
||||||
SMTP greeting will be issued as usual, but all RCPT/MAIL commands will be
|
SMTP greeting will be issued as usual, but all RCPT/MAIL commands will be
|
||||||
issued a deny or denysoft (depending on the value of I<action>). The default
|
issued a deny or denysoft (depending on the value of I<reject_type>). The default
|
||||||
is to react at the SMTP greeting stage by issuing the apropriate response code
|
is to react at the SMTP greeting stage by issuing the apropriate response code
|
||||||
and terminating the SMTP connection.
|
and terminating the SMTP connection.
|
||||||
|
|
||||||
=item check-at [ CONNECT | DATA ]
|
check_earlytalker defer-reject [ 0 | 1 ]
|
||||||
|
|
||||||
|
=head2 check-at [ CONNECT | DATA ]
|
||||||
|
|
||||||
Specifies when to check for early talkers. You can specify this option
|
Specifies when to check for early talkers. You can specify this option
|
||||||
multiple times to check more than once.
|
multiple times to check more than once.
|
||||||
|
|
||||||
The default is I<check-at CONNECT> only.
|
The default is I<check-at CONNECT> only.
|
||||||
|
|
||||||
=back
|
=head2 loglevel
|
||||||
|
|
||||||
|
Adjust the quantity of logging for this plugin. See docs/logging.pod
|
||||||
|
|
||||||
=cut
|
=cut
|
||||||
|
|
||||||
@ -67,7 +74,7 @@ sub register {
|
|||||||
|
|
||||||
if (@args % 2) {
|
if (@args % 2) {
|
||||||
$self->log(LOGERROR, "Unrecognized/mismatched arguments");
|
$self->log(LOGERROR, "Unrecognized/mismatched arguments");
|
||||||
return undef;
|
return;
|
||||||
}
|
}
|
||||||
my %check_at;
|
my %check_at;
|
||||||
for (0..$#args) {
|
for (0..$#args) {
|
||||||
@ -82,11 +89,20 @@ sub register {
|
|||||||
}
|
}
|
||||||
$self->{_args} = {
|
$self->{_args} = {
|
||||||
'wait' => 1,
|
'wait' => 1,
|
||||||
'action' => 'denysoft',
|
|
||||||
'defer-reject' => 0,
|
|
||||||
@args,
|
@args,
|
||||||
'check-at' => \%check_at,
|
'check-at' => \%check_at,
|
||||||
};
|
};
|
||||||
|
# backwards compat with old 'action' argument
|
||||||
|
if ( defined $self->{_args}{action} && ! defined $self->{_args}{reject} ) {
|
||||||
|
$self->{_args}{reject} = $self->{_args}{action} =~ /^deny/i ? 1 : 0;
|
||||||
|
};
|
||||||
|
if ( defined $self->{_args}{'defer-reject'} && ! defined $self->{_args}{reject_type} ) {
|
||||||
|
$self->{_args}{reject_type} = $self->{_args}{action} == 'denysoft' ? 'temp' : 'perm';
|
||||||
|
};
|
||||||
|
if ( ! defined $self->{_args}{reject_type} ) {
|
||||||
|
$self->{_args}{reject_type} = 'perm';
|
||||||
|
};
|
||||||
|
# /end compat
|
||||||
if ( $qp->{conn} && $qp->{conn}->isa('Apache2::Connection')) {
|
if ( $qp->{conn} && $qp->{conn}->isa('Apache2::Connection')) {
|
||||||
require APR::Const;
|
require APR::Const;
|
||||||
APR::Const->import(qw(POLLIN SUCCESS));
|
APR::Const->import(qw(POLLIN SUCCESS));
|
||||||
@ -98,117 +114,115 @@ sub register {
|
|||||||
$self->register_hook('data', 'data_handler');
|
$self->register_hook('data', 'data_handler');
|
||||||
}
|
}
|
||||||
$self->register_hook('mail', 'mail_handler')
|
$self->register_hook('mail', 'mail_handler')
|
||||||
if $self->{_args}->{'defer-reject'};
|
if $self->{_args}{'defer-reject'};
|
||||||
1;
|
$self->{_args}{reject} = 1 if ! defined $self->{_args}{reject};
|
||||||
}
|
}
|
||||||
|
|
||||||
sub apr_connect_handler {
|
sub apr_connect_handler {
|
||||||
my ($self, $transaction) = @_;
|
my ($self, $transaction) = @_;
|
||||||
|
|
||||||
return DECLINED unless $self->{_args}{'check-at'}{CONNECT};
|
return DECLINED unless $self->{_args}{'check-at'}{CONNECT};
|
||||||
return DECLINED if ($self->connection->notes('whitelisthost'));
|
return DECLINED if $self->is_immune();
|
||||||
my $ip = $self->qp->connection->remote_ip;
|
|
||||||
|
|
||||||
my $c = $self->qp->{conn};
|
my $c = $self->qp->{conn} or return DECLINED;
|
||||||
my $socket = $c->client_socket;
|
my $socket = $c->client_socket or return DECLINED;
|
||||||
my $timeout = $self->{_args}->{'wait'} * 1_000_000;
|
my $timeout = $self->{_args}{'wait'} * 1_000_000;
|
||||||
|
|
||||||
my $rc = $socket->poll($c->pool, $timeout, APR::Const::POLLIN());
|
my $rc = $socket->poll($c->pool, $timeout, APR::Const::POLLIN());
|
||||||
if ($rc == APR::Const::SUCCESS()) {
|
if ($rc == APR::Const::SUCCESS()) {
|
||||||
$self->log(LOGNOTICE, "remote host started talking before we said hello [$ip]");
|
if ($self->{_args}{'defer-reject'}) {
|
||||||
if ($self->{_args}->{'defer-reject'}) {
|
$self->qp->connection->notes('earlytalker', 1);
|
||||||
$self->connection->notes('earlytalker', 1);
|
return DECLINED;
|
||||||
}
|
};
|
||||||
else {
|
return $self->log_and_deny();
|
||||||
my $msg = 'Connecting host started transmitting before SMTP greeting';
|
};
|
||||||
return (DENY,$msg) if $self->{_args}->{'action'} eq 'deny';
|
return $self->log_and_pass();
|
||||||
return (DENYSOFT,$msg) if $self->{_args}->{'action'} eq 'denysoft';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$self->log(LOGINFO, "pass: remote host said nothing spontaneous");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub apr_data_handler {
|
sub apr_data_handler {
|
||||||
my ($self, $transaction) = @_;
|
my ($self, $transaction) = @_;
|
||||||
|
|
||||||
return DECLINED unless $self->{_args}{'check-at'}{DATA};
|
return DECLINED unless $self->{_args}{'check-at'}{DATA};
|
||||||
return DECLINED if ($self->connection->notes('whitelisthost'));
|
return DECLINED if $self->is_immune();
|
||||||
my $ip = $self->qp->connection->remote_ip;
|
|
||||||
|
|
||||||
my $c = $self->qp->{conn};
|
my $c = $self->qp->{conn} or return DECLINED;
|
||||||
my $socket = $c->client_socket;
|
my $socket = $c->client_socket or return DECLINED;
|
||||||
my $timeout = $self->{_args}->{'wait'} * 1_000_000;
|
my $timeout = $self->{_args}{'wait'} * 1_000_000;
|
||||||
|
|
||||||
my $rc = $socket->poll($c->pool, $timeout, APR::Const::POLLIN());
|
my $rc = $socket->poll($c->pool, $timeout, APR::Const::POLLIN());
|
||||||
if ($rc == APR::Const::SUCCESS()) {
|
if ($rc == APR::Const::SUCCESS()) {
|
||||||
$self->log(LOGNOTICE, "remote host started talking before we said hello [$ip]");
|
return $self->log_and_deny();
|
||||||
my $msg = 'Connecting host started transmitting before SMTP greeting';
|
};
|
||||||
return (DENY,$msg) if $self->{_args}->{'action'} eq 'deny';
|
return $self->log_and_pass();
|
||||||
return (DENYSOFT,$msg) if $self->{_args}->{'action'} eq 'denysoft';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$self->log(LOGINFO, "pass: remote host said nothing spontaneous");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub connect_handler {
|
sub connect_handler {
|
||||||
my ($self, $transaction) = @_;
|
my ($self, $transaction) = @_;
|
||||||
my $in = new IO::Select;
|
my $in = new IO::Select;
|
||||||
my $ip = $self->qp->connection->remote_ip;
|
|
||||||
|
|
||||||
return DECLINED unless $self->{_args}{'check-at'}{CONNECT};
|
return DECLINED unless $self->{_args}{'check-at'}{CONNECT};
|
||||||
return DECLINED
|
return DECLINED if $self->is_immune();
|
||||||
if ($self->connection->notes('whitelisthost'));
|
|
||||||
|
|
||||||
$in->add(\*STDIN) || return DECLINED;
|
$in->add(\*STDIN) or return DECLINED;
|
||||||
if ($in->can_read($self->{_args}->{'wait'})) {
|
if (! $in->can_read($self->{_args}{'wait'})) {
|
||||||
$self->log(LOGNOTICE, "remote host started talking before we said hello [$ip]");
|
return $self->log_and_pass();
|
||||||
if ($self->{_args}->{'defer-reject'}) {
|
};
|
||||||
$self->connection->notes('earlytalker', 1);
|
|
||||||
} else {
|
if ( ! $self->{_args}{'defer-reject'}) {
|
||||||
my $msg = 'Connecting host started transmitting before SMTP greeting';
|
return $self->log_and_deny();
|
||||||
return (DENY,$msg) if $self->{_args}->{'action'} eq 'deny';
|
};
|
||||||
return (DENYSOFT,$msg) if $self->{_args}->{'action'} eq 'denysoft';
|
|
||||||
}
|
$self->qp->connection->notes('earlytalker', 1);
|
||||||
} else {
|
|
||||||
$self->log(LOGINFO, 'pass: remote host said nothing spontaneous');
|
|
||||||
}
|
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub data_handler {
|
sub data_handler {
|
||||||
my ($self, $transaction) = @_;
|
my ($self, $transaction) = @_;
|
||||||
my $in = new IO::Select;
|
my $in = new IO::Select;
|
||||||
my $ip = $self->qp->connection->remote_ip;
|
|
||||||
|
|
||||||
return DECLINED unless $self->{_args}{'check-at'}{DATA};
|
return DECLINED unless $self->{_args}{'check-at'}{DATA};
|
||||||
return DECLINED
|
return DECLINED if $self->is_immune();
|
||||||
if ($self->connection->notes('whitelisthost'));
|
|
||||||
|
|
||||||
$in->add(\*STDIN) || return DECLINED;
|
$in->add(\*STDIN) or return DECLINED;
|
||||||
if ($in->can_read($self->{_args}->{'wait'})) {
|
if ( ! $in->can_read($self->{_args}{'wait'})) {
|
||||||
$self->log(LOGNOTICE, "remote host started talking before we said hello [$ip]");
|
return $self->log_and_pass();
|
||||||
|
};
|
||||||
|
|
||||||
|
return $self->log_and_deny();
|
||||||
|
};
|
||||||
|
|
||||||
|
sub log_and_pass {
|
||||||
|
my $self = shift;
|
||||||
|
my $ip = $self->qp->connection->remote_ip || 'remote host';
|
||||||
|
$self->log(LOGINFO, "pass: $ip said nothing spontaneous");
|
||||||
|
return DECLINED;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub log_and_deny {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
|
my $ip = $self->qp->connection->remote_ip || 'remote host';
|
||||||
my $msg = 'Connecting host started transmitting before SMTP greeting';
|
my $msg = 'Connecting host started transmitting before SMTP greeting';
|
||||||
return (DENY,$msg) if $self->{_args}->{'action'} eq 'deny';
|
|
||||||
return (DENYSOFT,$msg) if $self->{_args}->{'action'} eq 'denysoft';
|
$self->qp->connection->notes('earlytalker', 1);
|
||||||
}
|
$self->log(LOGNOTICE, "fail: $ip started talking before we said hello");
|
||||||
else {
|
|
||||||
$self->log(LOGINFO, 'pass: remote host said nothing spontaneous');
|
return ( $self->get_reject_type(), $msg ) if $self->{_args}{reject};
|
||||||
}
|
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub mail_handler {
|
sub mail_handler {
|
||||||
my ($self, $transaction) = @_;
|
my ($self, $transaction) = @_;
|
||||||
my $msg = 'Connecting host started transmitting before SMTP greeting';
|
|
||||||
|
|
||||||
return DECLINED unless $self->connection->notes('earlytalker');
|
return DECLINED unless $self->qp->connection->notes('earlytalker');
|
||||||
return (DENY,$msg) if $self->{_args}->{'action'} eq 'deny';
|
return $self->log_and_deny();
|
||||||
return (DENYSOFT,$msg) if $self->{_args}->{'action'} eq 'denysoft';
|
|
||||||
return DECLINED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
1;
|
sub get_reject_type {
|
||||||
|
my $self = shift;
|
||||||
|
my $deny = $self->{_args}{reject_type} or return DENY;
|
||||||
|
|
||||||
|
return $deny eq 'temp' ? DENYSOFT
|
||||||
|
: $deny eq 'disconnect' ? DENY_DISCONNECT
|
||||||
|
: DENY;
|
||||||
|
};
|
||||||
|
@ -136,6 +136,7 @@ sub hook_connect {
|
|||||||
|
|
||||||
# perform RBLSMTPD checks to mimic Dan Bernstein's rblsmtpd
|
# perform RBLSMTPD checks to mimic Dan Bernstein's rblsmtpd
|
||||||
return DECLINED if $self->is_set_rblsmtpd();
|
return DECLINED if $self->is_set_rblsmtpd();
|
||||||
|
return DECLINED if $self->is_immune();
|
||||||
return DECLINED if $self->ip_whitelisted();
|
return DECLINED if $self->ip_whitelisted();
|
||||||
|
|
||||||
my %dnsbl_zones = map { (split /:/, $_, 2)[0,1] } $self->qp->config('dnsbl_zones');
|
my %dnsbl_zones = map { (split /:/, $_, 2)[0,1] } $self->qp->config('dnsbl_zones');
|
||||||
@ -196,23 +197,12 @@ sub is_set_rblsmtpd {
|
|||||||
};
|
};
|
||||||
|
|
||||||
sub ip_whitelisted {
|
sub ip_whitelisted {
|
||||||
my ($self) = @_;
|
my $self = shift;
|
||||||
|
|
||||||
my $remote_ip = $self->qp->connection->remote_ip;
|
my $remote_ip = shift || $self->qp->connection->remote_ip;
|
||||||
my $white = $self->connection->notes('whitelisthost');
|
|
||||||
if ( $white ) {
|
|
||||||
$self->log(LOGDEBUG, "skip: whitelist overrode blacklist: $white");
|
|
||||||
return 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( $self->qp->connection->relay_client() ) {
|
return
|
||||||
$self->log(LOGWARN, "skip: don't blacklist relay/auth clients");
|
grep { s/\.?$/./; $_ eq substr($remote_ip . '.', 0, length $_) }
|
||||||
return 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
return grep { s/\.?$/./;
|
|
||||||
$_ eq substr($remote_ip . '.', 0, length $_)
|
|
||||||
}
|
|
||||||
$self->qp->config('dnsbl_allow');
|
$self->qp->config('dnsbl_allow');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -306,6 +296,8 @@ sub process_sockets {
|
|||||||
sub hook_rcpt {
|
sub hook_rcpt {
|
||||||
my ($self, $transaction, $rcpt, %param) = @_;
|
my ($self, $transaction, $rcpt, %param) = @_;
|
||||||
|
|
||||||
|
return DECLINED if $self->is_immune();
|
||||||
|
|
||||||
# RBLSMTPD being non-empty means it contains the failure message to return
|
# RBLSMTPD being non-empty means it contains the failure message to return
|
||||||
if (defined $ENV{'RBLSMTPD'} && $ENV{'RBLSMTPD'} ne '') {
|
if (defined $ENV{'RBLSMTPD'} && $ENV{'RBLSMTPD'} ne '') {
|
||||||
my $result = $ENV{'RBLSMTPD'};
|
my $result = $ENV{'RBLSMTPD'};
|
||||||
@ -346,6 +338,6 @@ sub get_reject_type {
|
|||||||
|
|
||||||
return $self->{_args}{reject_type} eq 'temp' ? DENYSOFT
|
return $self->{_args}{reject_type} eq 'temp' ? DENYSOFT
|
||||||
: $self->{_args}{reject_type} eq 'disconnect' ? DENY_DISCONNECT
|
: $self->{_args}{reject_type} eq 'disconnect' ? DENY_DISCONNECT
|
||||||
: DENY;
|
: $default;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,6 +88,8 @@ sub data_post_handler {
|
|||||||
return DECLINED;
|
return DECLINED;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return DECLINED if $self->is_immune();
|
||||||
|
|
||||||
my $body = $self->assemble_body( $transaction );
|
my $body = $self->assemble_body( $transaction );
|
||||||
|
|
||||||
my $message = load Mail::DomainKeys::Message(
|
my $message = load Mail::DomainKeys::Message(
|
||||||
|
@ -17,7 +17,7 @@ has configurable timeout periods (black/grey/white) to control whether
|
|||||||
connections are allowed, instead of using connection counts or rates.
|
connections are allowed, instead of using connection counts or rates.
|
||||||
|
|
||||||
Automatic whitelisting is enabled for relayclients, whitelisted hosts,
|
Automatic whitelisting is enabled for relayclients, whitelisted hosts,
|
||||||
whitelisted senders, TLS connections, p0f matches, and geoip matches.
|
whitelisted senders, p0f matches, and geoip matches.
|
||||||
|
|
||||||
=head1 TRIPLETS
|
=head1 TRIPLETS
|
||||||
|
|
||||||
@ -169,7 +169,7 @@ use strict;
|
|||||||
use warnings;
|
use warnings;
|
||||||
use Qpsmtpd::Constants;
|
use Qpsmtpd::Constants;
|
||||||
|
|
||||||
my $VERSION = '0.10';
|
my $VERSION = '0.11';
|
||||||
|
|
||||||
BEGIN { @AnyDBM_File::ISA = qw(DB_File GDBM_File NDBM_File) }
|
BEGIN { @AnyDBM_File::ISA = qw(DB_File GDBM_File NDBM_File) }
|
||||||
use AnyDBM_File;
|
use AnyDBM_File;
|
||||||
@ -268,6 +268,8 @@ sub greylist {
|
|||||||
join(',',map { $_ . '=' . $config->{$_} } sort keys %$config));
|
join(',',map { $_ . '=' . $config->{$_} } sort keys %$config));
|
||||||
|
|
||||||
return DECLINED if $self->is_immune();
|
return DECLINED if $self->is_immune();
|
||||||
|
return DECLINED if ! $self->is_p0f_match();
|
||||||
|
return DECLINED if $self->geoip_match();
|
||||||
|
|
||||||
my $db = $self->get_db_location();
|
my $db = $self->get_db_location();
|
||||||
my $lock = $self->get_db_lock( $db ) or return DECLINED;
|
my $lock = $self->get_db_lock( $db ) or return DECLINED;
|
||||||
@ -516,6 +518,8 @@ sub prune_db {
|
|||||||
sub p0f_match {
|
sub p0f_match {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
|
|
||||||
|
return if ! $self->{_args}{p0f};
|
||||||
|
|
||||||
my $p0f = $self->connection->notes('p0f');
|
my $p0f = $self->connection->notes('p0f');
|
||||||
if ( !$p0f || !ref $p0f ) { # p0f fingerprint info not found
|
if ( !$p0f || !ref $p0f ) { # p0f fingerprint info not found
|
||||||
$self->LOGINFO(LOGERROR, "p0f info missing");
|
$self->LOGINFO(LOGERROR, "p0f info missing");
|
||||||
@ -559,6 +563,8 @@ sub p0f_match {
|
|||||||
sub geoip_match {
|
sub geoip_match {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
|
|
||||||
|
return if ! $self->{_args}{geoip};
|
||||||
|
|
||||||
my $country = $self->connection->notes('geoip_country');
|
my $country = $self->connection->notes('geoip_country');
|
||||||
my $c_name = $self->connection->notes('geoip_country_name') || '';
|
my $c_name = $self->connection->notes('geoip_country_name') || '';
|
||||||
|
|
||||||
|
@ -25,6 +25,10 @@ to return in the SMTP conversation e.g.
|
|||||||
|
|
||||||
=cut
|
=cut
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Qpsmtpd::Constants;
|
||||||
|
|
||||||
sub register {
|
sub register {
|
||||||
my ($self, $qp, $denial ) = @_;
|
my ($self, $qp, $denial ) = @_;
|
||||||
@ -34,12 +38,25 @@ sub register {
|
|||||||
else {
|
else {
|
||||||
$self->{_rhsbl}->{DENY} = DENY;
|
$self->{_rhsbl}->{DENY} = DENY;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub hook_mail {
|
sub hook_mail {
|
||||||
my ($self, $transaction, $sender, %param) = @_;
|
my ($self, $transaction, $sender, %param) = @_;
|
||||||
|
|
||||||
|
return DECLINED if $self->is_immune();
|
||||||
|
|
||||||
|
if ($sender->format eq '<>') {
|
||||||
|
$self->log(LOGINFO, 'skip, null sender');
|
||||||
|
return DECLINED;
|
||||||
|
};
|
||||||
|
|
||||||
|
my %rhsbl_zones = map { (split /\s+/, $_, 2)[0,1] } $self->qp->config('rhsbl_zones');
|
||||||
|
|
||||||
|
if ( ! %rhsbl_zones ) {
|
||||||
|
$self->log(LOGINFO, 'skip, no zones');
|
||||||
|
return DECLINED;
|
||||||
|
};
|
||||||
|
|
||||||
my $res = new Net::DNS::Resolver;
|
my $res = new Net::DNS::Resolver;
|
||||||
my $sel = IO::Select->new();
|
my $sel = IO::Select->new();
|
||||||
my %rhsbl_zones_map = ();
|
my %rhsbl_zones_map = ();
|
||||||
@ -48,9 +65,6 @@ sub hook_mail {
|
|||||||
# here and pick up any results in the RCPT handler.
|
# here and pick up any results in the RCPT handler.
|
||||||
# MTAs gets confused when you reject mail during MAIL FROM:
|
# MTAs gets confused when you reject mail during MAIL FROM:
|
||||||
|
|
||||||
my %rhsbl_zones = map { (split /\s+/, $_, 2)[0,1] } $self->qp->config('rhsbl_zones');
|
|
||||||
|
|
||||||
if ($sender->format ne '<>' and %rhsbl_zones) {
|
|
||||||
push(my @hosts, $sender->host);
|
push(my @hosts, $sender->host);
|
||||||
#my $helo = $self->qp->connection->hello_host;
|
#my $helo = $self->qp->connection->hello_host;
|
||||||
#push(@hosts, $helo) if $helo && $helo ne $sender->host;
|
#push(@hosts, $helo) if $helo && $helo ne $sender->host;
|
||||||
@ -70,28 +84,29 @@ sub hook_mail {
|
|||||||
|
|
||||||
%{$self->{_rhsbl_zones_map}} = %rhsbl_zones_map;
|
%{$self->{_rhsbl_zones_map}} = %rhsbl_zones_map;
|
||||||
$transaction->notes('rhsbl_sockets', $sel);
|
$transaction->notes('rhsbl_sockets', $sel);
|
||||||
} else {
|
|
||||||
$self->log(LOGDEBUG, 'no RHS checks necessary');
|
|
||||||
}
|
|
||||||
|
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub hook_rcpt {
|
sub hook_rcpt {
|
||||||
my ($self, $transaction, $rcpt) = @_;
|
my ($self, $transaction, $rcpt) = @_;
|
||||||
my $host = $transaction->sender->host;
|
|
||||||
my $hello = $self->qp->connection->hello_host;
|
|
||||||
|
|
||||||
my $result = $self->process_sockets;
|
my $result = $self->process_sockets or do {
|
||||||
if ($result && defined($self->{_rhsbl_zones_map}{$result})) {
|
$self->log(LOGINFO, "pass");
|
||||||
|
return DECLINED;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if ( defined($self->{_rhsbl_zones_map}{$result}) ) {
|
||||||
|
my $host = $transaction->sender->host;
|
||||||
if ($result =~ /^$host\./ ) {
|
if ($result =~ /^$host\./ ) {
|
||||||
return ($self->{_rhsbl}->{DENY}, "Mail from $host rejected because it " . $self->{_rhsbl_zones_map}{$result});
|
return ($self->{_rhsbl}->{DENY}, "Mail from $host rejected because it " . $self->{_rhsbl_zones_map}{$result});
|
||||||
} else {
|
} else {
|
||||||
|
my $hello = $self->qp->connection->hello_host;
|
||||||
return ($self->{_rhsbl}->{DENY}, "Mail from HELO $hello rejected because it " . $self->{_rhsbl_zones_map}{$result});
|
return ($self->{_rhsbl}->{DENY}, "Mail from HELO $hello rejected because it " . $self->{_rhsbl_zones_map}{$result});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ($self->{_rhsbl}->{DENY}, $result) if $result;
|
return ($self->{_rhsbl}->{DENY}, $result);
|
||||||
return DECLINED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub process_sockets {
|
sub process_sockets {
|
||||||
|
@ -80,6 +80,8 @@ sub register {
|
|||||||
sub hook_mail {
|
sub hook_mail {
|
||||||
my ($self, $transaction, $sender, %param) = @_;
|
my ($self, $transaction, $sender, %param) = @_;
|
||||||
|
|
||||||
|
return (DECLINED) if $self->is_immune();
|
||||||
|
|
||||||
if ( ! $self->{_args}{reject} ) {
|
if ( ! $self->{_args}{reject} ) {
|
||||||
$self->log( LOGINFO, "skip: disabled in config" );
|
$self->log( LOGINFO, "skip: disabled in config" );
|
||||||
return (DECLINED);
|
return (DECLINED);
|
||||||
@ -91,7 +93,7 @@ sub hook_mail {
|
|||||||
return (DECLINED, "SPF - null sender");
|
return (DECLINED, "SPF - null sender");
|
||||||
};
|
};
|
||||||
|
|
||||||
if ( $self->is_relayclient() ) {
|
if ( $self->is_in_relayclients() ) {
|
||||||
return (DECLINED, "SPF - relaying permitted");
|
return (DECLINED, "SPF - relaying permitted");
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -189,16 +191,11 @@ sub hook_data_post {
|
|||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub is_relayclient {
|
sub is_in_relayclients {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
|
|
||||||
# If we are receiving from a relay permitted host, then we are probably
|
# If we are receiving from a relay permitted host, then we are probably
|
||||||
# not the delivery system, and so we shouldn't check
|
# not the delivery system, and so we shouldn't check
|
||||||
if ( $self->qp->connection->relay_client() ) {
|
|
||||||
$self->log( LOGINFO, "skip: relaying permitted (relay_client)" );
|
|
||||||
return 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
my $client_ip = $self->qp->connection->remote_ip;
|
my $client_ip = $self->qp->connection->remote_ip;
|
||||||
my @relay_clients = $self->qp->config('relayclients');
|
my @relay_clients = $self->qp->config('relayclients');
|
||||||
my $more_relay_clients = $self->qp->config('morerelayclients', 'map');
|
my $more_relay_clients = $self->qp->config('morerelayclients', 'map');
|
||||||
|
@ -163,15 +163,12 @@ sub register {
|
|||||||
sub hook_data_post {
|
sub hook_data_post {
|
||||||
my ($self, $transaction) = @_;
|
my ($self, $transaction) = @_;
|
||||||
|
|
||||||
|
return (DECLINED) if $self->is_immune();
|
||||||
|
|
||||||
if ( $transaction->data_size > 500_000 ) {
|
if ( $transaction->data_size > 500_000 ) {
|
||||||
$self->log(LOGINFO, "skip: too large (".$transaction->data_size.")");
|
$self->log(LOGINFO, "skip: too large (".$transaction->data_size.")");
|
||||||
return (DECLINED);
|
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 $SPAMD = $self->connect_to_spamd() or return (DECLINED);
|
||||||
my $username = $self->select_spamd_username( $transaction );
|
my $username = $self->select_spamd_username( $transaction );
|
||||||
|
@ -478,6 +478,8 @@ sub collect_results {
|
|||||||
sub data_handler {
|
sub data_handler {
|
||||||
my ($self, $transaction) = @_;
|
my ($self, $transaction) = @_;
|
||||||
|
|
||||||
|
return (DECLINED) if $self->is_immune();
|
||||||
|
|
||||||
my $queries = $self->lookup_start($transaction, sub {
|
my $queries = $self->lookup_start($transaction, sub {
|
||||||
my ($self, $name) = @_;
|
my ($self, $name) = @_;
|
||||||
return $self->send_query($name);
|
return $self->send_query($name);
|
||||||
|
@ -8,36 +8,36 @@ use Qpsmtpd::Address;
|
|||||||
sub register_tests {
|
sub register_tests {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
|
|
||||||
$self->register_test("test_badmailfrom_is_immune", 5);
|
$self->register_test("test_badmailfrom_is_immune_sender", 5);
|
||||||
$self->register_test("test_badmailfrom_match", 7);
|
$self->register_test("test_badmailfrom_match", 7);
|
||||||
$self->register_test("test_badmailfrom_hook_mail", 4);
|
$self->register_test("test_badmailfrom_hook_mail", 4);
|
||||||
$self->register_test("test_badmailfrom_hook_rcpt", 2);
|
$self->register_test("test_badmailfrom_hook_rcpt", 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
sub test_badmailfrom_is_immune {
|
sub test_badmailfrom_is_immune_sender {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
|
|
||||||
my $transaction = $self->qp->transaction;
|
my $transaction = $self->qp->transaction;
|
||||||
my $test_email = 'matt@test.com';
|
my $test_email = 'matt@test.com';
|
||||||
my $address = Qpsmtpd::Address->new( "<$test_email>" );
|
my $address = Qpsmtpd::Address->new( "<$test_email>" );
|
||||||
$transaction->sender($address);
|
$transaction->sender($address);
|
||||||
ok( $self->is_immune( $transaction->sender, [] ), "is_immune, empty list");
|
ok( $self->is_immune_sender( $transaction->sender, [] ), "empty list");
|
||||||
|
|
||||||
$address = Qpsmtpd::Address->new( '<>' );
|
$address = Qpsmtpd::Address->new( '<>' );
|
||||||
$transaction->sender($address);
|
$transaction->sender($address);
|
||||||
ok( $self->is_immune( $transaction->sender, ['bad@example.com'] ), "is_immune, null sender");
|
ok( $self->is_immune_sender( $transaction->sender, ['bad@example.com'] ), "null sender");
|
||||||
|
|
||||||
$address = Qpsmtpd::Address->new( '<matt@>' );
|
$address = Qpsmtpd::Address->new( '<matt@>' );
|
||||||
$transaction->sender($address);
|
$transaction->sender($address);
|
||||||
ok( $self->is_immune( $transaction->sender, ['bad@example.com'] ), "is_immune, missing host");
|
ok( $self->is_immune_sender( $transaction->sender, ['bad@example.com'] ), "missing host");
|
||||||
|
|
||||||
$address = Qpsmtpd::Address->new( '<@example.com>' );
|
$address = Qpsmtpd::Address->new( '<@example.com>' );
|
||||||
$transaction->sender($address);
|
$transaction->sender($address);
|
||||||
ok( $self->is_immune( $transaction->sender, ['bad@example.com'] ), "is_immune, missing user");
|
ok( $self->is_immune_sender( $transaction->sender, ['bad@example.com'] ), "missing user");
|
||||||
|
|
||||||
$address = Qpsmtpd::Address->new( '<matt@example.com>' );
|
$address = Qpsmtpd::Address->new( '<matt@example.com>' );
|
||||||
$transaction->sender($address);
|
$transaction->sender($address);
|
||||||
ok( ! $self->is_immune( $transaction->sender, ['bad@example.com'] ), "is_immune, false");
|
ok( ! $self->is_immune_sender( $transaction->sender, ['bad@example.com'] ), "false");
|
||||||
};
|
};
|
||||||
|
|
||||||
sub test_badmailfrom_hook_mail {
|
sub test_badmailfrom_hook_mail {
|
||||||
|
147
t/plugin_tests/check_earlytalker
Normal file
147
t/plugin_tests/check_earlytalker
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
#!perl -w
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Qpsmtpd::Constants;
|
||||||
|
|
||||||
|
sub register_tests {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
|
$self->register_test('test_reject_type', 3);
|
||||||
|
$self->register_test('test_log_and_pass', 1);
|
||||||
|
$self->register_test('test_log_and_deny', 3);
|
||||||
|
$self->register_test('test_data_handler', 3);
|
||||||
|
$self->register_test('test_connect_handler', 3);
|
||||||
|
$self->register_test('test_apr_data_handler', 3);
|
||||||
|
$self->register_test('test_apr_connect_handler', 3);
|
||||||
|
$self->register_test('test_mail_handler', 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
sub test_apr_connect_handler {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
|
$self->{_args}{'check-at'} = undef;
|
||||||
|
my ($code, $mess) = $self->apr_connect_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "no check-at set");
|
||||||
|
|
||||||
|
$self->{_args}{'check-at'}{'DATA'} = 1;
|
||||||
|
$self->qp->connection->notes('whitelisthost', 1);
|
||||||
|
($code, $mess) = $self->apr_connect_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "whitelisted host");
|
||||||
|
|
||||||
|
$self->qp->connection->notes('whitelisthost', 0);
|
||||||
|
($code, $mess) = $self->apr_connect_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "not sure");
|
||||||
|
};
|
||||||
|
|
||||||
|
sub test_apr_data_handler {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
|
$self->{_args}{'check-at'} = undef;
|
||||||
|
my ($code, $mess) = $self->apr_data_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "no check-at set");
|
||||||
|
|
||||||
|
$self->{_args}{'check-at'}{'DATA'} = 1;
|
||||||
|
$self->qp->connection->notes('whitelisthost', 1);
|
||||||
|
($code, $mess) = $self->apr_data_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "whitelisted host");
|
||||||
|
|
||||||
|
$self->qp->connection->notes('whitelisthost', 0);
|
||||||
|
($code, $mess) = $self->apr_data_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "not sure");
|
||||||
|
};
|
||||||
|
|
||||||
|
sub test_connect_handler {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
|
$self->{_args}{'check-at'} = undef;
|
||||||
|
my ($code, $mess) = $self->connect_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "no check-at set");
|
||||||
|
|
||||||
|
$self->{_args}{'check-at'}{'CONNECT'} = 1;
|
||||||
|
$self->qp->connection->notes('whitelisthost', 1);
|
||||||
|
($code, $mess) = $self->connect_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "whitelisted host");
|
||||||
|
|
||||||
|
$self->qp->connection->notes('whitelisthost', 0);
|
||||||
|
($code, $mess) = $self->connect_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "not sure");
|
||||||
|
};
|
||||||
|
|
||||||
|
sub test_data_handler {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
|
$self->{_args}{'check-at'} = undef;
|
||||||
|
my ($code, $mess) = $self->data_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "no check-at set");
|
||||||
|
|
||||||
|
$self->{_args}{'check-at'}{'DATA'} = 1;
|
||||||
|
$self->qp->connection->notes('whitelisthost', 1);
|
||||||
|
($code, $mess) = $self->data_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "whitelisted host");
|
||||||
|
|
||||||
|
$self->qp->connection->notes('whitelisthost', 0);
|
||||||
|
($code, $mess) = $self->data_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "not sure");
|
||||||
|
};
|
||||||
|
|
||||||
|
sub test_log_and_pass {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
|
my ($code, $mess) = $self->log_and_pass();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "default");
|
||||||
|
};
|
||||||
|
|
||||||
|
sub test_log_and_deny {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
|
$self->{_args}{reject_type} = undef;
|
||||||
|
|
||||||
|
my ($code, $mess) = $self->log_and_deny();
|
||||||
|
cmp_ok( $code, '==', DENY, "default");
|
||||||
|
|
||||||
|
$self->{_args}{reject_type} = 'temp';
|
||||||
|
($code, $mess) = $self->log_and_deny();
|
||||||
|
cmp_ok( $code, '==', DENYSOFT, "bad, temp");
|
||||||
|
|
||||||
|
$self->{_args}{reject_type} = 'disconnect';
|
||||||
|
($code, $mess) = $self->log_and_deny();
|
||||||
|
cmp_ok( $code, '==', DENY_DISCONNECT, "bad, disconnect");
|
||||||
|
};
|
||||||
|
|
||||||
|
sub test_mail_handler {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
|
$self->{_args}{reject_type} = undef;
|
||||||
|
$self->qp->connection->notes('earlytalker', 0);
|
||||||
|
|
||||||
|
my ($code, $mess) = $self->mail_handler();
|
||||||
|
cmp_ok( $code, '==', DECLINED, "good");
|
||||||
|
|
||||||
|
$self->qp->connection->notes('earlytalker', 1);
|
||||||
|
($code, $mess) = $self->mail_handler();
|
||||||
|
cmp_ok( $code, '==', DENY, "bad");
|
||||||
|
|
||||||
|
$self->{_args}{reject_type} = 'temp';
|
||||||
|
($code, $mess) = $self->mail_handler();
|
||||||
|
cmp_ok( $code, '==', DENYSOFT, "bad, temp");
|
||||||
|
|
||||||
|
$self->{_args}{reject_type} = 'disconnect';
|
||||||
|
($code, $mess) = $self->mail_handler();
|
||||||
|
cmp_ok( $code, '==', DENY_DISCONNECT, "bad, disconnect");
|
||||||
|
};
|
||||||
|
|
||||||
|
sub test_reject_type {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
|
$self->{_args}{reject_type} = undef;
|
||||||
|
cmp_ok( $self->get_reject_type(), '==', DENY, "default");
|
||||||
|
|
||||||
|
$self->{_args}{reject_type} = 'temp';
|
||||||
|
cmp_ok( $self->get_reject_type(), '==', DENYSOFT, "defer");
|
||||||
|
|
||||||
|
$self->{_args}{reject_type} = 'disconnect';
|
||||||
|
cmp_ok( $self->get_reject_type(), '==', DENY_DISCONNECT, "disconnect");
|
||||||
|
};
|
||||||
|
|
@ -19,17 +19,16 @@ sub register_tests {
|
|||||||
sub test_ip_whitelisted {
|
sub test_ip_whitelisted {
|
||||||
my $self = shift;
|
my $self = shift;
|
||||||
|
|
||||||
$self->qp->connection->remote_ip('10.1.1.1');
|
$self->qp->connection->remote_ip('192.168.99.5');
|
||||||
|
ok( $self->ip_whitelisted(), "+");
|
||||||
|
|
||||||
$self->qp->connection->relay_client(1);
|
$self->qp->connection->remote_ip('192.168.99.6');
|
||||||
ok( $self->ip_whitelisted('10.1.1.1'), "yes, +");
|
ok( ! $self->ip_whitelisted(), "-");
|
||||||
|
|
||||||
$self->qp->connection->relay_client(0);
|
$self->qp->connection->remote_ip('192.168.99.5');
|
||||||
ok( ! $self->ip_whitelisted('10.1.1.1'), "no, -");
|
$self->qp->connection->notes('whitelisthost', 'hello honey!');
|
||||||
|
ok( $self->ip_whitelisted(), "+");
|
||||||
$self->connection->notes('whitelisthost', 'hello honey!');
|
$self->qp->connection->notes('whitelisthost', undef);
|
||||||
ok( $self->ip_whitelisted('10.1.1.1'), "yes, +");
|
|
||||||
$self->connection->notes('whitelisthost', undef);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sub test_is_set_rblsmtpd {
|
sub test_is_set_rblsmtpd {
|
||||||
|
Loading…
Reference in New Issue
Block a user