extracted config*() from Qpsmtpd.pm -> Config.pm

* includes full test converage for Qpsmtpd::Config
    * folded t/config.t into t/qpsmtpd-config.t
* includes additional tests for Qpsmtpd
    * folded t/tempstuff into t/qpsmtpd.t
* PBP adjustments here and there
* other tweaks to handle test warnings
This commit is contained in:
Matt Simerson 2014-09-15 23:41:31 -07:00
parent 79d2b99211
commit ebdb25a4bd
19 changed files with 408 additions and 322 deletions

View File

@ -41,6 +41,7 @@ lib/Qpsmtpd/Address.pm
lib/Qpsmtpd/Auth.pm lib/Qpsmtpd/Auth.pm
lib/Qpsmtpd/Base.pm lib/Qpsmtpd/Base.pm
lib/Qpsmtpd/Command.pm lib/Qpsmtpd/Command.pm
lib/Qpsmtpd/Config.pm
lib/Qpsmtpd/ConfigServer.pm lib/Qpsmtpd/ConfigServer.pm
lib/Qpsmtpd/Connection.pm lib/Qpsmtpd/Connection.pm
lib/Qpsmtpd/Constants.pm lib/Qpsmtpd/Constants.pm
@ -166,7 +167,6 @@ run.tcpserver
STATUS STATUS
t/addresses.t t/addresses.t
t/auth.t t/auth.t
t/config.t
t/config/badhelo t/config/badhelo
t/config/badrcptto t/config/badrcptto
t/config/dnsbl_allow t/config/dnsbl_allow
@ -178,6 +178,8 @@ t/config/plugins
t/config/public_suffix_list t/config/public_suffix_list
t/config/rcpthosts t/config/rcpthosts
t/config/relayclients t/config/relayclients
t/config/size_threshold
t/config/test_config_file
t/helo.t t/helo.t
t/misc.t t/misc.t
t/plugin_tests.t t/plugin_tests.t
@ -210,10 +212,10 @@ t/plugin_tests/user_config
t/plugin_tests/virus/clamdscan t/plugin_tests/virus/clamdscan
t/qpsmtpd-address.t t/qpsmtpd-address.t
t/qpsmtpd-base.t t/qpsmtpd-base.t
t/qpsmtpd-config.t
t/qpsmtpd-smtp.t t/qpsmtpd-smtp.t
t/qpsmtpd.t t/qpsmtpd.t
t/rset.t t/rset.t
t/tempstuff.t
t/Test/Qpsmtpd.pm t/Test/Qpsmtpd.pm
t/Test/Qpsmtpd/Plugin.pm t/Test/Qpsmtpd/Plugin.pm
UPGRADING.pod UPGRADING.pod

View File

@ -5,11 +5,10 @@ use strict;
our $VERSION = "0.95"; our $VERSION = "0.95";
use vars qw($TraceLevel $Spool_dir $Size_threshold); use vars qw($TraceLevel $Spool_dir $Size_threshold);
use Sys::Hostname;
use lib 'lib'; use lib 'lib';
use base 'Qpsmtpd::Base'; use base 'Qpsmtpd::Base';
use Qpsmtpd::Address; use Qpsmtpd::Address;
use Qpsmtpd::Config;
use Qpsmtpd::Constants; use Qpsmtpd::Constants;
my $git; my $git;
@ -21,12 +20,6 @@ if (-e ".git") {
} }
our $hooks = {}; our $hooks = {};
my %defaults = (
me => hostname,
timeout => 1200,
);
my $_config_cache = {};
our %config_dir_memo;
our $LOGGING_LOADED = 0; our $LOGGING_LOADED = 0;
@ -36,10 +29,9 @@ sub _restart {
if ($args{restart}) { if ($args{restart}) {
# reset all global vars to defaults # reset all global vars to defaults
$self->clear_config_cache; $self->conf->clear_cache();
$hooks = {}; $hooks = {};
$LOGGING_LOADED = 0; $LOGGING_LOADED = 0;
%config_dir_memo = ();
$TraceLevel = LOGWARN; $TraceLevel = LOGWARN;
$Spool_dir = undef; $Spool_dir = undef;
$Size_threshold = undef; $Size_threshold = undef;
@ -60,11 +52,11 @@ sub load_logging {
my $configdir = $self->config_dir("logging"); my $configdir = $self->config_dir("logging");
my $configfile = "$configdir/logging"; my $configfile = "$configdir/logging";
my @loggers = $self->_config_from_file($configfile, 'logging'); my @loggers = $self->conf->from_file($configfile, 'logging');
$configdir = $self->config_dir('plugin_dirs'); $configdir = $self->config_dir('plugin_dirs');
$configfile = "$configdir/plugin_dirs"; $configfile = "$configdir/plugin_dirs";
my @plugin_dirs = $self->_config_from_file($configfile, 'plugin_dirs'); my @plugin_dirs = $self->conf->from_file($configfile, 'plugin_dirs');
unless (@plugin_dirs) { unless (@plugin_dirs) {
my ($name) = ($0 =~ m!(.*?)/([^/]+)$!); my ($name) = ($0 =~ m!(.*?)/([^/]+)$!);
@plugin_dirs = ("$name/plugins"); @plugin_dirs = ("$name/plugins");
@ -81,7 +73,7 @@ sub load_logging {
$configdir = $self->config_dir("loglevel"); $configdir = $self->config_dir("loglevel");
$configfile = "$configdir/loglevel"; $configfile = "$configdir/loglevel";
$TraceLevel = $self->_config_from_file($configfile, 'loglevel'); $TraceLevel = $self->conf->from_file($configfile, 'loglevel');
unless (defined($TraceLevel) and $TraceLevel =~ /^\d+$/) { unless (defined($TraceLevel) and $TraceLevel =~ /^\d+$/) {
$TraceLevel = LOGWARN; # Default if no loglevel file found. $TraceLevel = LOGWARN; # Default if no loglevel file found.
@ -94,7 +86,7 @@ sub load_logging {
sub trace_level { return $TraceLevel; } sub trace_level { return $TraceLevel; }
sub init_logger { # needed for compatibility purposes sub init_logger { # needed for compatibility
shift->trace_level(); shift->trace_level();
} }
@ -106,17 +98,11 @@ sub log {
sub varlog { sub varlog {
my ($self, $trace) = (shift, shift); my ($self, $trace) = (shift, shift);
my ($hook, $plugin, @log); my ($hook, $plugin, @log);
if ($#_ == 0) { # log itself if ($#_ == 0) { (@log) = @_; } # log itself
(@log) = @_; elsif ($#_ == 1) { ($hook, @log) = @_; } # plus the hook
} else { ($hook, $plugin, @log) = @_; } # from a plugin
elsif ($#_ == 1) { # plus the hook
($hook, @log) = @_;
}
else { # called from plugin
($hook, $plugin, @log) = @_;
}
$self->load_logging; # in case we don't have this loaded yet $self->load_logging;
my ($rc) = my ($rc) =
$self->run_hooks_no_respond("logging", $trace, $hook, $plugin, @log) $self->run_hooks_no_respond("logging", $trace, $hook, $plugin, @log)
@ -135,15 +121,14 @@ sub varlog {
warn join(' ', $$ . $prefix, @log), "\n"; warn join(' ', $$ . $prefix, @log), "\n";
} }
sub clear_config_cache { sub conf {
$_config_cache = {}; my $self = shift;
if (!$self->{_config}) {
$self->{_config} = Qpsmtpd::Config->new();
};
return $self->{_config};
} }
#
# method to get the configuration. It just calls get_qmail_config by
# default, but it could be overwritten to look configuration up in a
# database or whatever.
#
sub config { sub config {
my ($self, $c, $type) = @_; my ($self, $c, $type) = @_;
@ -169,30 +154,17 @@ sub config {
return wantarray ? @config : $config[0]; return wantarray ? @config : $config[0];
}; };
# then get_qmail_config # then qmail
@config = $self->get_qmail_config($c, $type); @config = $self->conf->get_qmail($c, $type);
return wantarray ? @config : $config[0] if @config; return wantarray ? @config : $config[0] if @config;
# then the default, if any # then the default, which may be undefined
if (exists $defaults{$c}) { return $self->conf->default($c);
return wantarray ? ($defaults{$c}) : $defaults{$c};
};
return;
} }
sub config_dir { sub config_dir {
my ($self, $config) = @_; my $self = shift;
if (exists $config_dir_memo{$config}) { return $self->conf->config_dir(@_);
return $config_dir_memo{$config};
}
my $configdir = ($ENV{QMAIL} || '/var/qmail') . '/control';
my ($path) = ($ENV{PROCESS} ? $ENV{PROCESS} : $0) =~ m!(.*?)/([^/]+)$!;
$configdir = "$path/config" if -e "$path/config/$config";
if (exists $ENV{QPSMTPD_CONFIG}) {
$ENV{QPSMTPD_CONFIG} =~ /^(.*)$/; # detaint
$configdir = $1 if -e "$1/$config";
}
return $config_dir_memo{$config} = $configdir;
} }
sub plugin_dirs { sub plugin_dirs {
@ -206,143 +178,6 @@ sub plugin_dirs {
return @plugin_dirs; return @plugin_dirs;
} }
sub get_qmail_config {
my ($self, $config, $type) = @_;
$self->log(LOGDEBUG, "trying to get config for $config");
my $configdir = $self->config_dir($config);
my $configfile = "$configdir/$config";
# CDB config support really should be moved to a plugin
if ($type and $type eq "map") {
return $self->get_qmail_config_map($config, $configfile);
}
return $self->_config_from_file($configfile, $config);
}
sub get_qmail_config_map {
my ($self, $config, $configfile) = @_;
unless (-e $configfile . ".cdb") {
$_config_cache->{$config} ||= [];
return +{};
}
eval { require CDB_File };
if ($@) {
$self->log(LOGERROR,
"No CDB Support! Did NOT read $configfile.cdb, could not load CDB_File module: $@"
);
return +{};
}
my %h;
unless (tie(%h, 'CDB_File', "$configfile.cdb")) {
$self->log(LOGERROR, "tie of $configfile.cdb failed: $!");
return +{};
}
# We explicitly don't cache cdb entries. The assumption is that
# the data is in a CDB file in the first place because there's
# lots of data and the cache hit ratio would be low.
return \%h;
}
sub _config_from_file {
my ($self, $configfile, $config, $visited) = @_;
unless (-e $configfile) {
$_config_cache->{$config} ||= [];
return;
}
$visited ||= [];
push @$visited, $configfile;
open my $CF, '<', $configfile or do {
warn "$$ could not open configfile $configfile: $!";
return;
};
my @config = <$CF>;
close $CF;
chomp @config;
@config = grep { length($_) and $_ !~ m/^\s*#/ and $_ =~ m/\S/ } @config;
for (@config) { s/^\s+//; s/\s+$//; } # trim leading/trailing whitespace
my $pos = 0;
while ($pos < @config) {
# recursively pursue an $include reference, if found. An inclusion which
# begins with a leading slash is interpreted as a path to a file and will
# supercede the usual config path resolution. Otherwise, the normal
# config_dir() lookup is employed (the location in which the inclusion
# appeared receives no special precedence; possibly it should, but it'd
# be complicated beyond justifiability for so simple a config system.
if ($config[$pos] =~ /^\s*\$include\s+(\S+)\s*$/) {
my ($includedir, $inclusion) = ('', $1);
splice @config, $pos, 1; # remove the $include line
if ($inclusion !~ /^\//) {
$includedir = $self->config_dir($inclusion);
$inclusion = "$includedir/$inclusion";
}
if (grep($_ eq $inclusion, @{$visited})) {
$self->log(LOGERROR,
"Circular \$include reference in config $config:");
$self->log(LOGERROR, "From $visited->[0]:");
$self->log(LOGERROR, " includes $_")
for (@{$visited}[1 .. $#{$visited}], $inclusion);
return wantarray ? () : undef;
}
push @{$visited}, $inclusion;
for my $inc ($self->expand_inclusion_($inclusion, $configfile)) {
my @insertion =
$self->_config_from_file($inc, $config, $visited);
splice @config, $pos, 0, @insertion; # insert the inclusion
$pos += @insertion;
}
}
else {
$pos++;
}
}
$_config_cache->{$config} = \@config;
return wantarray ? @config : $config[0];
}
sub expand_inclusion_ {
my $self = shift;
my $inclusion = shift;
my $context = shift;
my @includes;
if (-d $inclusion) {
$self->log(LOGDEBUG, "inclusion of directory $inclusion from $context");
if (opendir(INCD, $inclusion)) {
@includes = map { "$inclusion/$_" }
(grep { -f "$inclusion/$_" and !/^\./ } sort readdir INCD);
closedir INCD;
}
else {
$self->log(LOGERROR,
"Couldn't open directory $inclusion,"
. " referenced from $context ($!)"
);
}
}
else {
$self->log(LOGDEBUG, "inclusion of file $inclusion from $context");
@includes = ($inclusion);
}
return @includes;
}
sub load_plugins { sub load_plugins {
my $self = shift; my $self = shift;

View File

@ -3,6 +3,10 @@ use strict;
use Net::IP; use Net::IP;
sub new {
return bless {}, shift;
};
sub tildeexp { sub tildeexp {
my ($self, $path) = @_; my ($self, $path) = @_;
$path =~ s{^~([^/]*)} { $path =~ s{^~([^/]*)} {

192
lib/Qpsmtpd/Config.pm Normal file
View File

@ -0,0 +1,192 @@
package Qpsmtpd::Config;
use strict;
use warnings;
use Sys::Hostname;
use lib 'lib';
use parent 'Qpsmtpd::Base';
use Qpsmtpd::Constants;
our $_config_cache = {};
our %dir_memo;
our %defaults = (
me => hostname,
timeout => 1200,
);
sub log {
my ($self, $trace, @log) = @_;
# logging methods attempt to read config files, this log() prevents that
# until after logging has fully loaded
return if $trace > LOGWARN;
no warnings 'once';
if ($Qpsmtpd::LOGGING_LOADED) {
return Qpsmtpd->log($trace, @log);
}
warn join(' ', $$, @log) . "\n";
}
sub config_dir {
my ($self, $config) = @_;
if (exists $dir_memo{$config}) {
return $dir_memo{$config};
}
my $configdir = ($ENV{QMAIL} || '/var/qmail') . '/control';
my ($path) = ($ENV{PROCESS} ? $ENV{PROCESS} : $0) =~ m!(.*?)/([^/]+)$!;
$configdir = "$path/config" if -e "$path/config/$config";
if (exists $ENV{QPSMTPD_CONFIG}) {
$ENV{QPSMTPD_CONFIG} =~ /^(.*)$/; # detaint
$configdir = $1 if -e "$1/$config";
}
return $dir_memo{$config} = $configdir;
}
sub clear_cache {
$_config_cache = {};
%dir_memo = ();
}
sub default {
my ($self, $def) = @_;
return if ! $defaults{$def};
return wantarray ? ($defaults{$def}) : $defaults{$def};
}
sub get_qmail {
my ($self, $config, $type) = @_;
$self->log(LOGDEBUG, "trying to get config for $config");
my $configdir = $self->config_dir($config);
my $configfile = "$configdir/$config";
# CDB config support really should be moved to a plugin
if ($type and $type eq "map") {
return $self->get_qmail_map($config, $configfile);
}
return $self->from_file($configfile, $config);
}
sub get_qmail_map {
my ($self, $config, $configfile) = @_;
if (!-e $configfile . ".cdb") {
$_config_cache->{$config} ||= [];
return +{};
}
eval { require CDB_File };
if ($@) {
$self->log(LOGERROR,
"No CDB Support! Did NOT read $configfile.cdb, could not load CDB_File module: $@"
);
return +{};
}
my %h;
unless (tie(%h, 'CDB_File', "$configfile.cdb")) {
$self->log(LOGERROR, "tie of $configfile.cdb failed: $!");
return +{};
}
# We explicitly don't cache cdb entries. The assumption is that
# the data is in a CDB file in the first place because there's
# lots of data and the cache hit ratio would be low.
return \%h;
}
sub from_file {
my ($self, $configfile, $config, $visited) = @_;
if (!-e $configfile) {
$_config_cache->{$config} ||= [];
return;
}
$visited ||= [];
push @$visited, $configfile;
open my $CF, '<', $configfile or do {
warn "$$ could not open configfile $configfile: $!";
return;
};
my @config = <$CF>;
close $CF;
chomp @config;
@config = grep { length($_) and $_ !~ m/^\s*#/ and $_ =~ m/\S/ } @config;
for (@config) { s/^\s+//; s/\s+$//; } # trim leading/trailing whitespace
my $pos = 0;
while ($pos < @config) {
# recursively pursue an $include reference, if found. An inclusion which
# begins with a leading slash is interpreted as a path to a file and will
# supercede the usual config path resolution. Otherwise, the normal
# config_dir() lookup is employed (the location in which the inclusion
# appeared receives no special precedence; possibly it should, but it'd
# be complicated beyond justifiability for so simple a config system.
if ($config[$pos] =~ /^\s*\$include\s+(\S+)\s*$/) {
my ($includedir, $inclusion) = ('', $1);
splice @config, $pos, 1; # remove the $include line
if ($inclusion !~ /^\//) {
$includedir = $self->config_dir($inclusion);
$inclusion = "$includedir/$inclusion";
}
if (grep($_ eq $inclusion, @{$visited})) {
$self->log(LOGERROR,
"Circular \$include reference in config $config:");
$self->log(LOGERROR, "From $visited->[0]:");
$self->log(LOGERROR, " includes $_")
for (@{$visited}[1 .. $#{$visited}], $inclusion);
return wantarray ? () : undef;
}
push @{$visited}, $inclusion;
for my $inc ($self->expand_inclusion_($inclusion, $configfile)) {
my @insertion = $self->from_file($inc, $config, $visited);
splice @config, $pos, 0, @insertion; # insert the inclusion
$pos += @insertion;
}
}
else {
$pos++;
}
}
$_config_cache->{$config} = \@config;
return wantarray ? @config : $config[0];
}
sub expand_inclusion_ {
my $self = shift;
my $inclusion = shift;
my $context = shift;
my @includes;
if (-d $inclusion) {
$self->log(LOGDEBUG, "inclusion of directory $inclusion from $context");
if (opendir(INCD, $inclusion)) {
@includes = map { "$inclusion/$_" }
(grep { -f "$inclusion/$_" and !/^\./ } sort readdir INCD);
closedir INCD;
}
else {
$self->log(LOGERROR,
"Couldn't open directory $inclusion,"
. " referenced from $context ($!)"
);
}
}
else {
$self->log(LOGDEBUG, "inclusion of file $inclusion from $context");
@includes = ($inclusion);
}
return @includes;
}
1;

View File

@ -142,9 +142,9 @@ sub reset_transaction {
sub connection { sub connection {
my $self = shift; my $self = shift;
@_ and $self->{_connection} = shift; if (@_) { $self->{_connection} = shift; }
return $self->{_connection} return $self->{_connection} if $self->{_connection};
|| ($self->{_connection} = Qpsmtpd::Connection->new()); return $self->{_connection} = Qpsmtpd::Connection->new();
} }
sub helo { sub helo {

View File

@ -30,8 +30,9 @@ sub hook_mail {
my $from = lc($sender->user) . '@' . $host; my $from = lc($sender->user) . '@' . $host;
for my $bad (@badmailfromto) { for my $bad (@badmailfromto) {
next if !$bad;
$bad =~ s/^\s*(\S+).*/$1/; $bad =~ s/^\s*(\S+).*/$1/;
next unless $bad; next if !$bad;
$bad = lc $bad; $bad = lc $bad;
if ($bad !~ m/\@/) { if ($bad !~ m/\@/) {
$self->log(LOGWARN, 'bad config, no @ sign in ' . $bad); $self->log(LOGWARN, 'bad config, no @ sign in ' . $bad);

View File

@ -157,24 +157,26 @@ Adjust the quantity of logging for this plugin. See docs/logging.pod
Written by Gavin Carr <gavin@openfusion.com.au>. Written by Gavin Carr <gavin@openfusion.com.au>.
nfslock feature by JT Moree <jtmoree@kahalacorp.com> - 2007-01-22 2007-01-22 - nfslock feature by JT Moree <jtmoree@kahalacorp.com>
p0f feature by Matt Simerson <msimerson@cpan.org> - 2010-05-03 2010-05-03 - p0f feature by Matt Simerson <msimerson@cpan.org>
geoip, loglevel, reject added. Refactored into subs - Matt Simerson - 2012-05 2012-05 - geoip, loglevel, reject added. Refactored into subs by Matt Simerson
=cut =cut
use strict; use strict;
use warnings; use warnings;
use Net::IP;
use Qpsmtpd::Constants; use Qpsmtpd::Constants;
my $VERSION = '0.11'; my $VERSION = '0.12';
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;
use Fcntl qw(:DEFAULT :flock LOCK_EX LOCK_NB); use Fcntl qw(:DEFAULT :flock LOCK_EX LOCK_NB);
use Net::IP;
my $DENYMSG = "This mail is temporarily denied"; my $DENYMSG = "This mail is temporarily denied";
my ($QPHOME) = ($0 =~ m!(.*?)/([^/]+)$!); my ($QPHOME) = ($0 =~ m!(.*?)/([^/]+)$!);
@ -197,9 +199,10 @@ my %DEFAULTS = (
sub register { sub register {
my ($self, $qp, %arg) = @_; my ($self, $qp, %arg) = @_;
my $c = $self->qp->config('denysoft_greylist');
my $config = { my $config = {
%DEFAULTS, %DEFAULTS,
map { split /\s+/, $_, 2 } $self->qp->config('denysoft_greylist'), ($c ? map { split /\s+/, $_, 2 } $c : ()),
%arg %arg
}; };
if (my @bad = grep { !exists $PERMITTED_ARGS{$_} } sort keys %$config) { if (my @bad = grep { !exists $PERMITTED_ARGS{$_} } sort keys %$config) {
@ -265,9 +268,8 @@ sub hook_data {
return DECLINED unless $transaction->notes('greylist'); return DECLINED unless $transaction->notes('greylist');
# Decline if ALL recipients are whitelisted # Decline if ALL recipients are whitelisted
if (($transaction->notes('whitelistrcpt') || 0) == my $recips = scalar $transaction->recipients || 0;
scalar($transaction->recipients)) if (($transaction->notes('whitelistrcpt') || 0) == $recips) {
{
$self->log(LOGWARN, "skip: all recipients whitelisted"); $self->log(LOGWARN, "skip: all recipients whitelisted");
return DECLINED; return DECLINED;
} }
@ -370,7 +372,7 @@ sub get_db_key {
sub get_db_tie { sub get_db_tie {
my ($self, $db, $lock) = @_; my ($self, $db, $lock) = @_;
tie(my %db, 'AnyDBM_File', $db, O_CREAT | O_RDWR, 0600) or do { tie(my %db, 'AnyDBM_File', $db, O_CREAT | O_RDWR, oct('0600')) or do {
$self->log(LOGCRIT, "tie to database $db failed: $!"); $self->log(LOGCRIT, "tie to database $db failed: $!");
close $lock; close $lock;
return; return;
@ -419,7 +421,7 @@ sub get_db_lock {
return $self->get_db_lock_nfs($db) if $self->{_args}{nfslock}; return $self->get_db_lock_nfs($db) if $self->{_args}{nfslock};
# Check denysoft db # Check denysoft db
open(my $lock, ">$db.lock") or do { open(my $lock, '>', "$db.lock") or do {
$self->log(LOGCRIT, "opening lockfile failed: $!"); $self->log(LOGCRIT, "opening lockfile failed: $!");
return; return;
}; };
@ -450,7 +452,7 @@ sub get_db_lock_nfs {
return; return;
}; };
open(my $lock, "+<$db.lock") or do { open(my $lock, '+<', "$db.lock") or do {
$self->log(LOGCRIT, "opening nfs lockfile failed: $!"); $self->log(LOGCRIT, "opening nfs lockfile failed: $!");
return; return;
}; };

View File

@ -129,17 +129,16 @@ sub register {
$self->log(LOGERROR, "Bad parameters for the clamdscan plugin") if @_ % 2; $self->log(LOGERROR, "Bad parameters for the clamdscan plugin") if @_ % 2;
$self->{'_args'} = {@_}; $self->{'_args'} = {@_};
eval 'use ClamAV::Client'; eval 'use ClamAV::Client'; ## no critic (Stringy)
if ($@) { if ($@) {
warn "unable to load ClamAV::Client\n";
$self->log(LOGERROR, "unable to load ClamAV::Client"); $self->log(LOGERROR, "unable to load ClamAV::Client");
return; return;
} }
# Set some sensible defaults # Set sensible defaults
$self->{'_args'}{'deny_viruses'} ||= 'yes'; $self->{_args}{deny_viruses} ||= 'yes';
$self->{'_args'}{'max_size'} ||= 1024; $self->{_args}{max_size} ||= 1024;
$self->{'_args'}{'scan_all'} ||= 1; $self->{_args}{scan_all} ||= 1;
for my $setting ('deny_viruses', 'defer_on_error') { for my $setting ('deny_viruses', 'defer_on_error') {
next unless $self->{'_args'}{$setting}; next unless $self->{'_args'}{$setting};
if (lc $self->{'_args'}{$setting} eq 'no') { if (lc $self->{'_args'}{$setting} eq 'no') {
@ -241,9 +240,11 @@ sub get_filename {
} }
if (!-f $filename) { if (!-f $filename) {
$self->log(LOGERROR, "spool file missing! Attempting to respool"); if ($transaction->data_size) {
$transaction->body_spool; $self->log(LOGERROR, "spool file missing! Attempting to respool");
$filename = $transaction->body_filename; $transaction->body_spool;
$filename = $transaction->body_filename;
};
if (!-f $filename) { if (!-f $filename) {
$self->log(LOGERROR, "skip: failed spool to $filename! Giving up"); $self->log(LOGERROR, "skip: failed spool to $filename! Giving up");
return; return;
@ -264,14 +265,14 @@ sub set_permission {
my $dir_mode = (stat($self->spool_dir()))[2]; my $dir_mode = (stat($self->spool_dir()))[2];
$self->log(LOGDEBUG, "spool dir mode: $dir_mode"); $self->log(LOGDEBUG, "spool dir mode: $dir_mode");
if ($dir_mode & 0010 || $dir_mode & 0001) { if ($dir_mode & oct('0010') || $dir_mode & oct('0001')) {
# match the spool file mode with the mode of the directory -- add # match the spool file mode with the mode of the directory -- add
# the read bit for group, world, or both, depending on what the # the read bit for group, world, or both, depending on what the
# spool dir had, and strip all other bits, especially the sticky bit # spool dir had, and strip all other bits, especially the sticky bit
my $fmode = my $fmode =
($dir_mode & 0044) | ($dir_mode & 0010 ? 0040 : 0) | ($dir_mode & oct('0044')) | ($dir_mode & oct('0010') ? oct('0040') : 0) |
($dir_mode & 0001 ? 0004 : 0); ($dir_mode & oct('0001') ? oct('0004') : 0);
unless (chmod $fmode, $filename) { unless (chmod $fmode, $filename) {
$self->log(LOGERROR, "chmod: $filename: $!"); $self->log(LOGERROR, "chmod: $filename: $!");

View File

@ -396,7 +396,7 @@ sub cmd_hup {
# clear cache # clear cache
print "Clearing cache\n"; print "Clearing cache\n";
Qpsmtpd::clear_config_cache(); Qpsmtpd::Config::clear_cache();
# should also reload modules... but can't do that yet. # should also reload modules... but can't do that yet.
} }

View File

@ -1,10 +1,13 @@
package Test::Qpsmtpd; package Test::Qpsmtpd;
use strict; use strict;
use Carp qw(croak);
use Test::More;
use lib 't'; use lib 't';
use lib 'lib'; use lib 'lib';
use Carp qw(croak); use parent 'Qpsmtpd::SMTP';
use base qw(Qpsmtpd::SMTP);
use Test::More;
use Qpsmtpd::Constants; use Qpsmtpd::Constants;
use Test::Qpsmtpd::Plugin; use Test::Qpsmtpd::Plugin;
@ -78,7 +81,7 @@ sub input {
sub config_dir { sub config_dir {
return './t/config' if $ENV{QPSMTPD_DEVELOPER}; return './t/config' if $ENV{QPSMTPD_DEVELOPER};
'./config.sample'; return './config.sample';
} }
sub plugin_dirs { sub plugin_dirs {

View File

@ -1,4 +1,5 @@
package Test::Qpsmtpd::Plugin; package Test::Qpsmtpd::Plugin;
use strict;
1; 1;
# Additional plugin methods used during testing # Additional plugin methods used during testing

View File

@ -1,37 +0,0 @@
#!/usr/bin/perl -w
use Test::More qw(no_plan);
use File::Path;
use strict;
use lib 't';
use_ok('Test::Qpsmtpd');
my @mes;
BEGIN { # need this to happen before anything else
my $cwd = `pwd`;
chomp($cwd);
@mes = qw{ ./config.sample/me ./t/config/me };
foreach my $f (@mes) {
open my $me_config, '>', $f;
print $me_config "some.host.example.org";
close $me_config;
}
}
ok(my ($smtpd, $conn) = Test::Qpsmtpd->new_conn(), "get new connection");
is($smtpd->config('me'), 'some.host.example.org', 'config("me")');
# test for ignoring leading/trailing whitespace (relayclients has a
# line with both)
my $relayclients = join ",", sort $smtpd->config('relayclients');
is(
$relayclients,
'127.0.0.1,192.0.,2001:0DB8,2001:0DB8:0000:0000:0000:0000:0000:0001,2001:DB8::1,2001:DB8::1/32',
'config("relayclients") are trimmed'
);
foreach my $f (@mes) {
unlink $f if -f $f;
}

View File

@ -46,10 +46,10 @@ helo
sender_permitted_from sender_permitted_from
greylisting p0f genre,windows greylisting p0f genre,windows
auth/auth_checkpassword checkpw /usr/local/vpopmail/bin/vchkpw true /usr/bin/true #auth/auth_checkpassword checkpw /usr/local/vpopmail/bin/vchkpw true /usr/bin/true
auth/auth_vpopmail #auth/auth_vpopmail
auth/auth_vpopmaild auth/auth_vpopmaild
auth/auth_vpopmail_sql #auth/auth_vpopmail_sql
auth/auth_flat_file auth/auth_flat_file
auth/authdeny auth/authdeny
@ -57,7 +57,7 @@ auth/authdeny
rcpt_ok rcpt_ok
headers days 5 reject_type temp require From,Date headers days 5 reject_type temp require From,Date
domainkeys #domainkeys
dkim dkim
dmarc dmarc

3
t/config/size_threshold Normal file
View File

@ -0,0 +1,3 @@
# Messages below the size below will be stored in memory and not spooled.
# Without this file, the default is 0 bytes, i.e. all messages will be spooled.
10000

View File

@ -8,7 +8,7 @@ use Qpsmtpd::Constants;
sub register_tests { sub register_tests {
my $self = shift; my $self = shift;
eval 'use ClamAV::Client'; eval 'use ClamAV::Client'; ## no critic (Stringy)
if ( ! $@ ) { if ( ! $@ ) {
$self->register_test('test_register', 3); $self->register_test('test_register', 3);
$self->register_test('test_get_clamd', 1); $self->register_test('test_get_clamd', 1);
@ -42,6 +42,9 @@ sub test_err_and_return {
sub test_get_filename { sub test_get_filename {
my $self = shift; my $self = shift;
my $tran = $self->qp->transaction();
$tran->{_body_array} = ['line','two'];
my $filename = $self->get_filename(); my $filename = $self->get_filename();
ok( $filename, "get_filename ($filename)" ); ok( $filename, "get_filename ($filename)" );
} }

View File

@ -6,9 +6,12 @@ use Test::More;
use lib 'lib'; # test lib/Qpsmtpd/Base (vs site_perl) use lib 'lib'; # test lib/Qpsmtpd/Base (vs site_perl)
BEGIN { use_ok('Qpsmtpd::Base'); } BEGIN {
use_ok('Qpsmtpd::Base');
use_ok('Qpsmtpd::Constants');
}
my $base = bless {}, 'Qpsmtpd::Base'; my $base = Qpsmtpd::Base->new();
__tildeexp(); __tildeexp();
__is_localhost(); __is_localhost();

89
t/qpsmtpd-config.t Normal file
View File

@ -0,0 +1,89 @@
use strict;
use warnings;
use Data::Dumper;
use File::Path;
use Test::More;
use lib 'lib'; # test lib/Qpsmtpd (vs site_perl)
use lib 't';
my @mes;
BEGIN {
use_ok('Qpsmtpd::Config'); # call classes directly
use_ok('Qpsmtpd::Constants');
use_ok('Test::Qpsmtpd'); # call via a connection object
@mes = qw{ ./config.sample/me ./t/config/me };
foreach my $f (@mes) {
open my $me_config, '>', $f;
print $me_config "host.example.org";
close $me_config;
}
}
my $config = Qpsmtpd::Config->new();
isa_ok($config, 'Qpsmtpd::Config');
__log();
__config_dir();
__from_file();
__get_qmail();
__config_via_smtpd();
foreach my $f (@mes) { unlink $f; }
done_testing();
sub __log {
my $warned = '';
local $SIG{__WARN__} = sub {
if ($_[0] eq "$$ test log message\n") {
$warned = join ' ', @_;
}
else {
warn @_;
}
};
ok($config->log(LOGWARN, "test log message"), 'log');
is($warned, "$$ test log message\n", 'LOGWARN emitted correct warning');
}
sub __get_qmail {
is($config->get_qmail('me'), 'host.example.org', 'get_qmail("me")');
ok(!$config->get_qmail('not-me'), 'get_qmail("not-me")');
}
sub __from_file {
my $test_file = 't/config/test_config_file';
my @r = $config->from_file($test_file, 'test_config_file');
ok( @r, "from_file, $test_file");
cmp_ok('1st line with content', 'eq', $r[0], "from_file string compare");
ok( !$r[1], "from_file");
};
sub __config_dir {
my $dir = $config->config_dir('logging');
ok($dir, "config_dir, $dir");
#warn Data::Dumper::Dumper($Qpsmtpd::config_dir_memo{logging});
$dir = $Qpsmtpd::Config::dir_memo{logging};
ok($dir, "config_dir, $dir (memo)");
}
sub __config_via_smtpd {
ok(my ($smtpd, $conn) = Test::Qpsmtpd->new_conn(), "get new connection");
is($smtpd->config('me'), 'host.example.org', 'config("me")');
# test for ignoring leading/trailing whitespace (relayclients has a
# line with both)
my $relayclients = join ',', sort $smtpd->config('relayclients');
is($relayclients,
'127.0.0.1,192.0.,2001:0DB8,2001:0DB8:0000:0000:0000:0000:0000:0001,2001:DB8::1,2001:DB8::1/32',
'config("relayclients") are trimmed'
);
};

View File

@ -7,20 +7,23 @@ use File::Path;
use Test::More; use Test::More;
use lib 'lib'; # test lib/Qpsmtpd (vs site_perl) use lib 'lib'; # test lib/Qpsmtpd (vs site_perl)
use lib 't';
BEGIN { BEGIN {
use_ok('Qpsmtpd'); use_ok('Qpsmtpd');
use_ok('Qpsmtpd::Constants'); use_ok('Qpsmtpd::Constants');
} use_ok('Test::Qpsmtpd');
use lib 't'; }
use_ok('Test::Qpsmtpd');
my $qp = bless {}, 'Qpsmtpd'; my $qp = bless {}, 'Qpsmtpd';
ok($qp->version(), "version, " . $qp->version()); ok($qp->version(), "version, " . $qp->version());
is_deeply(Qpsmtpd::hooks(), {}, 'hooks, empty'); is_deeply(Qpsmtpd::hooks(), {}, 'hooks, empty');
ok(my ($smtpd, $conn) = Test::Qpsmtpd->new_conn(), "get new connection");
ok(Qpsmtpd::hooks(), "hooks, populated");
__temp_file(); __temp_file();
__temp_dir(); __temp_dir();
__size_threshold(); __size_threshold();
@ -33,26 +36,10 @@ __log();
__load_logging(); __load_logging();
__config_dir(); __config_dir();
__config_from_file();
__get_qmail_config();
__config(); __config();
done_testing(); done_testing();
sub __get_qmail_config {
ok(!$qp->get_qmail_config('me'), "get_qmail_config, me");
# TODO: add positive tests.
}
sub __config_from_file {
my $test_file = 't/config/test_config_file';
my @r = $qp->_config_from_file($test_file);
ok( @r, "_config_from_file, $test_file");
cmp_ok('1st line with content', 'eq', $r[0], "_config_from_file string compare");
ok( !$r[1], "_config_from_file");
};
sub __log { sub __log {
my $warned = ''; my $warned = '';
local $SIG{__WARN__} = sub { local $SIG{__WARN__} = sub {
@ -67,15 +54,6 @@ sub __log {
is($warned, "$$ test log message\n", 'LOGWARN emitted correct warning'); is($warned, "$$ test log message\n", 'LOGWARN emitted correct warning');
} }
sub __config_dir {
my $dir = $qp->config_dir('logging');
ok($dir, "config_dir, $dir");
#warn Data::Dumper::Dumper($Qpsmtpd::config_dir_memo{logging});
$dir = $Qpsmtpd::config_dir_memo{logging};
ok($dir, "config_dir, $dir (memo)");
}
sub __load_logging { sub __load_logging {
$Qpsmtpd::LOGGING_LOADED = 1; $Qpsmtpd::LOGGING_LOADED = 1;
ok(!$qp->load_logging(), "load_logging, loaded"); ok(!$qp->load_logging(), "load_logging, loaded");
@ -90,6 +68,25 @@ sub __load_logging {
sub __spool_dir { sub __spool_dir {
my $dir = $qp->spool_dir(); my $dir = $qp->spool_dir();
ok( $dir, "spool_dir is at $dir"); ok( $dir, "spool_dir is at $dir");
my $cwd = `pwd`;
chomp($cwd);
open my $spooldir, '>', "./config.sample/spool_dir";
print $spooldir "$cwd/t/tmp";
close $spooldir;
my $spool_dir = $smtpd->spool_dir();
ok($spool_dir =~ m!t/tmp/$!, "Located the spool directory");
my $tempfile = $smtpd->temp_file();
my $tempdir = $smtpd->temp_dir();
ok($tempfile =~ /^$spool_dir/, "Temporary filename");
ok($tempdir =~ /^$spool_dir/, "Temporary directory");
ok(-d $tempdir, "And that directory exists");
unlink "./config.sample/spool_dir";
rmtree($spool_dir);
} }
sub __temp_file { sub __temp_file {
@ -112,7 +109,7 @@ sub __temp_dir {
} }
sub __size_threshold { sub __size_threshold {
ok( ! $qp->size_threshold(), "size_threshold is undefined") is( $qp->size_threshold(), 10000, "size_threshold from t/config is 1000")
or warn "size_threshold: " . $qp->size_threshold; or warn "size_threshold: " . $qp->size_threshold;
$Qpsmtpd::Size_threshold = 5; $Qpsmtpd::Size_threshold = 5;
@ -149,9 +146,19 @@ sub __auth_mechanism {
$qp->{_auth_mechanism} = undef; $qp->{_auth_mechanism} = undef;
} }
sub __config_dir {
my $dir = $qp->config_dir('logging');
ok($dir, "config_dir, $dir");
#warn Data::Dumper::Dumper($Qpsmtpd::config_dir_memo{logging});
$dir = $Qpsmtpd::Config::dir_memo{logging};
ok($dir, "config_dir, $dir (memo)");
}
sub __config { sub __config {
my @r = $qp->config('badhelo'); my @r = $qp->config('badhelo');
ok($r[0], "config, badhelo, @r"); ok($r[0], "config, badhelo, @r");
my $a = FakeAddress->new(test => 'test value'); my $a = FakeAddress->new(test => 'test value');
ok(my ($qp, $cxn) = Test::Qpsmtpd->new_conn(), "get new connection"); ok(my ($qp, $cxn) = Test::Qpsmtpd->new_conn(), "get new connection");
my @test_data = ( my @test_data = (
@ -250,6 +257,8 @@ sub __config {
} }
} }
1;
package FakeAddress; package FakeAddress;
sub new { sub new {
@ -258,3 +267,5 @@ sub new {
} }
sub address { } # pass the can('address') conditional sub address { } # pass the can('address') conditional
1;

View File

@ -1,27 +0,0 @@
#!/usr/bin/perl -w
use Test::More qw(no_plan);
use File::Path;
use strict;
use lib 't';
use_ok('Test::Qpsmtpd');
BEGIN { # need this to happen before anything else
my $cwd = `pwd`;
chomp($cwd);
open my $spooldir, '>', "./config.sample/spool_dir";
print $spooldir "$cwd/t/tmp";
close $spooldir;
}
ok(my ($smtpd, $conn) = Test::Qpsmtpd->new_conn(), "get new connection");
my ($spool_dir, $tempfile, $tempdir) =
($smtpd->spool_dir, $smtpd->temp_file(), $smtpd->temp_dir());
ok($spool_dir =~ m!t/tmp/$!, "Located the spool directory");
ok($tempfile =~ /^$spool_dir/, "Temporary filename");
ok($tempdir =~ /^$spool_dir/, "Temporary directory");
ok(-d $tempdir, "And that directory exists");
unlink "./config.sample/spool_dir";
rmtree($spool_dir);