mirror of
https://git.proxmox.com/git/proxmox-spamassassin
synced 2025-04-28 12:19:37 +00:00
382 lines
12 KiB
Perl
382 lines
12 KiB
Perl
package Mail::SpamAssassin::Spamd::Config;
|
|
use strict;
|
|
|
|
use Mail::SpamAssassin::Util ();
|
|
use File::Spec ();
|
|
use Getopt::Long qw(GetOptions :config bundling); # configuration is global
|
|
|
|
=head1 NAME
|
|
|
|
Mail::SpamAssassin::Spamd::Config -- parse spamd command line options
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
my $conf = Mail::SpamAssassin::Spamd::Config->new(
|
|
{
|
|
argv => \@ARGV,
|
|
defaults => { 'user-config' => 0, },
|
|
moreopts => [ qw( foo-option|f=s@ bar-option|b=i ) ],
|
|
}
|
|
);
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This module uses Getopt::Long to parse and validate spamd command line options.
|
|
|
|
Returns blessed hash reference, containing coherent set of options.
|
|
Dies on error.
|
|
|
|
See source and C<spamd(1)> for list of valid options.
|
|
|
|
=head1 WARNING
|
|
|
|
This interface is considered experimental and likely to change. Notify the dev
|
|
list if you're planning to rely on it.
|
|
|
|
Getopt::Long::Configure is used to modify global C<Getopt::Long(3)>
|
|
configuration. If you don't want C<:bundling> and / or wish to enable /
|
|
disable something else for whatever reason... well, keep that in mind.
|
|
|
|
=head1 BUGS
|
|
|
|
Error messages are not unified.
|
|
|
|
See E<lt>http://bugzilla.spamassassin.org/E<gt>
|
|
|
|
=head1 SEE ALSO
|
|
|
|
C<spamd(1)>,
|
|
C<apache-spamd(1)>
|
|
|
|
=cut
|
|
|
|
my %defaults = (
|
|
'user-config' => 1,
|
|
'ident-timeout' => 5.0,
|
|
|
|
# scaling settings; some of these aren't actually settable via cmdline
|
|
'server-scale-period' => 2, # how often to scale the # of kids, secs
|
|
'min-children' => 1, # min kids to have running
|
|
'min-spare' => 1, # min kids that must be spare
|
|
'max-spare' => 2, # max kids that should be spare
|
|
'max-children' => 5,
|
|
'max-conn-per-child' => 200,
|
|
'timeout-child' => 300,
|
|
'timeout-tcp' => 30,
|
|
|
|
# substituted at 'make' time
|
|
PREFIX => '/usr',
|
|
DEF_RULES_DIR => '/usr/share/spamassassin',
|
|
LOCAL_RULES_DIR => '/etc/mail/spamassassin',
|
|
LOCAL_STATE_DIR => '/var/lib',
|
|
);
|
|
|
|
sub new {
|
|
my ($class, $parms) = @_;
|
|
$parms ||= {};
|
|
die 'usage: ' . __PACKAGE__
|
|
. '->new({ argv=>\@, defaults=>\%, moreopts=>\@ })'
|
|
if ref $parms ne 'HASH'
|
|
or exists $parms->{argv} && ref $parms->{argv} ne 'ARRAY'
|
|
or exists $parms->{defaults} && ref $parms->{defaults} ne 'HASH'
|
|
or exists $parms->{moreopts} && ref $parms->{moreopts} ne 'ARRAY';
|
|
$parms->{argv} ||= \@ARGV;
|
|
|
|
local *ARGV = [@{ $parms->{argv} }];
|
|
|
|
my $self = { exists $parms->{defaults} ? %{ $parms->{defaults} } : () };
|
|
|
|
Getopt::Long::Configure('bundling');
|
|
GetOptions(
|
|
$self,
|
|
|
|
# !xargs -n1|sort|column|expand
|
|
qw(
|
|
allowed-ips|A=s@ round-robin!
|
|
allow-tell|l server-cert=s
|
|
auth-ident server-key=s
|
|
cf=s@ setuid-with-ldap
|
|
configpath|C=s setuid-with-sql|Q
|
|
create-prefs|c! siteconfigpath=s
|
|
daemonize|d! socketgroup=s
|
|
debug|D:s socketmode=s
|
|
groupname|g=s socketowner=s
|
|
help|h socketpath=s
|
|
ident-timeout=f sql-config|q!
|
|
ldap-config! ssl
|
|
listen-ip|ip-address|i:s@ syslog-socket=s
|
|
local|L! syslog|s=s
|
|
max-children|m=i timeout-child|t=i
|
|
max-conn-per-child=i timeout-tcp|T=i
|
|
max-spare=i user-config!
|
|
min-children=i username|u=s
|
|
min-spare=i version|V
|
|
paranoid|P! virtual-config-dir=s
|
|
pidfile|r=s vpopmail|v!
|
|
port|p=s
|
|
|
|
home_dir_for_helpers|helper-home-dir|H:s
|
|
PREFIX=s
|
|
DEF_RULES_DIR=s
|
|
LOCAL_RULES_DIR=s
|
|
LOCAL_STATE_DIR=s
|
|
),
|
|
|
|
x => sub { $self->{'user-config'} = 0 },
|
|
|
|
# NOTE: These are old options. We should ignore (but warn about)
|
|
# the ones that are now defaults. Everything else gets a die (see note2)
|
|
# so the user doesn't get us doing something they didn't expect.
|
|
#
|
|
# NOTE2: 'die' doesn't actually stop the process, GetOptions() catches
|
|
# it, then passes the error on, so we'll end up doing a Usage statement.
|
|
# You can avoid that by doing an explicit exit in the sub.
|
|
|
|
# last in 2.3
|
|
'F:i' => sub {
|
|
die "spamd: the -F option has been removed from spamd,",
|
|
" please remove from your commandline and re-run\n";
|
|
},
|
|
'add-from!' => sub {
|
|
die "spamd: the --add-from option has been removed from spamd,",
|
|
" please remove from your commandline and re-run\n";
|
|
},
|
|
|
|
# last in 2.4
|
|
'stop-at-threshold|S' => sub {
|
|
warn "spamd: the --stop-at-threshold|-S option has been deprecated",
|
|
" and is no longer supported, ignoring\n";
|
|
},
|
|
|
|
(exists $parms->{moreopts} ? @{$parms->{moreopts}} : ()),
|
|
) or die 'GetOptions() failed';
|
|
|
|
# XXX: uncomment this?
|
|
#$self = { map { y/-/_/; $_ => $self->{$_}; } keys %$self };
|
|
bless $self, $class;
|
|
|
|
|
|
$self->_validate_logging;
|
|
$self->_validate;
|
|
|
|
$self;
|
|
}
|
|
|
|
|
|
# check & set some factory settings
|
|
sub _validate {
|
|
my ($self) = @_;
|
|
|
|
# sanity checking on parameters: if --socketpath is used, it means that we're
|
|
# using UNIX domain sockets, none of the IP params are allowed. The code would
|
|
# probably work ok if we didn't check it, but it's better if we detect the
|
|
# error and report it lest the admin find surprises.
|
|
if (exists $self->{'socketpath'}) {
|
|
die "ERROR: --socketpath mutually exclusive with"
|
|
. " --allowed-ips/--ssl/--auth-ident/--port params"
|
|
if exists $self->{'allowed-ips'} && @{ $self->{'allowed-ips'} } > 0
|
|
|| exists $self->{'ssl'}
|
|
|| exists $self->{'auth-ident'}
|
|
|| exists $self->{'port'};
|
|
}
|
|
else {
|
|
die "ERROR: --socketowner/group/mode requires --socketpath param"
|
|
if exists $self->{'socketowner'}
|
|
|| exists $self->{'socketgroup'}
|
|
|| exists $self->{'socketmode'};
|
|
|
|
# These can be changed on command line with -A flag,
|
|
# but only if we're not using UNIX domain sockets
|
|
# warning: no validation here
|
|
$self->{'allowed-ips'} =
|
|
exists $self->{'allowed-ips'} && @{ $self->{'allowed-ips'} }
|
|
? [map { split /,/, $_; } @{ $self->{'allowed-ips'} }]
|
|
: ['127.0.0.1'];
|
|
|
|
$self->{'listen-ip'} =
|
|
!exists $self->{'listen-ip'}
|
|
? ['127.0.0.1']
|
|
: defined $self->{'listen-ip'} && grep(length, @{ $self->{'listen-ip'} })
|
|
? [grep length, map { split /,/, $_; } @{ $self->{'listen-ip'} }]
|
|
: undef; # handle !defined elsewhere
|
|
}
|
|
|
|
# bug 2228: make the values of (almost) all parameters which accept file paths
|
|
# absolute, so they are still valid after daemonize()
|
|
for my $opt (
|
|
grep(exists $self->{$_},
|
|
qw(configpath siteconfigpath socketpath pidfile server-cert server-key
|
|
PREFIX DEF_RULES_DIR LOCAL_RULES_DIR LOCAL_STATE_DIR)),
|
|
grep { exists $self->{$_} && $self->{$_} }
|
|
qw(home_dir_for_helpers) # value is optional
|
|
)
|
|
{
|
|
$self->{$opt} = Mail::SpamAssassin::Util::untaint_file_path(
|
|
File::Spec->rel2abs($self->{$opt}) # rel2abs taints the new value!
|
|
);
|
|
}
|
|
|
|
# -d
|
|
for my $opt (
|
|
grep(exists $self->{$_},
|
|
qw(configpath siteconfigpath
|
|
PREFIX DEF_RULES_DIR LOCAL_RULES_DIR LOCAL_STATE_DIR)),
|
|
grep { exists $self->{$_} && $self->{$_} }
|
|
qw(home_dir_for_helpers) # value is optional
|
|
)
|
|
{
|
|
die "ERROR: --$opt='$self->{$opt}' does not exist or not a directory\n"
|
|
unless -d $self->{$opt};
|
|
}
|
|
|
|
# >= 0
|
|
for my $opt (grep exists $self->{$_}, qw(min-spare max-spare)) {
|
|
die "ERROR: --$opt must be >= 0\n" if $self->{$opt} <= 0;
|
|
}
|
|
|
|
# >= 1
|
|
for my $opt (
|
|
grep exists $self->{$_},
|
|
qw(timeout-tcp timeout-child min-children max-children max-conn-per-child)
|
|
)
|
|
{
|
|
next if $self->{$opt} >= 1;
|
|
warn "ERROR: --$opt must be >= 1, ignoring\n"; # die?
|
|
delete $self->{$opt};
|
|
}
|
|
|
|
# ident-based spamc user authentication
|
|
if ($self->{'auth-ident'}) {
|
|
eval { sub Net::Ident::_export_hooks(); require Net::Ident };
|
|
die "spamd: ident-based authentication requested,",
|
|
" but Net::Ident is unavailable\n"
|
|
if $@;
|
|
|
|
if (exists $self->{'ident-timeout'} && $self->{'ident-timeout'} <= 0) {
|
|
die "ERROR: --ident-timeout must be > 0\n";
|
|
}
|
|
##Net::Ident->import(qw(ident_lookup));
|
|
}
|
|
|
|
# let's not modify %ENV here...
|
|
my $home =
|
|
(exists $ENV{HOME} && defined $ENV{HOME} && -d $ENV{HOME})
|
|
? $ENV{HOME}
|
|
: undef;
|
|
|
|
if (exists $self->{username})
|
|
{ # spamd is going to run as another user, so reset $HOME
|
|
if (my $nh = (getpwnam($self->{username}))[7]) {
|
|
$home = $nh;
|
|
}
|
|
else {
|
|
die "spamd: unable to determine home directory for user"
|
|
. " '$self->{username}'\n";
|
|
}
|
|
}
|
|
|
|
if (!exists $self->{home_dir_for_helpers}) {
|
|
die "ERROR: \$HOME='$home' does not exist or not a directory\n"
|
|
unless defined $home && -d $home;
|
|
$self->{home_dir_for_helpers} = $home;
|
|
}
|
|
|
|
if (exists $self->{'max-spare'}) {
|
|
if (exists $self->{'min-spare'}) {
|
|
## emulate Apache behaviour:
|
|
## http://httpd.apache.org/docs-2.0/mod/prefork.html#maxspareservers
|
|
$self->{'max-spare'} = $self->{'min-spare'} + 1
|
|
if $self->{'max-spare'} < $self->{'min-spare'};
|
|
}
|
|
else {
|
|
$self->{'min-spare'} = $self->{'max-spare'};
|
|
}
|
|
}
|
|
elsif (exists $self->{'min-spare'}) {
|
|
$self->{'max-spare'} = $self->{'min-spare'};
|
|
}
|
|
|
|
# set other defaults
|
|
for my $opt (keys %defaults) {
|
|
$self->{$opt} = $defaults{$opt} if !exists $self->{$opt};
|
|
}
|
|
|
|
# check for server certs, must be done after setting other defaults
|
|
if ($self->{'ssl'}) {
|
|
$self->{'server-key'} ||= "$self->{LOCAL_RULES_DIR}/certs/server-key.pem";
|
|
$self->{'server-cert'} ||= "$self->{LOCAL_RULES_DIR}/certs/server-cert.pem";
|
|
eval { require IO::Socket::SSL };
|
|
die "spamd: SSL encryption requested, but IO::Socket::SSL is unavailable\n"
|
|
if $@;
|
|
die "spamd: server key file '$self->{'server-key'}' does not exist\n"
|
|
unless -f $self->{'server-key'};
|
|
die "spamd: server certificate file '$self->{'server-cert'}' does not exist\n"
|
|
unless -f $self->{'server-cert'};
|
|
}
|
|
|
|
# XXX: delete LOCAL_{RULES,STATE}_DIR and PREFIX if eq $defaults{$_}?
|
|
|
|
1;
|
|
}
|
|
|
|
sub _validate_logging {
|
|
my $self = shift;
|
|
|
|
# Enable debugging, if any areas were specified. We do this already here,
|
|
# accessing some non-public API so we can use the convenient dbg() routine.
|
|
# Don't do this at home (aka any 3rd party tools), kids!
|
|
$self->{debug} ||= 'all' if exists $self->{debug};
|
|
|
|
# always turn on at least info-level debugging for spamd
|
|
$self->{debug} ||= 'info';
|
|
|
|
$self->{'syslog-socket'} = lc $self->{'syslog-socket'} || 'unix';
|
|
$self->{'log-facility'} = $self->{syslog} || 'mail';
|
|
|
|
# This is the default log file; it can be changed on the command line
|
|
# via a --syslog flag containing non-word characters.
|
|
$self->{'log-file'} = 'spamd.log';
|
|
|
|
if ($self->{'log-facility'} =~ /[^a-z0-9]/) {
|
|
# A specific log file was given (--syslog=/path/to/file).
|
|
$self->{'log-file'} = $self->{'log-facility'};
|
|
$self->{'syslog-socket'} = 'file';
|
|
}
|
|
elsif ($self->{'log-facility'} eq 'file') {
|
|
# The generic log file was requested (--syslog=file).
|
|
$self->{'syslog-socket'} = 'file';
|
|
}
|
|
else {
|
|
# The casing is kept only if the facility specified a file.
|
|
$self->{'log-facility'} = lc $self->{'log-facility'};
|
|
}
|
|
|
|
if ($self->{'syslog-socket'} eq 'file') {
|
|
# Either above or at the command line the socket was set
|
|
# to 'file' (--syslog-socket=file).
|
|
$self->{'log-facility'} = 'file';
|
|
}
|
|
elsif ($self->{'syslog-socket'} eq 'none') {
|
|
# The socket 'none' (--syslog-socket=none) historically
|
|
# represents logging to STDERR.
|
|
$self->{'log-facility'} = 'stderr';
|
|
}
|
|
|
|
# Either above or at the command line the facility was set
|
|
# to 'stderr' (--syslog=stderr).
|
|
$self->{'syslog-socket'} = 'file' if $self->{'log-facility'} eq 'stderr';
|
|
|
|
1;
|
|
}
|
|
|
|
sub option {
|
|
my ($self, $opt) = @_;
|
|
return exists $self->{$opt}
|
|
? $self->{$opt}
|
|
: undef;
|
|
}
|
|
|
|
1;
|
|
|
|
# vim: ts=8 sw=2 et
|