mirror of
https://git.proxmox.com/git/proxmox-spamassassin
synced 2025-04-28 12:19:37 +00:00
3958 lines
132 KiB
Plaintext
Executable File
3958 lines
132 KiB
Plaintext
Executable File
#!/usr/bin/perl -T -w
|
|
# <@LICENSE>
|
|
# Licensed to the Apache Software Foundation (ASF) under one or more
|
|
# contributor license agreements. See the NOTICE file distributed with
|
|
# this work for additional information regarding copyright ownership.
|
|
# The ASF licenses this file to you under the Apache License, Version 2.0
|
|
# (the "License"); you may not use this file except in compliance with
|
|
# the License. You may obtain a copy of the License at:
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# </@LICENSE>
|
|
|
|
#IMPORTANT: The order of -T -w above is important for spamd_hup.t on Solaris 10 - changed per bug 6883
|
|
|
|
use strict;
|
|
use warnings;
|
|
use re 'taint';
|
|
|
|
my @ORIG_INC_OPTS;
|
|
BEGIN {
|
|
# bug 8030 - Save what is in @INC to capture any -I arguments passed in to use at SIGHUP restart
|
|
# This is done before any use lib statements add anything else to @INC
|
|
my %orig_inc;
|
|
for (my $i = $#INC; $i >=0; $i--) {
|
|
my $path = $INC[$i];
|
|
if (!$orig_inc{$path}) { # more stringent checking will done later after more modules are loaded
|
|
$orig_inc{$path} = 1;
|
|
unshift(@ORIG_INC_OPTS, $path);
|
|
}
|
|
}
|
|
}
|
|
|
|
my $PREFIX = '@@PREFIX@@'; # substituted at 'make' time
|
|
my $DEF_RULES_DIR = '@@DEF_RULES_DIR@@'; # substituted at 'make' time
|
|
my $LOCAL_RULES_DIR = '@@LOCAL_RULES_DIR@@'; # substituted at 'make' time
|
|
my $LOCAL_STATE_DIR = '@@LOCAL_STATE_DIR@@'; # substituted at 'make' time
|
|
use lib '@@INSTALLSITELIB@@'; # substituted at 'make' time
|
|
|
|
# added by jm for use inside the distro
|
|
# This is disabled during the "make install" process.
|
|
BEGIN {
|
|
if ( -e '../blib/lib/Mail/SpamAssassin.pm' ) { # REMOVEFORINST
|
|
unshift ( @INC, '../blib/lib' ); # REMOVEFORINST
|
|
} else { # REMOVEFORINST
|
|
unshift ( @INC, '../lib' ); # REMOVEFORINST
|
|
} # REMOVEFORINST
|
|
}
|
|
|
|
our ($have_getaddrinfo_in_core, $have_getaddrinfo_legacy, $io_socket_module_name,
|
|
$have_inet4, $have_inet6, $ai_addrconfig_flag);
|
|
|
|
# don't force requirement on IO::Socket::IP or IO::Socket::INET6
|
|
BEGIN {
|
|
require Socket;
|
|
$have_getaddrinfo_in_core = eval {
|
|
# The Socket module (1.94) bundled with Perl 5.14.* provides
|
|
# new affordances for IPv6, including implementations of the
|
|
# Socket::getaddrinfo() and Socket::getnameinfo() functions,
|
|
# along with related constants and a handful of new functions.
|
|
# Perl 5.16.0 upgrades the core Socket module to version 2.001.
|
|
# Socket->VERSION(1.94); # provides getaddrinfo() and getnameinfo()
|
|
# Socket->VERSION(1.95); # provides AI_ADDRCONFIG
|
|
Socket->VERSION(1.96); # provides NIx_NOSERV, and Exporter tag :addrinfo
|
|
# Socket->VERSION(1.97); # IO::Socket::IP depends on Socket 1.97
|
|
Socket->import(qw(/^(?:AI|NI|NIx|EAI)_/));
|
|
|
|
# AUTOLOADing 'constants' here enables inlining - see Exporter man page
|
|
&AI_ADDRCONFIG; &AI_PASSIVE;
|
|
&NI_NUMERICHOST, &NI_NUMERICSERV; &NIx_NOSERV; 1;
|
|
};
|
|
|
|
$have_getaddrinfo_legacy = !$have_getaddrinfo_in_core && eval {
|
|
require Socket6;
|
|
# Socket6->VERSION(0.13); # provides NI_NAMEREQD
|
|
Socket6->VERSION(0.18); # provides AI_NUMERICSERV
|
|
Socket6->import(qw(/^(?:AI|NI|NIx|EAI)_/));
|
|
&AI_ADDRCONFIG; &AI_PASSIVE; # enable inlining
|
|
&NI_NUMERICHOST; &NI_NUMERICSERV; &NI_NAMEREQD; 1;
|
|
};
|
|
|
|
Socket->import(qw(:DEFAULT IPPROTO_TCP));
|
|
|
|
&SOCK_STREAM; &IPPROTO_TCP; &SOMAXCONN; # enable inlining
|
|
|
|
&AF_UNSPEC; &AF_INET; &AF_INET6; # enable inlining
|
|
|
|
$ai_addrconfig_flag = 0;
|
|
|
|
if ($have_getaddrinfo_in_core) {
|
|
# using a modern Socket module
|
|
|
|
eval { # does the operating system recognize an AI_ADDRCONFIG flag?
|
|
if (&AI_ADDRCONFIG && &EAI_BADFLAGS) {
|
|
my($err, @res) = Socket::getaddrinfo("localhost", 0,
|
|
{ family => &AF_UNSPEC, flags => &AI_ADDRCONFIG });
|
|
$ai_addrconfig_flag = &AI_ADDRCONFIG if !$err || $err != &EAI_BADFLAGS;
|
|
}
|
|
};
|
|
|
|
*ip_or_name_to_ip_addresses = sub {
|
|
my($addr, $ai_family) = @_;
|
|
# Socket::getaddrinfo returns a list of hashrefs
|
|
my($error, @res) =
|
|
Socket::getaddrinfo($addr, 0,
|
|
{ family => $ai_family, flags => $ai_addrconfig_flag | &AI_PASSIVE,
|
|
socktype => &SOCK_STREAM, protocol => &IPPROTO_TCP });
|
|
my(@ip_addrs);
|
|
if (!$error) {
|
|
for my $a (@res) {
|
|
my($err, $ip_addr) =
|
|
Socket::getnameinfo($a->{addr},
|
|
&NI_NUMERICHOST | &NI_NUMERICSERV, &NIx_NOSERV);
|
|
if (!$err) { push(@ip_addrs, $ip_addr) }
|
|
elsif (!$error) { $error = $err }
|
|
}
|
|
}
|
|
return ($error, @ip_addrs);
|
|
};
|
|
|
|
*peer_info_from_socket = sub {
|
|
my $sock = shift;
|
|
my $peer_addr = $sock->peerhost; # textual representation of an IP addr
|
|
$peer_addr or return;
|
|
my $peer_hostname;
|
|
if ($sock->UNIVERSAL::can('peerhostname')) {
|
|
$peer_hostname = $sock->peerhostname; # provided by IO::Socket::IP
|
|
} else {
|
|
my($err, $host) = Socket::getnameinfo($sock->peername,
|
|
&NI_NAMEREQD, &NIx_NOSERV);
|
|
$peer_hostname = $host if !$err;
|
|
}
|
|
return ($sock->peerport, $peer_addr, $peer_hostname||$peer_addr,
|
|
$sock->sockport);
|
|
};
|
|
|
|
} elsif ($have_getaddrinfo_legacy) {
|
|
# using a legacy Socket6 module; somewhat different API on getaddrinfo()
|
|
# and getnameinfo() compared to these functions in a module Socket
|
|
|
|
eval { # does the operating system recognize an AI_ADDRCONFIG flag?
|
|
if (&AI_ADDRCONFIG && &EAI_BADFLAGS) {
|
|
my @res = Socket6::getaddrinfo("localhost", "", 0, &SOCK_STREAM,
|
|
&IPPROTO_TCP, &AI_ADDRCONFIG);
|
|
my $err = @res >= 5 ? 0 : $res[0];
|
|
$ai_addrconfig_flag = &AI_ADDRCONFIG if !$err || $err != &EAI_BADFLAGS;
|
|
}
|
|
};
|
|
|
|
*ip_or_name_to_ip_addresses = sub {
|
|
my($addr, $ai_family) = @_;
|
|
# Socket6::getaddrinfo returns a list of quintuples
|
|
my @res = Socket6::getaddrinfo($addr, '',
|
|
$ai_family, &SOCK_STREAM, &IPPROTO_TCP,
|
|
$ai_addrconfig_flag | &AI_PASSIVE);
|
|
my($error, @ip_addrs);
|
|
if (@res < 5) {
|
|
$error = $res[0];
|
|
} else {
|
|
my($family, $socktype, $proto, $saddr, $canonname);
|
|
while (@res >= 5) {
|
|
($family, $socktype, $proto, $saddr, $canonname, @res) = @res;
|
|
my(@resinfo) =
|
|
Socket6::getnameinfo($saddr, &NI_NUMERICHOST | &NI_NUMERICSERV);
|
|
if (@resinfo >= 2) { push(@ip_addrs, $resinfo[0]) }
|
|
elsif (!$error) { $error = $resinfo[0] }
|
|
}
|
|
}
|
|
return ($error, @ip_addrs);
|
|
};
|
|
|
|
*peer_info_from_socket = sub {
|
|
my $sock = shift;
|
|
my $peer_addr = $sock->peerhost;
|
|
$peer_addr or return;
|
|
my @resinfo = (Socket6::getnameinfo($sock->peername, &NI_NAMEREQD))[0];
|
|
my $peer_hostname = @resinfo > 1 ? $resinfo[0] : undef;
|
|
return ($sock->peerport, $peer_addr, $peer_hostname||$peer_addr,
|
|
$sock->sockport);
|
|
};
|
|
|
|
} else { # IPv4 only, no getaddrinfo() available
|
|
|
|
*ip_or_name_to_ip_addresses = sub {
|
|
my($addr, $ai_family) = @_;
|
|
$ai_family == &AF_UNSPEC || $ai_family == &AF_INET
|
|
or die "Protocol family $ai_family not supported on this platform";
|
|
my($error, @ip_addrs, @binaddr);
|
|
$! = 0; my @res = gethostbyname($addr);
|
|
if (!@res) {
|
|
$error = "no results from gethostbyname $!";
|
|
} else {
|
|
my($name,$aliases,$addrtype,$length);
|
|
($name,$aliases,$addrtype,$length,@binaddr) = @res;
|
|
}
|
|
if (!@binaddr) {
|
|
$error = "no such host";
|
|
} else {
|
|
for (@binaddr) {
|
|
my $ip_addr = Socket::inet_ntoa($_);
|
|
push(@ip_addrs, $ip_addr) if $ip_addr;
|
|
}
|
|
}
|
|
return ($error, @ip_addrs);
|
|
};
|
|
|
|
*peer_info_from_socket = sub {
|
|
my $sock = shift;
|
|
my ($peer_port, $in_addr) = Socket::sockaddr_in($sock->peername)
|
|
or return;
|
|
my $peer_addr = Socket::inet_ntoa($in_addr) or return;
|
|
my $peer_hostname = gethostbyaddr($in_addr, &AF_INET);
|
|
return ($peer_port, $peer_addr, $peer_hostname||$peer_addr,
|
|
$sock->sockport);
|
|
};
|
|
|
|
}
|
|
|
|
if (eval { require IO::Socket::IP }) { # handles IPv6 and IPv4
|
|
IO::Socket::IP->VERSION(0.09); # implements IPV6_V6ONLY
|
|
$io_socket_module_name = 'IO::Socket::IP';
|
|
|
|
} elsif (eval { require IO::Socket::INET6 }) { # handles IPv6 and IPv4
|
|
$io_socket_module_name = 'IO::Socket::INET6';
|
|
|
|
} elsif (eval { require IO::Socket::INET }) { # IPv4 only
|
|
$io_socket_module_name = 'IO::Socket::INET';
|
|
}
|
|
|
|
$have_inet4 = # can we create a PF_INET socket?
|
|
defined $io_socket_module_name && eval {
|
|
my $sock =
|
|
$io_socket_module_name->new(LocalAddr => '0.0.0.0', Proto => 'tcp');
|
|
$sock->close or die "error closing socket: $!" if $sock;
|
|
$sock ? 1 : undef;
|
|
};
|
|
|
|
$have_inet6 = # can we create a PF_INET6 socket?
|
|
defined $io_socket_module_name &&
|
|
$io_socket_module_name ne 'IO::Socket::INET' &&
|
|
eval {
|
|
my $sock =
|
|
$io_socket_module_name->new(LocalAddr => '::', Proto => 'tcp');
|
|
$sock->close or die "error closing socket: $!" if $sock;
|
|
$sock ? 1 : undef;
|
|
};
|
|
|
|
}
|
|
|
|
use IO::Handle;
|
|
use IO::Pipe;
|
|
use IO::File ();
|
|
|
|
use Mail::SpamAssassin;
|
|
use Mail::SpamAssassin::NetSet;
|
|
use Mail::SpamAssassin::SubProcBackChannel;
|
|
use Mail::SpamAssassin::SpamdForkScaling qw(:pfstates);
|
|
use Mail::SpamAssassin::Logger qw(:DEFAULT log_message);
|
|
use Mail::SpamAssassin::Util qw(untaint_var untaint_file_path secure_tmpdir
|
|
exit_status_str am_running_on_windows
|
|
get_user_groups force_die);
|
|
use Mail::SpamAssassin::Timeout;
|
|
|
|
use Getopt::Long;
|
|
use POSIX qw(:sys_wait_h);
|
|
use POSIX qw(locale_h setsid sigprocmask);
|
|
use Errno;
|
|
use Fcntl qw(:flock);
|
|
|
|
use Cwd ();
|
|
use File::Spec 0.8;
|
|
use File::Path;
|
|
use Carp ();
|
|
use Time::HiRes qw(time);
|
|
|
|
use constant RUNNING_ON_MACOS => ($^O =~ /^darwin/oi);
|
|
|
|
# Check to make sure the script version and the module version matches.
|
|
# If not, die here! Also, deal with unchanged VERSION macro.
|
|
if ($Mail::SpamAssassin::VERSION ne '@@VERSION@@' && '@@VERSION@@' ne "\@\@VERSION\@\@") {
|
|
die 'spamd: spamd script is v@@VERSION@@, but using modules v'.$Mail::SpamAssassin::VERSION."\n";
|
|
}
|
|
|
|
# Bug 3062: SpamAssassin should be "locale safe"
|
|
POSIX::setlocale(LC_TIME,'C');
|
|
|
|
my %resphash = (
|
|
EX_OK => 0, # no problems
|
|
EX_USAGE => 64, # command line usage error
|
|
EX_DATAERR => 65, # data format error
|
|
EX_NOINPUT => 66, # cannot open input
|
|
EX_NOUSER => 67, # addressee unknown
|
|
EX_NOHOST => 68, # host name unknown
|
|
EX_UNAVAILABLE => 69, # service unavailable
|
|
EX_SOFTWARE => 70, # internal software error
|
|
EX_OSERR => 71, # system error (e.g., can't fork)
|
|
EX_OSFILE => 72, # critical OS file missing
|
|
EX_CANTCREAT => 73, # can't create (user) output file
|
|
EX_IOERR => 74, # input/output error
|
|
EX_TEMPFAIL => 75, # temp failure; user is invited to retry
|
|
EX_PROTOCOL => 76, # remote error in protocol
|
|
EX_NOPERM => 77, # permission denied
|
|
EX_CONFIG => 78, # configuration error
|
|
EX_TIMEOUT => 79, # read timeout
|
|
);
|
|
|
|
sub print_version {
|
|
printf("SpamAssassin Server version %s\n", Mail::SpamAssassin::Version());
|
|
printf(" running on Perl %s\n",
|
|
join(".", map( 0+($_||0), ($] =~ /(\d)\.(\d{3})(\d{3})?/) )));
|
|
eval { require IO::Socket::SSL; };
|
|
printf(" with SSL support (%s %s)\n", "IO::Socket::SSL", $IO::Socket::SSL::VERSION) unless ($@);
|
|
eval { require Compress::Zlib; };
|
|
printf(" with zlib support (%s %s)\n", "Compress::Zlib", $Compress::Zlib::VERSION) unless ($@);
|
|
}
|
|
|
|
sub print_usage_and_exit {
|
|
my ( $message, $respnam ) = (@_);
|
|
$respnam ||= 'EX_USAGE';
|
|
|
|
if ($respnam eq 'EX_OK' ) {
|
|
print_version();
|
|
print("\n");
|
|
}
|
|
|
|
require Pod::Usage;
|
|
Pod::Usage->import;
|
|
pod2usage(
|
|
-verbose => 0,
|
|
-message => $message,
|
|
-exitval => $resphash{$respnam},
|
|
);
|
|
}
|
|
|
|
# defaults
|
|
my %opt = (
|
|
'user-config' => 1,
|
|
# 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
|
|
'pre' => [], # extra .pre lines
|
|
'cf' => [], # extra config lines
|
|
);
|
|
|
|
|
|
# bug 1725, 2192:
|
|
# Untaint all command-line options and ENV vars, since spamd is launched
|
|
# as a daemon from a known-safe environment. Also store away some of the
|
|
# vars we need for a SIGHUP later on.
|
|
|
|
# Testing for taintedness only works before detainting %ENV
|
|
Mail::SpamAssassin::Util::am_running_in_taint_mode();
|
|
|
|
# First clean PATH and untaint the environment -- need to do this before
|
|
# Cwd::cwd(), else it will croak.
|
|
Mail::SpamAssassin::Util::clean_path_in_taint_mode();
|
|
untaint_var( \%ENV );
|
|
|
|
# The zeroth argument will be replaced in daemonize().
|
|
my $ORIG_ARG0 = untaint_var($0);
|
|
|
|
# Getopt::Long clears all arguments it processed (untaint both @ARGVs here!)
|
|
my @ORIG_ARGV = untaint_var( \@ARGV );
|
|
|
|
# daemonize() switches to the root later on and we need to come back here
|
|
# somehow -- untaint the dir to be on the safe side.
|
|
my $ORIG_CWD = untaint_var( Cwd::cwd() );
|
|
|
|
prepare_for_sighup_restart();
|
|
|
|
# Parse the command line
|
|
Getopt::Long::Configure("bundling");
|
|
GetOptions(
|
|
'allow-tell' => \$opt{'tell'},
|
|
'allowed-ips|A=s' => \@{ $opt{'allowed-ip'} },
|
|
'configpath|C=s' => \$opt{'configpath'},
|
|
'c' => \$opt{'create-prefs'},
|
|
'create-prefs!' => \$opt{'create-prefs'},
|
|
'daemonize!' => \$opt{'daemonize'},
|
|
'debug|D:s' => \$opt{'debug'},
|
|
'default-user|U=s' => \$opt{'default-user'},
|
|
'd' => \$opt{'daemonize'},
|
|
'groupname|g=s' => \$opt{'groupname'},
|
|
'helper-home-dir|H:s' => \$opt{'home_dir_for_helpers'},
|
|
'help|h' => \$opt{'help'},
|
|
'4|ipv4only|ipv4-only|ipv4'=> sub { $opt{'force_ipv4'} = 1;
|
|
$opt{'force_ipv6'} = 0; },
|
|
'6' => sub { $opt{'force_ipv6'} = 1;
|
|
$opt{'force_ipv4'} = 0; },
|
|
'ldap-config!' => \$opt{'ldap-config'},
|
|
'listen|listen-ip|ip-address|i:s' => \@{ $opt{'listen-sockets'} },
|
|
'local!' => \$opt{'local'},
|
|
'L' => \$opt{'local'},
|
|
'l' => \$opt{'tell'},
|
|
'round-robin!' => \$opt{'round-robin'},
|
|
'min-children=i' => \$opt{'min-children'},
|
|
'max-children|m=i' => \$opt{'max-children'},
|
|
'min-spare=i' => \$opt{'min-spare'},
|
|
'max-spare=i' => \$opt{'max-spare'},
|
|
'max-conn-per-child=i' => \$opt{'max-conn-per-child'},
|
|
'nouser-config|x' => sub { $opt{'user-config'} = 0 },
|
|
'paranoid!' => \$opt{'paranoid'},
|
|
'P' => \$opt{'paranoid'},
|
|
'pidfile|r=s' => \$opt{'pidfile'},
|
|
'port|p=s' => \$opt{'port'},
|
|
'Q' => \$opt{'setuid-with-sql'},
|
|
'q' => \$opt{'sql-config'},
|
|
'server-cert=s' => \$opt{'server-cert'},
|
|
'server-key=s' => \$opt{'server-key'},
|
|
'setuid-with-ldap' => \$opt{'setuid-with-ldap'},
|
|
'setuid-with-sql' => \$opt{'setuid-with-sql'},
|
|
'siteconfigpath=s' => \$opt{'siteconfigpath'},
|
|
'pre=s' => \@{$opt{'pre'}},
|
|
'cf=s' => \@{$opt{'cf'}},
|
|
'socketgroup=s' => \$opt{'socketgroup'},
|
|
'socketmode=s' => \$opt{'socketmode'},
|
|
'socketowner=s' => \$opt{'socketowner'},
|
|
'socketpath=s' => \$opt{'socketpath'},
|
|
'sql-config!' => \$opt{'sql-config'},
|
|
'ssl' => \$opt{'ssl'},
|
|
'ssl-verify' => \$opt{'ssl-verify'},
|
|
'ssl-ca-file=s' => \$opt{'ssl-ca-file'},
|
|
'ssl-ca-path=s' => \$opt{'ssl-ca-path'},
|
|
'ssl-port=s' => \$opt{'ssl-port'},
|
|
'syslog-socket=s' => \$opt{'syslog-socket'},
|
|
'syslog|s=s' => \$opt{'syslog'},
|
|
'log-timestamp-fmt:s' => \$opt{'log-timestamp-fmt'},
|
|
'timeout-tcp|T=i' => \$opt{'timeout-tcp'},
|
|
'timeout-child|t=i' => \$opt{'timeout-child'},
|
|
'timing' => \$opt{'timing'},
|
|
'user-config' => \$opt{'user-config'},
|
|
'username|u=s' => \$opt{'username'},
|
|
'version|V' => \$opt{'version'},
|
|
'virtual-config-dir=s' => \$opt{'virtual-config-dir'},
|
|
'v' => \$opt{'vpopmail'},
|
|
'vpopmail!' => \$opt{'vpopmail'},
|
|
|
|
#
|
|
# 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 { warn "spamd: the -F option has been removed from spamd, please remove from your commandline and re-run\n"; exit 2; },
|
|
'add-from!' => sub { warn "spamd: the --add-from option has been removed from spamd, please remove from your commandline and re-run\n"; exit 2; },
|
|
|
|
# last in 2.4
|
|
'stop-at-threshold|S' => sub { warn "spamd: the -S option has been deprecated and is no longer supported, ignoring\n" },
|
|
|
|
) or print_usage_and_exit();
|
|
|
|
if ($opt{'help'}) {
|
|
print_usage_and_exit(qq{For more details, use "man spamd".\n}, 'EX_OK');
|
|
}
|
|
if ($opt{'version'}) {
|
|
print_version();
|
|
exit($resphash{'EX_OK'});
|
|
}
|
|
|
|
if (!defined $opt{'default-user'}) {
|
|
$opt{'default-user'} = 'nobody';
|
|
}
|
|
|
|
my $log_timestamp_fmt = $opt{'log-timestamp-fmt'};
|
|
if (defined $log_timestamp_fmt && lc($log_timestamp_fmt) eq 'default') {
|
|
undef $log_timestamp_fmt; # undefined implies per-logger's default
|
|
}
|
|
if (defined $log_timestamp_fmt) {
|
|
# a nondefault timestamp format was specified, need to reopen stderr logger
|
|
Mail::SpamAssassin::Logger::remove('stderr');
|
|
Mail::SpamAssassin::Logger::add(method => 'stderr',
|
|
timestamp_fmt => $log_timestamp_fmt,
|
|
escape => 1);
|
|
}
|
|
|
|
# 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!
|
|
if (defined $opt{'debug'}) {
|
|
$opt{'debug'} ||= 'all';
|
|
}
|
|
# always turn on at least info-level debugging for spamd
|
|
$opt{'debug'} ||= 'info';
|
|
# turn on debugging facilities as soon as possible
|
|
Mail::SpamAssassin::Logger::add_facilities($opt{'debug'});
|
|
|
|
# bug 2228: make the values of (almost) all parameters which accept file paths
|
|
# absolute, so they are still valid after daemonize()
|
|
foreach my $opt (
|
|
qw(
|
|
configpath
|
|
siteconfigpath
|
|
socketpath
|
|
pidfile
|
|
home_dir_for_helpers
|
|
)
|
|
)
|
|
{
|
|
# rel2abs taints the new value!
|
|
$opt{$opt} =
|
|
untaint_file_path(File::Spec->rel2abs( $opt{$opt} )) if $opt{$opt};
|
|
}
|
|
|
|
# These can be changed on command line with -A flag
|
|
my $allowed_nets = Mail::SpamAssassin::NetSet->new();
|
|
if ( @{ $opt{'allowed-ip'} } ) {
|
|
set_allowed_ip( grep length, map { split /,/ } @{ $opt{'allowed-ip'} } );
|
|
} else {
|
|
set_allowed_ip('127.0.0.1', '::1');
|
|
}
|
|
|
|
### Begin initialization of logging ########################
|
|
|
|
# The syslog facility can be changed on the command line with the
|
|
# --syslog flag. Special cases are:
|
|
# * A log facility of 'stderr' will log to STDERR
|
|
# * " " " " 'null' disables all logging
|
|
# * " " " " 'file' logs to the file "spamd.log"
|
|
# * Any facility containing non-word characters is interpreted as the name
|
|
# of a specific logfile
|
|
my $log_facility = $opt{'syslog'} || 'mail';
|
|
|
|
# The --syslog-socket option specifies one of the possible socket types or
|
|
# logging mechanisms as accepted by the Sys::Syslog::setlogsock() subroutine.
|
|
# Depending on a version of Sys::Syslog and on the underlying operating system,
|
|
# one of the following values (or their subset) can be used: native, eventlog,
|
|
# tcp, udp, inet, unix, stream, pipe, console. The value 'eventlog' is
|
|
# specific to Win32 events logger and requires a perl module Win32::EventLog.
|
|
#
|
|
# In addition to values acceptable by Sys::Syslog::setlogsock(),
|
|
# a --syslog-socket=none is mapped to --syslog=stderr and $log_socket='file'.
|
|
#
|
|
# A value 'file' in variable $log_socket implies logging to any file handler
|
|
# (either a specific log file or STDERR), A value 'none' in $log_socket
|
|
# represents no logging, equivalent to --syslog=null.
|
|
#
|
|
# (old text: The socket to log over can be changed on the command line with
|
|
# the --syslog-socket flag. Logging to any file handler (either a specific log
|
|
# file or STDERR) is internally represented by a socket 'file', no logging
|
|
# at all is 'none'. The latter is different from --syslog-socket=none which
|
|
# gets mapped to --syslog=stderr and such --syslog-socket=file. An internal
|
|
# socket of 'none' means as much as --syslog=null. Sounds complicated? It is.
|
|
# But it works.
|
|
# )
|
|
|
|
my $log_socket = $opt{'syslog-socket'};
|
|
|
|
if (!defined $log_socket || $log_socket eq '') {
|
|
$log_socket = am_running_on_windows() ? 'none' : 'unix';
|
|
} else {
|
|
$log_socket = lc $log_socket;
|
|
}
|
|
|
|
# This is the default log file; it can be changed on the command line
|
|
# via a --syslog flag containing non-word characters.
|
|
my $log_file = "spamd.log";
|
|
|
|
# A specific log file was given (--syslog=/path/to/file).
|
|
if ($log_facility =~ /[^a-z0-9]/) {
|
|
$log_file = $log_facility;
|
|
$log_socket = 'file';
|
|
}
|
|
# The generic log file was requested (--syslog=file).
|
|
elsif (lc($log_facility) eq 'file') {
|
|
$log_socket = 'file';
|
|
}
|
|
# The casing is kept only if the facility specified a file.
|
|
else {
|
|
$log_facility = lc($log_facility);
|
|
}
|
|
|
|
# Either above or at the command line the socket was set
|
|
# to 'file' (--syslog-socket=file).
|
|
if ($log_socket eq 'file') {
|
|
$log_facility = 'file';
|
|
}
|
|
# The socket 'none' (--syslog-socket=none) historically
|
|
# represents logging to STDERR.
|
|
elsif ($log_socket eq 'none') {
|
|
$log_facility = 'stderr';
|
|
}
|
|
|
|
# Either above or at the command line the facility was set
|
|
# to 'stderr' (--syslog=stderr).
|
|
if ($log_facility eq 'stderr') {
|
|
$log_socket = 'file';
|
|
}
|
|
|
|
# The --log-timestamp-fmt option can provide a POSIX strftime(3) format for
|
|
# timestamps included in each logged message. Each logger (stderr, file,
|
|
# syslog) has its own default value for a timestamp format, which applies when
|
|
# --log-timestamp-fmt option is not given, or with --log-timestamp-fmt=default
|
|
# Timestamps can be turned off by specifying an empty string with this
|
|
# option, e.g. --log-timestamp-fmt='' or just --log-timestamp-fmt=
|
|
# Typical use: --log-timestamp-fmt='%a %b %e %H:%M:%S %Y' .
|
|
|
|
# Logging via syslog is requested.
|
|
if ($log_socket ne 'file' && $log_facility ne 'null') {
|
|
if (!Mail::SpamAssassin::Logger::add(method => 'syslog',
|
|
socket => $log_socket,
|
|
facility => $log_facility,
|
|
ident => 'spamd',
|
|
timestamp_fmt => $log_timestamp_fmt,
|
|
escape => 1))
|
|
{
|
|
# syslog method failed
|
|
$log_facility = 'stderr';
|
|
}
|
|
}
|
|
# Otherwise, the user wants to log to some file.
|
|
elsif ($log_facility eq 'file') {
|
|
if (!Mail::SpamAssassin::Logger::add(method => 'file',
|
|
filename => $log_file,
|
|
timestamp_fmt => $log_timestamp_fmt,
|
|
escape => 1))
|
|
{
|
|
# file method failed
|
|
$log_facility = 'stderr';
|
|
}
|
|
}
|
|
|
|
### End initialization of logging ##########################
|
|
|
|
# REIMPLEMENT: if $log_socket is none, fall back to log_facility 'stderr'.
|
|
# If log_fac is stderr and defined $opt{'debug'}, set log_fac to 'null' to
|
|
# avoid duplicating log messages.
|
|
# TVD: isn't this already done up above?
|
|
|
|
# support setuid() to user unless:
|
|
# run with -u
|
|
# we're not root
|
|
# doing --vpopmail or --virtual-config-dir
|
|
# using --sql-config or --ldap-config
|
|
# (unless we're also using --setuid-with-sql or --setuid-with-ldap)
|
|
my $setuid_to_user = (
|
|
$opt{'username'} ||
|
|
$> != 0 ||
|
|
$opt{'vpopmail'} ||
|
|
$opt{'virtual-config-dir'} ||
|
|
($opt{'sql-config'} && !$opt{'setuid-with-sql'}) ||
|
|
($opt{'ldap-config'} && !$opt{'setuid-with-ldap'})
|
|
) ? 0 : 1;
|
|
|
|
dbg("spamd: will perform setuids? $setuid_to_user");
|
|
|
|
if ( $opt{'vpopmail'} ) {
|
|
if ( !$opt{'username'} ) {
|
|
die "spamd: cannot use --vpopmail without -u\n";
|
|
}
|
|
}
|
|
|
|
if ( $opt{'virtual-config-dir'} ) {
|
|
if ( !$opt{'username'} ) {
|
|
die "spamd: cannot use --virtual-config-dir without -u\n";
|
|
}
|
|
}
|
|
|
|
if ($opt{'sql-config'} && !$opt{'setuid-with-sql'}) {
|
|
if ( !$opt{'username'} ) {
|
|
die "spamd: cannot use --sql-config without -u\n";
|
|
}
|
|
}
|
|
|
|
if ($opt{'ldap-config'} && !$opt{'setuid-with-ldap'}) {
|
|
if ( !$opt{'username'} ) {
|
|
die "spamd: cannot use --ldap-config without -u\n";
|
|
}
|
|
}
|
|
|
|
# always copy the config, later code may disable
|
|
my $copy_config_p = 1;
|
|
|
|
my $current_user;
|
|
|
|
my $client; # used for the client connection ...
|
|
my $childlimit; # max number of kids allowed
|
|
my $timeout_tcp; # socket timeout (connect->headers), 0=no timeout
|
|
my $timeout_child; # processing timeout (headers->finish), 0=no timeout
|
|
my $clients_per_child; # number of clients each child should process
|
|
my %children; # current children
|
|
my @children_exited;
|
|
|
|
if ( defined $opt{'max-children'} ) {
|
|
$childlimit = $opt{'max-children'};
|
|
|
|
# Make sure that the values are at least 1
|
|
$childlimit = undef if ( $childlimit < 1 );
|
|
}
|
|
|
|
if ( defined $opt{'max-conn-per-child'} ) {
|
|
$clients_per_child = $opt{'max-conn-per-child'};
|
|
|
|
# Make sure that the values are at least 1
|
|
$clients_per_child = undef if ( $clients_per_child < 1 );
|
|
}
|
|
|
|
# Set some "sane" limits for defaults
|
|
$childlimit ||= 5;
|
|
$clients_per_child ||= 200;
|
|
|
|
if (defined $opt{'timeout-tcp'} && $opt{'timeout-tcp'} >= 0) {
|
|
$timeout_tcp = $opt{'timeout-tcp'};
|
|
$timeout_tcp = undef if ($timeout_tcp == 0);
|
|
}
|
|
else {
|
|
$timeout_tcp = 30;
|
|
}
|
|
|
|
if (defined $opt{'timeout-child'} && $opt{'timeout-child'} >= 0) {
|
|
$timeout_child = $opt{'timeout-child'};
|
|
$timeout_child = undef if ($timeout_child == 0);
|
|
}
|
|
else {
|
|
$timeout_child = 300;
|
|
}
|
|
|
|
# ensure scaling parameters are logical
|
|
if ($opt{'min-children'} < 1) {
|
|
$opt{'min-children'} = 1;
|
|
}
|
|
if ($opt{'min-spare'} < 0) {
|
|
$opt{'min-spare'} = 0;
|
|
}
|
|
if ($opt{'min-spare'} > $childlimit) {
|
|
$opt{'min-spare'} = $childlimit-1;
|
|
}
|
|
if ($opt{'max-spare'} < $opt{'min-spare'}) {
|
|
# emulate Apache behaviour:
|
|
# http://httpd.apache.org/docs-2.0/mod/prefork.html#maxspareservers
|
|
$opt{'max-spare'} = $opt{'min-spare'}+1;
|
|
}
|
|
|
|
my $dontcopy = 1;
|
|
if ( $opt{'create-prefs'} ) { $dontcopy = 0; }
|
|
|
|
my $orighome;
|
|
if ( defined $ENV{'HOME'} ) {
|
|
if ( defined $opt{'username'} )
|
|
{ # spamd is going to run as another user, so reset $HOME
|
|
if ( my $nh = ( getpwnam( $opt{'username'} ) )[7] ) {
|
|
$ENV{'HOME'} = $nh;
|
|
}
|
|
else {
|
|
die "spamd: unable to determine home directory for user '"
|
|
. $opt{'username'} . "'\n";
|
|
}
|
|
}
|
|
|
|
$orighome = $ENV{'HOME'}; # keep a copy for use by Razor, Pyzor etc.
|
|
delete $ENV{'HOME'}; # we do not want to use this when running spamd
|
|
}
|
|
|
|
# Do welcomelist later in tmp dir. Side effect: this will be done as -u user.
|
|
|
|
# Initialize SSL options
|
|
|
|
$opt{'server-key'} ||= "$LOCAL_RULES_DIR/certs/server-key.pem";
|
|
$opt{'server-cert'} ||= "$LOCAL_RULES_DIR/certs/server-cert.pem";
|
|
|
|
$opt{'ssl-verify'} = 1 if $opt{'ssl-ca-file'} || $opt{'ssl-ca-path'};
|
|
$opt{'ssl'} ||= $opt{'ssl-verify'};
|
|
if ($opt{'ssl-ca-file'} && !-e $opt{'ssl-ca-file'}) {
|
|
die "spamd: ssl-ca-file $opt{'ssl-ca-file'} does not exist\n";
|
|
}
|
|
if ($opt{'ssl-ca-path'} && !-e $opt{'ssl-ca-path'}) {
|
|
die "spamd: ssl-ca-path $opt{'ssl-ca-path'} does not exist\n";
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Server (listening) socket setup for the various supported types
|
|
|
|
dbg("spamd: socket module of choice: %s %s, Socket %s".
|
|
", %s PF_INET, %s PF_INET6, %s, AI_ADDRCONFIG %s",
|
|
$io_socket_module_name,
|
|
$io_socket_module_name->VERSION,
|
|
Socket->VERSION,
|
|
$have_inet4 ? 'have' : 'no',
|
|
$have_inet6 ? 'have' : 'no',
|
|
$have_getaddrinfo_in_core ? 'using Socket::getaddrinfo'
|
|
: $have_getaddrinfo_legacy ? 'using legacy Socket6::getaddrinfo'
|
|
: 'no getaddrinfo, using gethostbyname, IPv4-only',
|
|
$ai_addrconfig_flag ? "is supported" : "not supported",
|
|
);
|
|
|
|
my $have_ssl_module;
|
|
my @listen_sockets; # list of hashrefs, contains info on all listen sockets
|
|
my $server_select_mask;
|
|
|
|
my @listen_socket_specs = @{$opt{'listen-sockets'}};
|
|
|
|
{ # merge legacy option --socketpath into @listen_socket_specs
|
|
my $socketpath = $opt{'socketpath'};
|
|
if (defined $socketpath && $socketpath ne '') {
|
|
$socketpath =~ m{^/}
|
|
or die "socketpath option should specify an absolute path: $socketpath";
|
|
push(@listen_socket_specs, $socketpath);
|
|
}
|
|
}
|
|
|
|
# supply a default socket (loopback IP address) if none specified
|
|
push(@listen_socket_specs, 'localhost') if !@listen_socket_specs;
|
|
|
|
for (@listen_socket_specs) {
|
|
my $socket_specs = $_;
|
|
|
|
$socket_specs = '*' if $socket_specs eq ''; # empty implies all interfaces
|
|
|
|
local($1,$2,$3,$4,$5,$6);
|
|
if ($socket_specs =~
|
|
m{^ (?: (ssl) : )?
|
|
( / .* ) \z }xsi) { # unix socket - absolute path
|
|
my($proto,$path) = ($1, $2);
|
|
# $proto = 'ssl' if defined $opt{'ssl'} || defined $opt{'ssl-port'};
|
|
$proto = !defined($proto) ? '' : lc($proto);
|
|
# abstracted out the setup-retry code
|
|
dbg("spamd: unix socket: %s", $path);
|
|
server_sock_setup(\&server_sock_setup_unix, $socket_specs, $path);
|
|
|
|
} elsif ($socket_specs =~
|
|
m{^ (?: (ssl) : )?
|
|
(?: \[ ( [^\]]* ) \]
|
|
| ( [a-z0-9._-]* )
|
|
| ( [a-f0-9]* : [a-f0-9]* : [a-f0-9:]*
|
|
(?: % [a-z0-9._~-]* )? \z )
|
|
| ( \* )
|
|
)?
|
|
(?: : ( [a-z0-9-]* ) )? \z }xsi) {
|
|
my($proto,$addr,$port) = ($1, $2||$3||$4||$5, $6);
|
|
$addr = 'localhost' if !defined $addr;
|
|
$proto = 'ssl' if defined $opt{'ssl'} || defined $opt{'ssl-port'};
|
|
$proto = !defined($proto) ? '' : lc($proto);
|
|
$port = $opt{'ssl-port'} if !defined $port && $proto eq 'ssl';
|
|
$port = $opt{'port'} if !defined $port || $port eq '';
|
|
$port = '783' if !defined $port || $port eq '';
|
|
if ($port ne '' && $port !~ /^(\d+)\z/) {
|
|
$port = ( getservbyname($port,'tcp') )[2];
|
|
$port or die "spamd: invalid port: $port, socket: $socket_specs\n";
|
|
}
|
|
# abstracted out the setup-retry code
|
|
dbg('spamd: %s socket specification: "%s", IP address: %s, port: %s',
|
|
$proto, $socket_specs, $addr, $port);
|
|
server_sock_setup(\&server_sock_setup_inet,
|
|
$socket_specs, $addr, $port, $proto eq 'ssl' ? 1 : 0);
|
|
} else {
|
|
die "Invalid socket specification syntax: $socket_specs\n";
|
|
}
|
|
}
|
|
|
|
@listen_sockets or die "No listen sockets specified, aborting\n";
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Check for server certs
|
|
if ( $have_ssl_module ) {
|
|
if ( !-e $opt{'server-key'} ) {
|
|
die "spamd: server key file $opt{'server-key'} does not exist\n";
|
|
}
|
|
if ( !-e $opt{'server-cert'} ) {
|
|
die "spamd: server certificate file $opt{'server-cert'} does not exist\n";
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
my $sockets_access_lock_tempfile; # a File::Temp object, if locking is needed
|
|
my $sockets_access_lock_fh; # per-child file handle on a lock file
|
|
|
|
my $backchannel = Mail::SpamAssassin::SubProcBackChannel->new();
|
|
my $scaling;
|
|
|
|
if (!$opt{'round-robin'})
|
|
{
|
|
my $max_children = $childlimit;
|
|
|
|
# change $childlimit to avoid churn when we startup and create loads
|
|
# of spare servers; when we're using scaling, it's not as important
|
|
# as it was with the old algorithm.
|
|
if ($childlimit > $opt{'max-spare'}) {
|
|
$childlimit = $opt{'max-spare'};
|
|
}
|
|
if ($childlimit < $opt{'min-children'}) {
|
|
$childlimit = $opt{'min-children'};
|
|
}
|
|
|
|
$scaling = Mail::SpamAssassin::SpamdForkScaling->new({
|
|
backchannel => $backchannel,
|
|
min_children => $opt{'min-children'},
|
|
max_children => $max_children,
|
|
min_idle => $opt{'min-spare'},
|
|
max_idle => $opt{'max-spare'},
|
|
cur_children_ref => \$childlimit
|
|
});
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
sub compose_listen_info_string {
|
|
my @listeninfo;
|
|
|
|
for my $socket_info (@listen_sockets) {
|
|
next if !$socket_info;
|
|
my $socket = $socket_info->{socket};
|
|
next if !$socket;
|
|
my $socket_specs = $socket_info->{specs};
|
|
|
|
if ($socket->isa('IO::Socket::UNIX')) {
|
|
push(@listeninfo, "UNIX domain socket " . $socket_info->{path});
|
|
|
|
} elsif ( $socket->isa('IO::Socket::INET') ||
|
|
$socket->isa('IO::Socket::INET6') ||
|
|
$socket->isa('IO::Socket::IP') ) {
|
|
push(@listeninfo, sprintf("%s [%s]:%s", ref $socket,
|
|
$socket_info->{ip_addr}, $socket_info->{port}));
|
|
|
|
} elsif ($socket->isa('IO::Socket::SSL')) {
|
|
push(@listeninfo, sprintf("SSL [%r]:%s", $socket_info->{ip_addr},
|
|
$socket_info->{port}));
|
|
}
|
|
}
|
|
|
|
# just for reporting at startup
|
|
return join(', ', @listeninfo);
|
|
}
|
|
|
|
sub server_sock_setup {
|
|
my($sub, @args) = @_;
|
|
|
|
# retry 3 times to bind to the listening socket; 3 seconds delay,
|
|
# max, but should allow a little time for any existing shutting-down
|
|
# server to complete shutdown
|
|
my $lastretry = 10;
|
|
for my $retry (1 .. $lastretry) {
|
|
if ($retry > 1) { sleep 1; }
|
|
|
|
eval { &$sub(@args) } and last; # success => break
|
|
|
|
if ($retry == $lastretry) {
|
|
die $@; # this is fatal
|
|
} else {
|
|
warn "server socket setup failed, retry $retry: $@";
|
|
# but retry
|
|
}
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Create the sockets
|
|
sub server_sock_setup_unix {
|
|
my($socket_specs, $path) = @_;
|
|
|
|
# see if the socket is in use: if we connect to the current socket, it
|
|
# means that spamd is already running, so we have to bail on our own.
|
|
# Yes, there is a window here: best we can do for now. There is almost
|
|
# certainly a better way, but we don't know it. Yet.
|
|
|
|
if (-e $path) {
|
|
unless (-S $path) {
|
|
die "spamd: file $path exists but is no socket, exiting\n";
|
|
}
|
|
|
|
if ( IO::Socket::UNIX->new( Peer => $path, Type => &SOCK_STREAM ) ) {
|
|
# socket bind successful: must already be running
|
|
|
|
# make sure not to enter this socket into @listen_sockets,
|
|
# otherwise exit handlers would unlink it!
|
|
die "spamd: already running on $path, exiting\n";
|
|
}
|
|
else {
|
|
dbg("spamd: removing stale socket file $path");
|
|
unlink $path;
|
|
}
|
|
}
|
|
if (not -d (File::Spec->splitpath($path))[1]) {
|
|
die "spamd: directory for $path does not exist, exiting\n";
|
|
}
|
|
|
|
my %socket = (
|
|
Local => $path,
|
|
Type => &SOCK_STREAM,
|
|
Listen => &SOMAXCONN,
|
|
);
|
|
dbg("spamd: creating UNIX socket:\n" . join("\n", map { " $_: " . (defined $socket{$_} ? $socket{$_} : "(undef)") } sort keys %socket));
|
|
my $server_unix = IO::Socket::UNIX->new(%socket);
|
|
|
|
# sanity check! cf. bug 3490
|
|
if (not $server_unix or not -S $path) {
|
|
unless ($server_unix) {
|
|
dbg "spamd: socket path might have been truncated due to system limits\n";
|
|
die "spamd: could not create UNIX socket on $path: $!\n";
|
|
}
|
|
my $hostpath = $server_unix->hostpath();
|
|
if ($hostpath ne $path) {
|
|
warn "spamd: socket path was truncated at position " . length($hostpath) . "\n";
|
|
warn "spamd: leaving stale socket at $hostpath\n" if -S $hostpath;
|
|
die "spamd: path length for UNIX socket on $path exceeds system limit, exiting\n";
|
|
}
|
|
else {
|
|
die "spamd: could not find newly-created UNIX socket on $path: $!\n";
|
|
}
|
|
}
|
|
|
|
my $mode = $opt{socketmode};
|
|
if ($mode) {
|
|
$mode = oct $mode;
|
|
} else {
|
|
$mode = 0666; # default
|
|
}
|
|
|
|
my $owner = $opt{socketowner};
|
|
my $group = $opt{socketgroup};
|
|
if ($owner || $group) {
|
|
my $uid = -1;
|
|
my $gid = -1;
|
|
if ($owner) {
|
|
my ($login,$pass,$puid,$pgid) = getpwnam($owner)
|
|
or die "spamd: $owner not in passwd database\n";
|
|
$uid = $puid;
|
|
}
|
|
if ($group) {
|
|
my ($name,$pass,$ggid,$members) = getgrnam($group)
|
|
or die "spamd: $group not in group database\n";
|
|
$gid = $ggid;
|
|
}
|
|
if (!chown $uid, $gid, $path) {
|
|
die "spamd: could not chown $path to $uid/$gid: $!";
|
|
}
|
|
}
|
|
|
|
if (!chmod $mode, $path) { # make sure everybody can talk to it
|
|
die "spamd: could not chmod $path to $mode: $!";
|
|
}
|
|
|
|
push(@listen_sockets, { specs => $socket_specs,
|
|
path => $path,
|
|
socket => $server_unix,
|
|
fd => $server_unix->fileno }) if $server_unix;
|
|
1;
|
|
}
|
|
|
|
sub server_sock_setup_inet {
|
|
my($socket_specs, $addr, $port, $ssl) = @_;
|
|
|
|
$have_inet4 || $have_inet6
|
|
or warn "spamd: neither the PF_INET (IPv4) nor the PF_INET6 (IPv6) ".
|
|
"protocol families seem to be available, pushing our luck anyway\n";
|
|
|
|
my $ai_family = &AF_UNSPEC; # defaults to any address family (i.e. both)
|
|
if ($have_inet6 && (!$have_inet4 || $opt{'force_ipv6'})) {
|
|
$ai_family = &AF_INET6;
|
|
} elsif ($have_inet4 && (!$have_inet6 || $opt{'force_ipv4'})) {
|
|
$ai_family = &AF_INET;
|
|
}
|
|
my($error, @addresses);
|
|
if (!defined $addr || lc $addr eq 'localhost') { # loopback interface
|
|
push(@addresses, '::1')
|
|
if $ai_family == &AF_UNSPEC || $ai_family == &AF_INET6;
|
|
push(@addresses, '127.0.0.1')
|
|
if $ai_family == &AF_UNSPEC || $ai_family == &AF_INET;
|
|
} elsif ($addr eq '*' || $addr eq '') { # any address
|
|
push(@addresses, '::')
|
|
if $ai_family == &AF_UNSPEC || $ai_family == &AF_INET6;
|
|
push(@addresses, '0.0.0.0')
|
|
if $ai_family == &AF_UNSPEC || $ai_family == &AF_INET;
|
|
} else {
|
|
($error, @addresses) = ip_or_name_to_ip_addresses($addr, $ai_family);
|
|
}
|
|
die "spamd: invalid address for a listen socket: \"$socket_specs\": $error\n"
|
|
if $error;
|
|
die "spamd: no valid address for a listen socket: \"$socket_specs\"\n"
|
|
if !@addresses;
|
|
|
|
dbg("spamd: attempting to listen on IP addresses: %s, port %d",
|
|
join(', ',@addresses), $port);
|
|
my(@diag_succ, @diag_fail);
|
|
for my $adr (@addresses) {
|
|
my %sockopt = (
|
|
LocalAddr => $adr,
|
|
LocalPort => $port,
|
|
Type => &SOCK_STREAM,
|
|
Proto => 'tcp',
|
|
ReuseAddr => 1,
|
|
Listen => &SOMAXCONN,
|
|
);
|
|
$sockopt{V6Only} = 1 if $io_socket_module_name eq 'IO::Socket::IP'
|
|
&& IO::Socket::IP->VERSION >= 0.09;
|
|
if ($ssl) {
|
|
if (!$have_ssl_module) {
|
|
eval { require IO::Socket::SSL; }
|
|
or die "spamd: SSL encryption requested, ".
|
|
"but IO::Socket::SSL is unavailable ($@)\n";
|
|
$have_ssl_module = 1;
|
|
}
|
|
%sockopt = (%sockopt, (
|
|
SSL_server => 1,
|
|
SSL_key_file => $opt{'server-key'},
|
|
SSL_cert_file => $opt{'server-cert'},
|
|
));
|
|
my $ssl_mode;
|
|
if ($opt{'ssl-verify'}) {
|
|
$ssl_mode = Net::SSLeay::VERIFY_PEER()
|
|
| Net::SSLeay::VERIFY_FAIL_IF_NO_PEER_CERT();
|
|
if ($opt{'ssl-ca-file'}) {
|
|
$sockopt{SSL_ca_file} = $opt{'ssl-ca-file'};
|
|
}
|
|
if ($opt{'ssl-ca-path'}) {
|
|
$sockopt{SSL_ca_path} = $opt{'ssl-ca-path'};
|
|
}
|
|
$sockopt{SSL_check_crl} = 0;
|
|
$sockopt{SSL_verifycn_scheme} = 'none';
|
|
$sockopt{SSL_verifycn_publicsuffix} = '';
|
|
} else {
|
|
$ssl_mode = Net::SSLeay::VERIFY_NONE()
|
|
| Net::SSLeay::VERIFY_FAIL_IF_NO_PEER_CERT();
|
|
}
|
|
$sockopt{SSL_verify_mode} = $ssl_mode;
|
|
}
|
|
dbg("spamd: creating %s socket: %s",
|
|
$ssl ? 'IO::Socket::SSL' : $io_socket_module_name,
|
|
join(', ', map("$_: ".(defined $sockopt{$_} ? $sockopt{$_} : "(undef)"),
|
|
sort keys %sockopt)));
|
|
my $server_inet = $ssl ? IO::Socket::SSL->new(%sockopt)
|
|
: $io_socket_module_name->new(%sockopt);
|
|
my $diag;
|
|
if (!$server_inet) {
|
|
$diag = sprintf("could not create %s socket on [%s]:%s: %s",
|
|
$ssl ? 'IO::Socket::SSL' : $io_socket_module_name,
|
|
$adr, $port, $ssl && $IO::Socket::SSL::SSL_ERROR ?
|
|
"$!,$IO::Socket::SSL::SSL_ERROR" : $!);
|
|
push(@diag_fail, $diag);
|
|
} else {
|
|
$diag = sprintf("created %s socket on [%s]:%s",
|
|
$ssl ? 'IO::Socket::SSL' : $io_socket_module_name,
|
|
$adr, $port);
|
|
push(@diag_succ, $diag);
|
|
push(@listen_sockets, { specs => $socket_specs,
|
|
ip_addr => $adr, port => $port,
|
|
socket => $server_inet,
|
|
fd => $server_inet->fileno });
|
|
}
|
|
dbg("spamd: %s", $diag);
|
|
}
|
|
if (!@diag_fail) {
|
|
# no failures, nothing to report
|
|
} elsif (@diag_succ) { # some failures and some success
|
|
# just warn of all attempts, successful and failed
|
|
warn "spamd: $_\n" for @diag_succ;
|
|
warn "spamd: $_\n" for @diag_fail;
|
|
} else { # all failed, no success
|
|
warn "spamd: $_\n" for @diag_fail[0 .. $#diag_fail-1];
|
|
die "spamd: $_\n" for $diag_fail[-1];
|
|
}
|
|
|
|
1;
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# for select() purposes: make a map of the server socket FDs
|
|
map_server_sockets();
|
|
|
|
if (!$scaling && @listen_sockets > 1) {
|
|
require File::Temp;
|
|
|
|
# Have multiple sockets and autonomous child processes (--round-robin),
|
|
# prepare an anonymous lock file to protect access to select+accept.
|
|
|
|
# using the same choice of a tmp dir as in Util::secure_tmpfile()
|
|
my $tmpdir = untaint_file_path($ENV{'TMPDIR'} || File::Spec->tmpdir);
|
|
|
|
# the file will be automatically removed by DESTROY on program exit
|
|
$sockets_access_lock_tempfile =
|
|
File::Temp->new(DIR => $tmpdir, SUFFIX => '.lck', EXLOCK => 0);
|
|
|
|
dbg("spamd: created a lock file %s to protect select+accept",
|
|
$sockets_access_lock_tempfile->filename);
|
|
}
|
|
|
|
if ( defined $opt{'pidfile'} ) {
|
|
$opt{'pidfile'} = untaint_file_path( $opt{'pidfile'} );
|
|
}
|
|
|
|
|
|
my $spamtest = Mail::SpamAssassin->new(
|
|
{
|
|
dont_copy_prefs => $dontcopy,
|
|
rules_filename => ( $opt{'configpath'} || 0 ),
|
|
site_rules_filename => ( $opt{'siteconfigpath'} || 0 ),
|
|
pre_config_text => join("\n", @{$opt{'pre'}})."\n",
|
|
post_config_text => join("\n", @{$opt{'cf'}})."\n",
|
|
force_ipv4 => ( $opt{'force_ipv4'} || 0 ),
|
|
local_tests_only => ( $opt{'local'} || 0 ),
|
|
debug => ( $opt{'debug'} || 0 ),
|
|
paranoid => ( $opt{'paranoid'} || 0 ),
|
|
require_rules => 1,
|
|
skip_prng_reseeding => 1, # let us do the reseeding by ourselves
|
|
home_dir_for_helpers => (
|
|
defined $opt{'home_dir_for_helpers'}
|
|
? $opt{'home_dir_for_helpers'}
|
|
: $orighome
|
|
),
|
|
PREFIX => $PREFIX,
|
|
DEF_RULES_DIR => $DEF_RULES_DIR,
|
|
LOCAL_RULES_DIR => $LOCAL_RULES_DIR,
|
|
LOCAL_STATE_DIR => $LOCAL_STATE_DIR
|
|
}
|
|
);
|
|
|
|
#Enable Timing?
|
|
if ($opt{'timing'}) {
|
|
$spamtest->timer_enable();
|
|
}
|
|
|
|
# if $clients_per_child == 1, there's no point in copying configs around
|
|
unless ($clients_per_child > 1) {
|
|
# unset $copy_config_p so we don't bother trying to copy things back
|
|
# after closing the connection
|
|
$copy_config_p = 0;
|
|
}
|
|
|
|
# See Bug 6837: establishing a __DIE__ handler should be done after
|
|
# most modules have been loaded, as the $^S cannot distinguish
|
|
# true failures from eval attempt failures within a 'require'-d module.
|
|
# If the problem persists in some late-lodaded modules, we may need
|
|
# to tighten the condition to something like 'if defined $^S && !$^S'.
|
|
#
|
|
# redirect __WARN__ and __DIE__
|
|
# do not trap warnings here based on eval scope; evals are very
|
|
# common throughout. die()s can be trapped though.
|
|
$SIG{__WARN__} = sub {
|
|
log_message("warn", $_[0]);
|
|
};
|
|
$SIG{__DIE__} = sub {
|
|
# see http://use.perl.org/comments.pl?mode=flat&sid=33872 for $^S
|
|
log_message("error", $_[0]) unless $^S;
|
|
};
|
|
|
|
## DAEMONIZE! ##
|
|
|
|
my $originalparent = $$;
|
|
$opt{'daemonize'} and daemonize();
|
|
|
|
# bug 3443: setup signal handlers before the kids since we may have to
|
|
# kill them... make sure this happens before setting up the pidfile to
|
|
# avoid a race condition.
|
|
my $got_sighup;
|
|
setup_parent_sig_handlers();
|
|
|
|
# should be done post-daemonize such that any files created by this
|
|
# process are written with the right ownership and everything.
|
|
seteuid_to_user();
|
|
preload_modules_with_tmp_homedir();
|
|
restore_euid();
|
|
|
|
# this must be after preload_modules_with_tmp_homedir(), for bug 5606
|
|
$spamtest->init_learner({
|
|
opportunistic_expire_check_only => 1,
|
|
});
|
|
|
|
# bayes DBs may still be tied() at this point, so untie them and such.
|
|
$spamtest->finish_learner();
|
|
|
|
# If we're going to be switching users in check(), let's backup the
|
|
# fresh configuration now for later restoring ... MUST be placed after
|
|
# the M::SA creation.
|
|
my %conf_backup;
|
|
my %msa_backup;
|
|
|
|
if ($copy_config_p) {
|
|
foreach( 'username', 'user_dir', 'userstate_dir', 'learn_to_journal' ) {
|
|
$msa_backup{$_} = $spamtest->{$_} if (exists $spamtest->{$_});
|
|
}
|
|
|
|
$spamtest->copy_config(undef, \%conf_backup) ||
|
|
die "spamd: error returned from copy_config\n";
|
|
}
|
|
|
|
# bonus: SIGUSR2 to dump a stack trace. this is never reset
|
|
my $current_msgid = "(none)";
|
|
$SIG{USR2} = \&backtrace_handler if !am_running_on_windows();
|
|
|
|
# log server started, but processes watching the log to wait for connect
|
|
# should wait until they see the pid, after signal handlers are in place
|
|
# FIXME: two calls are one too much
|
|
info("spamd: server started on %s (running version %s)",
|
|
compose_listen_info_string(), Mail::SpamAssassin::Version());
|
|
|
|
my $remote_port;
|
|
|
|
# Make the pidfile ...
|
|
if (defined $opt{'pidfile'}) {
|
|
if (open PIDF, ">$opt{'pidfile'}") {
|
|
print PIDF "$$\n";
|
|
close PIDF;
|
|
}
|
|
else {
|
|
warn "spamd: cannot write to PID file: $!\n";
|
|
}
|
|
}
|
|
|
|
# The "prefork_init" plugin callback is called in the parent process shortly
|
|
# before forking off child processes. It allows plugins which were activated
|
|
# by the master spamd process to prepare for a fork, e.g. by closing or
|
|
# dropping some resources which won't be of any use by a child process.
|
|
#
|
|
$spamtest->call_plugins("prefork_init"); # since SA 3.4.0
|
|
|
|
# now allow waiting processes to connect, if they're watching the log.
|
|
# The test suite does this!
|
|
info("spamd: server pid: $$");
|
|
kill("USR1",$originalparent) if ($opt{'daemonize'});
|
|
|
|
# Fork off our children.
|
|
for ( 1 .. $childlimit ) {
|
|
spawn();
|
|
}
|
|
|
|
if ($scaling) {
|
|
$scaling->set_server_fh(map($_->{socket},@listen_sockets));
|
|
}
|
|
|
|
while (1) {
|
|
if (!$scaling) {
|
|
# wait for a signal (ie: child's death)
|
|
# bug 4190: use a time-limited sleep, and call child_handler() even
|
|
# if haven't received a SIGCHLD, due to inherent race condition
|
|
sleep 10;
|
|
} else {
|
|
$scaling->main_server_poll($opt{'server-scale-period'});
|
|
}
|
|
# bug 6377: on win32 the parent never receives SIGCHLD
|
|
# child_handler() if !$scaling || am_running_on_windows();
|
|
child_handler(); # it doesn't hurt to call child_handler unconditionally
|
|
|
|
child_cleaner();
|
|
|
|
do_sighup_restart() if defined $got_sighup;
|
|
|
|
for (my $i = keys %children; $i < $childlimit; $i++) {
|
|
spawn();
|
|
}
|
|
}
|
|
|
|
# Kicks off a kid ...
|
|
sub spawn {
|
|
my $pid;
|
|
|
|
$backchannel->setup_backchannel_parent_pre_fork();
|
|
|
|
# block signal for fork
|
|
my $sigset;
|
|
if (!am_running_on_windows()) {
|
|
$sigset = POSIX::SigSet->new( POSIX::SIGINT(), POSIX::SIGCHLD() );
|
|
sigprocmask( POSIX::SIG_BLOCK(), $sigset )
|
|
or die "spamd: cannot block SIGINT/SIGCHLD for fork: $!\n";
|
|
}
|
|
|
|
$pid = fork();
|
|
die "spamd: fork: $!" unless defined $pid;
|
|
|
|
if ($pid) {
|
|
## PARENT
|
|
|
|
$children{$pid} = 1;
|
|
info("spamd: server successfully spawned child process, pid $pid");
|
|
$backchannel->setup_backchannel_parent_post_fork($pid);
|
|
if ($scaling) {
|
|
$scaling->add_child($pid);
|
|
}
|
|
if (!am_running_on_windows()) {
|
|
sigprocmask( POSIX::SIG_UNBLOCK(), $sigset )
|
|
or die "spamd: cannot unblock SIGINT/SIGCHLD for fork: $!\n";
|
|
}
|
|
#Changing to return the process id to improve communications for bug 6304
|
|
return $pid;
|
|
}
|
|
else {
|
|
## CHILD
|
|
|
|
# Reset signal handling to default settings, and unblock.
|
|
# These lines must be as soon as possible after the fork (bug 4304)
|
|
setup_child_sig_handlers();
|
|
if (!am_running_on_windows()) {
|
|
sigprocmask( POSIX::SIG_UNBLOCK(), $sigset )
|
|
or die "spamd: cannot unblock SIGINT/SIGCHLD for fork: $!\n";
|
|
}
|
|
|
|
srand; # reseed pseudorandom number generator soon for each child process
|
|
if ($sockets_access_lock_tempfile) {
|
|
# A lock will be required across select+accept in a child processes,
|
|
# Bug 6996. Need to have a per-child filehandle on the same lock file
|
|
# for flock to work, let's dup(2) the parent's file handle.
|
|
my $fname = $sockets_access_lock_tempfile->filename;
|
|
$sockets_access_lock_fh = IO::File->new($fname, "+>");
|
|
$sockets_access_lock_fh or die "Can't open a lock file $fname: $!";
|
|
}
|
|
|
|
# support non-root use
|
|
if ( $opt{'username'} ) {
|
|
my ( $uuid, $ugid ) = ( getpwnam( $opt{'username'} ) )[ 2, 3 ];
|
|
if ( !defined $uuid || $uuid == 0 ) {
|
|
die "spamd: cannot run as nonexistent user or root with -u option\n";
|
|
}
|
|
|
|
if ( $opt{'groupname'} ) {
|
|
$ugid = getgrnam( $opt{'groupname'} ) || $ugid;
|
|
}
|
|
|
|
# bug 5518: assignments to $) and $( don't always work on all platforms
|
|
# bug 3900: assignments to $> and $< problems with BSD perl bug
|
|
# use the POSIX functions to hide the platform specific workarounds
|
|
dbg("spamd: Privilege de-escalation from user $< and groups $(\n");
|
|
my $togids = "$ugid ".get_user_groups($uuid);
|
|
if ($( ne $togids || $) ne $togids) {
|
|
$! = 0; POSIX::setgid($ugid); # set effective and real gid
|
|
if ($!) { warn("spamd: POSIX::setgid $ugid failed: $!\n"); }
|
|
$! = 0; $( = $ugid;
|
|
if ($!) { warn("spamd: failed to set gid $ugid: $!\n"); }
|
|
# set effective and real gid/grouplist another way because we lack initgroups in Perl
|
|
$! = 0; $) = $togids;
|
|
if ($!) {
|
|
# could be perl 5.30 bug #134169, let's be safe
|
|
if (grep { $_ eq '0' } split(/ /, ${)})) {
|
|
die("spamd: failed to set effective gid $togids: $!\n");
|
|
} else {
|
|
warn("spamd: failed to set effective gid $togids: $!\n");
|
|
}
|
|
}
|
|
} else {
|
|
dbg("spamd: Group already set to $(");
|
|
}
|
|
if ($< != $uuid || $> != $uuid) {
|
|
$! = 0; POSIX::setuid($uuid); # set effective and real UID
|
|
if ($!) { warn("spamd: POSIX::setuid $uuid failed: $!\n"); }
|
|
$! = 0; $< = $uuid; $> = $uuid; # bug 5574
|
|
if ($!) { warn("spamd: setuid $uuid failed: $!\n"); }
|
|
dbg("spamd: now running as: ruid=$< euid=$> rgid=$( egid=$)");
|
|
|
|
# keep the sanity check to catch problems like bug 3900 just in case
|
|
if ( $> != $uuid and $> != ( $uuid - 2**32 ) ) {
|
|
sleep(1); # prevent spamd fork flooding
|
|
die "spamd: setuid to uid $uuid failed (ruid=$<, euid=$>), not started as root?\n";
|
|
}
|
|
} else {
|
|
dbg("spamd: Uid already set to $<");
|
|
}
|
|
}
|
|
|
|
# set process name where supported
|
|
# this will help make it clear via process listing which is child/parent
|
|
$0 = 'spamd child';
|
|
|
|
# Let's call spamd_child_init only after root privs are dropped
|
|
# Mail::SpamAssassin::main() will also run this to set global_state_dir
|
|
$spamtest->call_plugins("spamd_child_init");
|
|
|
|
$backchannel->setup_backchannel_child_post_fork();
|
|
if ($scaling) { # only do this once, for efficiency; $$ is a syscall
|
|
$scaling->set_my_pid($$);
|
|
}
|
|
|
|
# handle $clients_per_child connections, then die in "old" age...
|
|
my $orders;
|
|
for ( my $i = 0 ; $i < $clients_per_child ; $i++ ) {
|
|
if ($scaling) {
|
|
$scaling->update_child_status_idle();
|
|
$orders = $scaling->wait_for_orders(); # and sleep...
|
|
|
|
if ($orders != PFORDER_ACCEPT) {
|
|
info("spamd: unknown order: $orders");
|
|
}
|
|
}
|
|
|
|
# use a large eval scope to catch die()s and ensure they
|
|
# don't kill the server.
|
|
my $evalret = eval { accept_a_conn($scaling ? 0.5 : undef); };
|
|
|
|
if (!defined $evalret) {
|
|
warn("spamd: error: $@, continuing\n");
|
|
if ($client) { $client->close(); } # avoid fd leaks
|
|
}
|
|
elsif ($evalret == -1) {
|
|
# serious error; used for accept() failure
|
|
die("spamd: respawning server\n");
|
|
}
|
|
|
|
$spamtest->call_plugins("spamd_child_post_connection_close");
|
|
|
|
# if we changed UID during processing, change back!
|
|
if ($setuid_to_user && ($> != $<) && ($> != ($< - 2**32))) {
|
|
$) = "$( $("; # change eGID
|
|
$> = $<; # change eUID
|
|
|
|
# check again; ensure the change happened
|
|
if ($> != $< && ($> != ( $< - 2**32))) {
|
|
# make it fatal to avoid security breaches
|
|
die("spamd: return setuid failed");
|
|
}
|
|
}
|
|
|
|
if ($copy_config_p) {
|
|
# use a timeout! There are bugs in Storable on certain platforms
|
|
# that can cause spamd to hang -- see bug 3828 comment 154.
|
|
# we don't use Storable any more, but leave this in -- just
|
|
# in case.
|
|
# bug 4699: this is the alarm that often ends up with an empty $@
|
|
|
|
my $timer = Mail::SpamAssassin::Timeout->new({ secs => 20 });
|
|
my $err = $timer->run(sub {
|
|
|
|
while(my($k,$v) = each %msa_backup) {
|
|
$spamtest->{$k} = $v;
|
|
}
|
|
|
|
# if we changed user, we would have also loaded up new configs
|
|
# (potentially), so let's restore back the saved version we
|
|
# had before.
|
|
$spamtest->copy_config(\%conf_backup, undef) ||
|
|
die "spamd: error returned from copy_config\n";
|
|
});
|
|
|
|
if ($timer->timed_out()) {
|
|
warn("spamd: copy_config timeout, respawning child process after ".
|
|
($i+1)." messages");
|
|
exit; # so that the master spamd can respawn
|
|
}
|
|
}
|
|
undef $current_user;
|
|
|
|
#LOG TIMING
|
|
if ($opt{'timing'}) {
|
|
info("timing: " . $spamtest->timer_report());
|
|
} else {
|
|
dbg("timing: " . $spamtest->timer_report()) if would_log('dbg', 'timing');
|
|
}
|
|
}
|
|
|
|
# If the child lives to get here, it will die ... Muhaha.
|
|
exit;
|
|
}
|
|
}
|
|
|
|
sub accept_from_any_server_socket {
|
|
my($timeout) = @_;
|
|
my($client, $selected_socket_info, $socket, $locked);
|
|
|
|
eval {
|
|
if (!@listen_sockets) {
|
|
# nothing?
|
|
die "no sockets?";
|
|
|
|
} elsif (@listen_sockets == 1) {
|
|
$selected_socket_info = $listen_sockets[0];
|
|
|
|
} else {
|
|
# determine which of our server FDs is ready using select().
|
|
# We only need to do this if we have more than one server
|
|
# socket supported, since otherwise there can only be one socket
|
|
# with a client waiting.
|
|
# (TODO: we could extend the prefork protocol to pass this data)
|
|
|
|
if ($sockets_access_lock_fh) {
|
|
dbg("spamd: acquiring a lock over select+accept");
|
|
# with multiple sockets a lock across select+accept is needed, Bug 6996
|
|
flock($sockets_access_lock_fh, LOCK_EX)
|
|
or die "Can't acquire lock access to sockets: $!";
|
|
$locked = 1;
|
|
}
|
|
|
|
my $sel_mask_str = unpack('b*', $server_select_mask);
|
|
dbg("spamd: select() on fd bit field %s, %s, %s",
|
|
$sel_mask_str, defined $timeout ? "timeout $timeout" : "no timeout",
|
|
$locked ? "locked" : "not locked");
|
|
|
|
my $fdvec = $server_select_mask;
|
|
my $nfound = select($fdvec, undef, undef, $timeout);
|
|
|
|
if (!defined $nfound || $nfound < 0) {
|
|
die "select failed on fd bit field $sel_mask_str: $!";
|
|
} elsif (!$nfound) {
|
|
die "no fd ready, fd bit field $sel_mask_str";
|
|
}
|
|
|
|
my(@ready_fd) = # list of file descriptors ready for read
|
|
grep(defined $_->{fd} && vec($fdvec, $_->{fd}, 1), @listen_sockets);
|
|
if (!@ready_fd) {
|
|
die "no file descriptors matching a bit field " . unpack('b*',$fdvec);
|
|
} elsif (@ready_fd == 1) { # easy, just one is ready
|
|
$selected_socket_info = $ready_fd[0];
|
|
} else { # give equal opportunity to each ready socket
|
|
my $j = int rand(@ready_fd);
|
|
$selected_socket_info = $ready_fd[$j];
|
|
dbg("spamd: requests ready on multiple sockets, picking #%d out of %d",
|
|
$j+1, scalar @ready_fd);
|
|
}
|
|
|
|
} # end multiple sockets case
|
|
|
|
if ($selected_socket_info) {
|
|
my $socket = $selected_socket_info->{socket};
|
|
$socket or die "no socket???, impossible";
|
|
dbg("spamd: accept() on fd %d", $selected_socket_info->{fd});
|
|
$client = $socket->accept;
|
|
if (!defined $client) {
|
|
if (defined $socket) {
|
|
die sprintf("%s accept failed: %s\n", ref $socket,
|
|
$socket->isa('IO::Socket::SSL') ?
|
|
$socket->errstr : $@);
|
|
} else {
|
|
die "accept failed: no socket available: $!\n";
|
|
}
|
|
}
|
|
}
|
|
1; # end eval with success
|
|
|
|
} or do {
|
|
my $err = $@ ne '' ? $@ : "errno=$!"; chomp $err;
|
|
info("spamd: accept_a_conn: $err");
|
|
};
|
|
|
|
if ($locked) {
|
|
dbg("spamd: releasing a lock over select+accept");
|
|
flock($sockets_access_lock_fh, LOCK_UN)
|
|
or die "Can't release sockets-access lock: $!";
|
|
}
|
|
|
|
return ($client, $selected_socket_info);
|
|
}
|
|
|
|
sub accept_a_conn {
|
|
my ($timeout) = @_;
|
|
|
|
my $socket_info;
|
|
# $client is a global variable
|
|
($client, $socket_info) = accept_from_any_server_socket($timeout);
|
|
|
|
if ($scaling) {
|
|
$scaling->update_child_status_busy();
|
|
}
|
|
|
|
# Bah!
|
|
if ( !$client || !defined $client->connected() ) {
|
|
|
|
# this can happen when interrupted by SIGCHLD on Solaris,
|
|
# perl 5.8.0, and some other platforms with -m.
|
|
if ( $! == &Errno::EINTR ) {
|
|
return 0;
|
|
}
|
|
elsif ( $@ =~ /ssl3_get_record:wrong version number/ ||
|
|
$@ =~ /peer did not return a certificate/ ) {
|
|
# Handshake error, not speaking SSL? No need to respawn
|
|
return 0;
|
|
}
|
|
else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
$client->autoflush(1);
|
|
|
|
# keep track of start time
|
|
$spamtest->timer_reset;
|
|
my $start = time;
|
|
|
|
my ($remote_hostname, $remote_hostaddr, $local_port);
|
|
|
|
if ($client->isa('IO::Socket::UNIX')) {
|
|
$remote_hostname = 'localhost';
|
|
$remote_hostaddr = '127.0.0.1';
|
|
$remote_port = $socket_info->{path};
|
|
info("spamd: got connection over %s", $socket_info->{path});
|
|
}
|
|
else {
|
|
($remote_port, $remote_hostaddr, $remote_hostname, $local_port) =
|
|
peer_info_from_socket($client);
|
|
$remote_hostaddr or die 'failed to obtain port and ip from socket';
|
|
|
|
my $ssl_info = '';
|
|
if ($client->isa('IO::Socket::SSL')) {
|
|
$ssl_info = ', ';
|
|
my $ssl_version = $client->get_sslversion();
|
|
if (defined $ssl_version) {
|
|
$ssl_info .= $ssl_version.'/';
|
|
} else {
|
|
$ssl_version = $client->get_sslversion_int();
|
|
if ($ssl_version == 0x0304) { $ssl_info .= 'TLSv1.3/'; }
|
|
elsif ($ssl_version == 0x0303) { $ssl_info .= 'TLSv1.2/'; }
|
|
elsif ($ssl_version == 0x0302) { $ssl_info .= 'TLSv1.1/'; }
|
|
elsif ($ssl_version == 0x0301) { $ssl_info .= 'TLSv1.0/'; }
|
|
elsif ($ssl_version == 0x0300) { $ssl_info .= 'SSLv3/'; }
|
|
elsif ($ssl_version == 0x0002) { $ssl_info .= 'SSLv2/'; }
|
|
}
|
|
$ssl_info .= $client->get_cipher();
|
|
}
|
|
|
|
my $msg = sprintf("connection from %s [%s]:%s to port %d, fd %d%s",
|
|
$remote_hostname, $remote_hostaddr, $remote_port,
|
|
$local_port, $socket_info->{fd}, $ssl_info);
|
|
if (ip_is_allowed($remote_hostaddr)) {
|
|
info("spamd: $msg");
|
|
}
|
|
else {
|
|
warn("spamd: unauthorized $msg");
|
|
$client->close;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
local ($_);
|
|
eval {
|
|
Mail::SpamAssassin::Util::trap_sigalrm_fully(sub {
|
|
die "tcp timeout";
|
|
});
|
|
alarm $timeout_tcp if ($timeout_tcp);
|
|
# send the request to the child process
|
|
$_ = $client->getline;
|
|
};
|
|
alarm 0;
|
|
|
|
if ($@) {
|
|
if ($@ =~ /tcp timeout/) {
|
|
service_timeout("($timeout_tcp second socket timeout reading input from client)");
|
|
} else {
|
|
warn "spamd: $@";
|
|
}
|
|
$client->close;
|
|
return 0;
|
|
}
|
|
|
|
if ( !defined $_ ) {
|
|
protocol_error("(closed before headers)");
|
|
$client->close;
|
|
return 0;
|
|
}
|
|
|
|
s/\r?\n//;
|
|
|
|
# It might be a CHECK message, meaning that we should just check
|
|
# if it's spam or not, then return the appropriate response.
|
|
# If we get the PROCESS command, the client is going to send a
|
|
# message that we need to filter.
|
|
|
|
if (/(PROCESS|CHECK|SYMBOLS|REPORT|HEADERS|REPORT_IFSPAM) SPAMC\/(.*)/) {
|
|
my $method = $1;
|
|
my $version = $2;
|
|
eval {
|
|
Mail::SpamAssassin::Util::trap_sigalrm_fully(sub {
|
|
die "child processing timeout";
|
|
});
|
|
alarm $timeout_child if ($timeout_child);
|
|
check($method, $version, $start, $remote_hostname, $remote_hostaddr);
|
|
};
|
|
alarm 0;
|
|
|
|
if ($@) {
|
|
if ($@ =~ /child processing timeout/) {
|
|
service_timeout("($timeout_child second timeout while trying to $method)");
|
|
} else {
|
|
warn "spamd: $@";
|
|
}
|
|
$client->close();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
elsif (/(TELL) SPAMC\/(.*)/) {
|
|
my $method = $1;
|
|
my $version = $2;
|
|
eval {
|
|
Mail::SpamAssassin::Util::trap_sigalrm_fully(sub {
|
|
die "child processing timeout";
|
|
});
|
|
alarm $timeout_child if ($timeout_child);
|
|
dotell($method, $version, $start, $remote_hostname, $remote_hostaddr);
|
|
};
|
|
alarm 0;
|
|
|
|
if ($@) {
|
|
if ($@ =~ /child processing timeout/) {
|
|
service_timeout("($timeout_child second timeout while trying to $method)");
|
|
} else {
|
|
warn "spamd: $@";
|
|
}
|
|
$client->close();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
# Looks like a client is just seeing if we're alive or changed its mind
|
|
|
|
elsif (/(SKIP|PING) SPAMC\/(.*)/) {
|
|
my $method = $1;
|
|
my $version = $2;
|
|
|
|
if ($method eq 'SKIP') {
|
|
# It may be a SKIP message, meaning that the client (spamc)
|
|
# thinks it is too big to check. So we don't do any real work
|
|
# in that case.
|
|
info("spamd: skipped large message in %3.1f seconds", time - $start);
|
|
}
|
|
doskip_or_ping($method, $version,
|
|
$start, $remote_hostname, $remote_hostaddr);
|
|
}
|
|
|
|
# If it was none of the above, then we don't know what it was.
|
|
|
|
else {
|
|
protocol_error($_);
|
|
}
|
|
|
|
# Close out our connection to the client ...
|
|
$client->close();
|
|
return 1;
|
|
}
|
|
|
|
sub handle_setuid_to_user {
|
|
if ($spamtest->{paranoid}) {
|
|
die("spamd: in paranoid mode, still running as root: closing connection");
|
|
}
|
|
if (!am_running_on_windows()) {
|
|
warn("spamd: still running as root: user not specified with -u, "
|
|
. "not found, or set to root, falling back to $opt{'default-user'}\n");
|
|
|
|
my ($name, $pwd, $uid, $gid, $quota, $comment, $gcos, $dir, $etc) =
|
|
getpwnam($opt{'default-user'});
|
|
|
|
$) = (get_user_groups($uid)); # eGID
|
|
$> = $uid; # eUID
|
|
if (!defined($uid) || ($> != $uid and $> != ($uid - 2**32))) {
|
|
die("spamd: setuid to $opt{'default-user'} failed");
|
|
}
|
|
|
|
$spamtest->signal_user_changed(
|
|
{
|
|
username => $name,
|
|
user_dir => $dir
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
sub parse_body {
|
|
my ($client, $expected_length, $compress_zlib, $start_time) = @_;
|
|
|
|
my @msglines;
|
|
my $actual_length;
|
|
|
|
if ($compress_zlib && !defined($expected_length)) {
|
|
service_unavailable_error("Compress requires Content-length header");
|
|
return;
|
|
}
|
|
|
|
if ($compress_zlib) {
|
|
$actual_length = zlib_inflate_read($client, $expected_length, \@msglines);
|
|
if ($actual_length < 0) { return; }
|
|
$expected_length = $actual_length;
|
|
}
|
|
else {
|
|
@msglines = ();
|
|
$actual_length = 0;
|
|
while (defined($_ = $client->getline())) {
|
|
$actual_length += length($_);
|
|
push(@msglines, $_);
|
|
last if (defined $expected_length && $actual_length >= $expected_length);
|
|
}
|
|
}
|
|
|
|
# Now parse *only* the message headers; the MIME tree won't be generated
|
|
# yet, it will be done on demand later on.
|
|
my $mail = $spamtest->parse(\@msglines, 0,
|
|
!$timeout_child || !$start_time ? ()
|
|
: { master_deadline => $start_time + $timeout_child } );
|
|
|
|
return ($mail, $actual_length);
|
|
}
|
|
|
|
sub zlib_inflate_read {
|
|
my ($client, $expected_length, $msglinesref) = @_;
|
|
my $out;
|
|
my $actual_length;
|
|
|
|
eval {
|
|
require Compress::Zlib;
|
|
my ($zlib, $status) = Compress::Zlib::inflateInit();
|
|
if (!$zlib) { die "inflateInit failed: $status"; }
|
|
|
|
my $red = 0;
|
|
my $buf;
|
|
|
|
# TODO: inflate in smaller buffers instead of at EOF
|
|
while (1) {
|
|
my $numbytes = $client->read($buf, (1024 * 64), $red);
|
|
if (!defined $numbytes) {
|
|
die "read of zlib data failed: $!";
|
|
return -1;
|
|
}
|
|
last if $numbytes == 0;
|
|
$red += $numbytes;
|
|
}
|
|
|
|
if ($red > $expected_length) {
|
|
warn "spamd: zlib read $red > expected_length $expected_length\n";
|
|
substr ($buf, $expected_length) = '';
|
|
}
|
|
|
|
($out, $status) = $zlib->inflate($buf);
|
|
if ($status != Compress::Zlib::Z_STREAM_END()) {
|
|
die "failed to find end of zlib stream\n";
|
|
}
|
|
};
|
|
|
|
if ($@) {
|
|
service_unavailable_error("zlib: $@");
|
|
return -1;
|
|
}
|
|
|
|
$actual_length = length($out);
|
|
|
|
# TODO: split during inflate, too
|
|
# note that this preserves line endings
|
|
@{$msglinesref} = map { my $s=$_; $s=~s/$/\n/gs; $s } split(/\n/, $out);
|
|
return $actual_length;
|
|
}
|
|
|
|
sub parse_msgids {
|
|
my ($mail) = @_;
|
|
|
|
# Extract the Message-Id(s) for logging purposes.
|
|
my $msgid = $mail->get_pristine_header("Message-Id");
|
|
my $rmsgid = $mail->get_pristine_header("Resent-Message-Id");
|
|
foreach my $id ((\$msgid, \$rmsgid)) {
|
|
if ( $$id ) {
|
|
# no re "strict"; # since perl 5.21.8: Ranges of ASCII printables...
|
|
while ( $$id =~ s/\([^\(\)]*\)// )
|
|
{ } # remove comments and
|
|
$$id =~ s/^\s+|\s+$//g; # leading and trailing spaces
|
|
$$id =~ s/\s+/ /g; # collapse whitespaces
|
|
$$id =~ s/^.*?<(.*?)>.*$/$1/; # keep only the id itself
|
|
$$id =~ s/[^\x21-\x7e]/?/g; # replace all weird chars
|
|
$$id =~ s/[<>]/?/g; # plus all dangling angle brackets
|
|
$$id =~ s/^(.+)$/<$1>/; # re-bracket the id (if not empty)
|
|
}
|
|
}
|
|
return ($msgid, $rmsgid);
|
|
}
|
|
|
|
sub check {
|
|
my ( $method, $version, $start_time, $remote_hostname, $remote_hostaddr ) = @_;
|
|
local ($_);
|
|
my $expected_length;
|
|
my $compress_zlib;
|
|
|
|
# used to ensure we don't accidentally fork (bug 4370)
|
|
my $starting_self_pid = $$;
|
|
|
|
# Protocol version 1.0 and greater may have "User:" and
|
|
# "Content-length:" headers. But they're not required.
|
|
|
|
if ( $version > 1.0 ) {
|
|
my $hdrs = {};
|
|
|
|
return 0 unless (parse_headers($hdrs, $client));
|
|
|
|
$expected_length = $hdrs->{expected_length};
|
|
$compress_zlib = $hdrs->{compress_zlib};
|
|
}
|
|
|
|
return 0 unless do_user_handling();
|
|
if ($> == 0 && !am_running_on_windows()) {
|
|
die "spamd: still running as root! dying";
|
|
}
|
|
|
|
my $resp = "EX_OK";
|
|
|
|
# generate mail object from input
|
|
my ($mail, $actual_length) = parse_body($client, $expected_length,
|
|
$compress_zlib, $start_time);
|
|
return 0 unless defined($mail); # error
|
|
|
|
if ($compress_zlib) {
|
|
$expected_length = $actual_length; # previously it was the gzipped length
|
|
}
|
|
|
|
# attempt to fetch the message ids
|
|
my ($msgid, $rmsgid) = parse_msgids($mail);
|
|
|
|
$msgid ||= "(unknown)";
|
|
$current_user ||= "(unknown)";
|
|
$current_msgid = $msgid; # for the SIGUSR2 backtrace
|
|
info("spamd: " . ($method eq 'PROCESS' ? "processing" : "checking")
|
|
. " message $msgid"
|
|
. ( $rmsgid ? " aka $rmsgid" : "" )
|
|
. " for ${current_user}:$>");
|
|
|
|
# Check length if we're supposed to.
|
|
if (defined $expected_length && $actual_length != $expected_length) {
|
|
protocol_error(
|
|
"(Content-Length mismatch: Expected $expected_length bytes, got $actual_length bytes)"
|
|
);
|
|
$mail->finish();
|
|
return 0;
|
|
}
|
|
|
|
# Go ahead and check the message
|
|
$spamtest->init(1);
|
|
my $status = Mail::SpamAssassin::PerMsgStatus->new($spamtest, $mail);
|
|
$status->check();
|
|
|
|
my $msg_score = &Mail::SpamAssassin::Util::get_tag_value_for_score($status->get_score, $status->get_required_score, $status->is_spam);
|
|
my $msg_threshold = sprintf( "%2.1f", $status->get_required_score );
|
|
|
|
my $response_spam_status = "";
|
|
my $was_it_spam;
|
|
if ( $status->is_spam ) {
|
|
$response_spam_status = $method eq "REPORT_IFSPAM" ? "Yes" : "True";
|
|
$was_it_spam = 'identified spam';
|
|
}
|
|
else {
|
|
$response_spam_status = $method eq "REPORT_IFSPAM" ? "No" : "False";
|
|
$was_it_spam = 'clean message';
|
|
}
|
|
|
|
my $spamhdr = "Spam: $response_spam_status ; $msg_score / $msg_threshold";
|
|
|
|
if ( $method eq 'PROCESS' || $method eq 'HEADERS' ) {
|
|
|
|
$status->set_tag('REMOTEHOSTNAME', $remote_hostname);
|
|
$status->set_tag('REMOTEHOSTADDR', $remote_hostaddr);
|
|
|
|
# Build the message to send back and measure it
|
|
my $msg_resp = $status->rewrite_mail();
|
|
|
|
if ($method eq 'HEADERS') {
|
|
# just the headers; delete everything after first \015\012\015\012
|
|
$msg_resp =~ s/(\015?\012\015?\012).*$/$1/gs;
|
|
}
|
|
|
|
my $msg_resp_length = length($msg_resp);
|
|
|
|
if ( $version >= 1.3 ) # Spamc protocol 1.3 means multi hdrs are OK
|
|
{
|
|
syswrite_full_buffer( $client, "SPAMD/1.1 $resphash{$resp} $resp\r\n" .
|
|
"Content-length: $msg_resp_length\r\n" . $spamhdr . "\r\n\r\n" .
|
|
$msg_resp );
|
|
}
|
|
elsif (
|
|
$version >= 1.2 ) # Spamc protocol 1.2 means it accepts content-length
|
|
{
|
|
syswrite_full_buffer( $client, "SPAMD/1.1 $resphash{$resp} $resp\r\n" .
|
|
"Content-length: $msg_resp_length\r\n\r\n" . $msg_resp );
|
|
}
|
|
else # Earlier than 1.2 didn't accept content-length
|
|
{
|
|
syswrite_full_buffer( $client, "SPAMD/1.0 $resphash{$resp} $resp\r\n" . $msg_resp );
|
|
}
|
|
}
|
|
else # $method eq 'CHECK' et al
|
|
{
|
|
syswrite_full_buffer( $client, "SPAMD/1.1 $resphash{$resp} $resp\r\n" );
|
|
|
|
if ( $method eq "CHECK" ) {
|
|
syswrite( $client, "$spamhdr\r\n\r\n" );
|
|
}
|
|
else {
|
|
my $msg_resp = '';
|
|
|
|
if ( $method eq "REPORT"
|
|
or ( $method eq "REPORT_IFSPAM" and $status->is_spam ) )
|
|
{
|
|
$msg_resp = $status->get_report;
|
|
}
|
|
elsif ( $method eq "REPORT_IFSPAM" ) {
|
|
|
|
# message is ham, $msg_resp remains empty
|
|
}
|
|
elsif ( $method eq "SYMBOLS" ) {
|
|
$msg_resp = $status->get_names_of_tests_hit;
|
|
$msg_resp .= "\r\n" if ( $version < 1.3 );
|
|
}
|
|
else {
|
|
die "spamd: unknown method $method";
|
|
}
|
|
|
|
if ( $version >= 1.3 ) # Spamc protocol > 1.2 means multi hdrs are OK
|
|
{
|
|
my $msg_resp_length = length($msg_resp);
|
|
syswrite_full_buffer( $client,
|
|
"Content-length: $msg_resp_length\r\n" .
|
|
$spamhdr . "\r\n\r\n" . $msg_resp );
|
|
}
|
|
else {
|
|
syswrite_full_buffer( $client, $spamhdr . "\r\n\r\n" . $msg_resp );
|
|
}
|
|
}
|
|
}
|
|
|
|
my $scantime = sprintf( "%.1f", time - $start_time );
|
|
|
|
info("spamd: $was_it_spam ($msg_score/$msg_threshold) for $current_user:$> in"
|
|
. " $scantime seconds, $actual_length bytes." );
|
|
|
|
# add a summary "result:" line, based on mass-check format
|
|
my @extra;
|
|
push(@extra, "scantime=".$scantime, "size=$actual_length",
|
|
"user=".$current_user, "uid=".$>,
|
|
"required_score=".$msg_threshold,
|
|
"rhost=".$remote_hostname, "raddr=".$remote_hostaddr,
|
|
"rport=".$remote_port);
|
|
|
|
{
|
|
# no re "strict"; # since perl 5.21.8: Ranges of ASCII printables...
|
|
my $safe = $msgid; $safe =~ s/[\x00-\x20\s,]/_/gs; push(@extra, "mid=$safe");
|
|
}
|
|
if ($rmsgid) {
|
|
# no re "strict"; # since perl 5.21.8: Ranges of ASCII printables...
|
|
my $safe = $rmsgid; $safe =~ s/[\x00-\x20\s,]/_/gs; push(@extra, "rmid=$safe");
|
|
}
|
|
if (defined $status->{bayes_score}) {
|
|
push(@extra, "bayes=".sprintf("%06f", $status->{bayes_score}));
|
|
}
|
|
push(@extra, "autolearn=".$status->get_autolearn_status());
|
|
push(@extra, $status->get_spamd_result_log_items());
|
|
|
|
my $yorn = $status->is_spam() ? 'Y' : '.';
|
|
my $score = $status->get_score();
|
|
my $tests = join(",", sort(grep(length,$status->get_names_of_tests_hit())));
|
|
|
|
my $log = sprintf("spamd: result: %s %2d - %s %s", $yorn, $score,
|
|
$tests, join(",", @extra));
|
|
info($log);
|
|
|
|
# bug 3808: log scan results to any listening plugins, too
|
|
$spamtest->call_plugins("log_scan_result", { result => $log });
|
|
|
|
# bug 3466: handle the bayes expiry bits after the results were returned to
|
|
# the client. keeps clients from timing out. if bayes_expiry_due is set,
|
|
# then the opportunistic check has already checked. go ahead and do another
|
|
# sync/expire run.
|
|
if ($status->{'bayes_expiry_due'}) {
|
|
dbg("spamd: bayes expiry was marked as due, running post-check");
|
|
$spamtest->rebuild_learner_caches();
|
|
$spamtest->finish_learner();
|
|
}
|
|
|
|
$status->finish(); # added by jm to allow GC'ing
|
|
$mail->finish();
|
|
|
|
# ensure we didn't accidentally fork (bug 4370)
|
|
if ($starting_self_pid != $$) {
|
|
eval { warn("spamd: accidental fork: $$ != $starting_self_pid"); };
|
|
force_die(0); # avoid END and dtor processing
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub dotell {
|
|
my ($method, $version, $start_time, $remote_hostname, $remote_hostaddr) = @_;
|
|
local ($_);
|
|
|
|
my $hdrs = {};
|
|
|
|
return 0 unless (parse_headers($hdrs, $client));
|
|
|
|
my $expected_length = $hdrs->{expected_length};
|
|
my $compress_zlib = $hdrs->{compress_zlib};
|
|
|
|
return 0 unless do_user_handling();
|
|
if ($> == 0 && !am_running_on_windows()) {
|
|
die "spamd: still running as root! dying";
|
|
}
|
|
|
|
if (!$opt{tell}) {
|
|
service_unavailable_error("TELL commands are not enabled, set the --allow-tell switch.");
|
|
return 0;
|
|
}
|
|
|
|
if ($hdrs->{set_local} && $hdrs->{remove_local}) {
|
|
protocol_error("Unable to set local and remove local in the same operation.");
|
|
return 0;
|
|
}
|
|
|
|
if ($hdrs->{set_remote} && $hdrs->{remove_remote}) {
|
|
protocol_error("Unable to set remote and remove remote in the same operation.");
|
|
return 0;
|
|
}
|
|
|
|
if ($opt{'sql-config'} && !defined($current_user)) {
|
|
unless (handle_user_sql('nobody')) {
|
|
service_unavailable_error("Error fetching user preferences via SQL");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if ($opt{'ldap-config'} && !defined($current_user)) {
|
|
handle_user_ldap('nobody');
|
|
}
|
|
|
|
my $resp = "EX_OK";
|
|
|
|
# generate mail object from input
|
|
my($mail, $actual_length) =
|
|
parse_body($client, $expected_length, $compress_zlib, $start_time);
|
|
|
|
return 0 unless defined($mail); # error
|
|
|
|
if ($compress_zlib) {
|
|
$expected_length = $actual_length; # previously it was the gzipped length
|
|
}
|
|
|
|
if ( $mail->get_header("X-Spam-Checker-Version") ) {
|
|
my $new_mail = $spamtest->parse($spamtest->remove_spamassassin_markup($mail), 1);
|
|
$mail->finish();
|
|
$mail = $new_mail;
|
|
}
|
|
|
|
# attempt to fetch the message ids
|
|
my ($msgid, $rmsgid) = parse_msgids($mail);
|
|
|
|
$msgid ||= "(unknown)";
|
|
$current_user ||= "(unknown)";
|
|
|
|
# Check length if we're supposed to.
|
|
if (defined $expected_length && $actual_length != $expected_length) {
|
|
protocol_error("(Content-Length mismatch: Expected $expected_length bytes, got $actual_length bytes)");
|
|
$mail->finish();
|
|
return 0;
|
|
}
|
|
|
|
my @did_set;
|
|
my @did_remove;
|
|
|
|
# bug 5740 Don't bayes learn if global configs disabkle bayes,
|
|
# also give user some control with userprefs bayes_learn_during_report option
|
|
|
|
if (defined $spamtest->{bayes_scanner} && $spamtest->{conf}->{bayes_learn_during_report}) {
|
|
if ($hdrs->{set_local}) {
|
|
my $status = $spamtest->learn($mail, undef, ($hdrs->{message_class} eq 'spam' ? 1 : 0), 0);
|
|
|
|
push(@did_set, 'local') if ($status->did_learn());
|
|
$status->finish();
|
|
}
|
|
|
|
if ($hdrs->{remove_local}) {
|
|
my $status = $spamtest->learn($mail, undef, undef, 1);
|
|
|
|
push(@did_remove, 'local') if ($status->did_learn());
|
|
$status->finish();
|
|
}
|
|
}
|
|
|
|
if ($hdrs->{set_remote}) {
|
|
require Mail::SpamAssassin::Reporter;
|
|
my $msgrpt = Mail::SpamAssassin::Reporter->new($spamtest, $mail);
|
|
|
|
push(@did_set, 'remote') if ($msgrpt->report());
|
|
}
|
|
|
|
if ($hdrs->{remove_remote}) {
|
|
require Mail::SpamAssassin::Reporter;
|
|
my $msgrpt = Mail::SpamAssassin::Reporter->new($spamtest, $mail);
|
|
|
|
push(@did_remove, 'remote') if ($msgrpt->revoke());
|
|
}
|
|
|
|
my $hdr = "";
|
|
my $info_str;
|
|
|
|
if (scalar(@did_set)) {
|
|
$hdr .= "DidSet: " . join(',', @did_set) . "\r\n";
|
|
$info_str .= " Setting " . join(',', @did_set) . " ";
|
|
}
|
|
|
|
if (scalar(@did_remove)) {
|
|
$hdr .= "DidRemove: " . join(',', @did_remove) . "\r\n";
|
|
$info_str .= " Removing " . join(',', @did_remove) . " ";
|
|
}
|
|
|
|
if (!$info_str) {
|
|
$info_str = " Did nothing ";
|
|
}
|
|
|
|
print $client "SPAMD/1.1 $resphash{$resp} $resp\r\n",
|
|
$hdr . "\r\n\r\n";
|
|
|
|
my $scantime = sprintf( "%.1f", time - $start_time );
|
|
|
|
info("spamd: Tell:${info_str}for $current_user:$> in"
|
|
. " $scantime seconds, $actual_length bytes");
|
|
|
|
$mail->finish();
|
|
return 1;
|
|
}
|
|
|
|
sub doskip_or_ping {
|
|
my ($method, $version, $start_time, $remote_hostname, $remote_hostaddr) = @_;
|
|
|
|
if ( $version >= 1.5 ) {
|
|
# Spamc protocol 1.5 means client is expected to send a protocol header
|
|
# (usually just a null header), followed by an empty line
|
|
# Fixes Bug 6187.
|
|
|
|
my $hdrs = {};
|
|
return 0 unless (parse_headers($hdrs, $client));
|
|
}
|
|
|
|
if ($method eq 'PING') {
|
|
print $client "SPAMD/1.5 $resphash{EX_OK} PONG\r\n";
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
###########################################################################
|
|
|
|
sub do_user_handling {
|
|
if ($setuid_to_user && $> == 0) {
|
|
handle_setuid_to_user();
|
|
}
|
|
|
|
if ( $opt{'sql-config'} && !defined($current_user) ) {
|
|
unless ( handle_user_sql('nobody') ) {
|
|
service_unavailable_error("Error fetching user preferences via SQL");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
if ( $opt{'ldap-config'} && !defined($current_user) ) {
|
|
handle_user_ldap('nobody');
|
|
}
|
|
|
|
dbg ("spamd: running as uid $>");
|
|
return 1;
|
|
}
|
|
|
|
# generalised header parser.
|
|
sub parse_headers {
|
|
my ($hdrs, $client) = @_;
|
|
|
|
my $got_user_header;
|
|
|
|
# max 255 headers
|
|
for my $hcount ( 0 .. 255 ) {
|
|
my $line = $client->getline;
|
|
|
|
unless (defined $line) {
|
|
protocol_error("(EOF during headers)");
|
|
return 0;
|
|
}
|
|
$line =~ s/\r\n$//;
|
|
|
|
if (!length $line) { # end of headers
|
|
return 1;
|
|
}
|
|
|
|
my ($header, $value) = split (/:\s*/, $line, 2);
|
|
unless (defined $value) {
|
|
protocol_error("(header not in 'Name: value' format)");
|
|
return 0;
|
|
}
|
|
|
|
if ($header eq 'Content-length') {
|
|
return 0 unless got_clen_header($hdrs, $header, $value);
|
|
}
|
|
elsif ($header eq 'User') {
|
|
return 0 unless got_user_header($hdrs, $header, $value);
|
|
$got_user_header++;
|
|
}
|
|
elsif ($header eq 'Message-class') {
|
|
return 0 unless got_message_class_header($hdrs, $header, $value);
|
|
}
|
|
elsif ($header eq 'Set') {
|
|
return 0 unless got_set_header($hdrs, $header, $value);
|
|
}
|
|
elsif ($header eq 'Remove') {
|
|
return 0 unless got_remove_header($hdrs, $header, $value);
|
|
}
|
|
elsif ($header eq 'Compress') {
|
|
return 0 unless got_compress_header($hdrs, $header, $value);
|
|
}
|
|
}
|
|
|
|
# avoid too-many-headers DOS attack
|
|
protocol_error("(too many headers)");
|
|
return 0;
|
|
}
|
|
|
|
# We'll run handle user unless we've been told not
|
|
# to process per-user config files. Otherwise
|
|
# we'll check and see if we need to try SQL
|
|
# lookups. If $opt{'user-config'} is true, we need to try
|
|
# their config file and then do the SQL lookup.
|
|
# If $opt{'user-config'} IS NOT true, we skip the conf file and
|
|
# only need to do the SQL lookup if $opt{'sql-config'} IS
|
|
# true. (I got that wrong the first time.)
|
|
#
|
|
sub got_user_header {
|
|
my ( $client, $header, $value ) = @_;
|
|
|
|
{ # no re "strict"; # since perl 5.21.8: Ranges of ASCII printables...
|
|
local $1;
|
|
if ( $value !~ /^([\x20-\xFF]*)$/ ) {
|
|
protocol_error("(User header contains control chars)");
|
|
return 0;
|
|
}
|
|
$current_user = $1;
|
|
}
|
|
|
|
if ( !$opt{'user-config'} ) {
|
|
if ( $opt{'sql-config'} ) {
|
|
unless ( handle_user_sql($current_user) ) {
|
|
service_unavailable_error("Error fetching user preferences via SQL");
|
|
return 0;
|
|
}
|
|
}
|
|
elsif ( $opt{'ldap-config'} ) {
|
|
handle_user_ldap($current_user);
|
|
}
|
|
elsif ( $opt{'virtual-config-dir'} ) {
|
|
handle_virtual_config_dir($current_user);
|
|
}
|
|
elsif ( $opt{'setuid-with-sql'} ) {
|
|
unless ( handle_user_setuid_with_sql($current_user) ) {
|
|
service_unavailable_error("Error fetching user preferences via SQL");
|
|
return 0;
|
|
}
|
|
$setuid_to_user = 1; #to benefit from any paranoia.
|
|
}
|
|
elsif ( $opt{'setuid-with-ldap'} ) {
|
|
handle_user_setuid_with_ldap($current_user);
|
|
$setuid_to_user = 1; # as above
|
|
}
|
|
else {
|
|
handle_user_setuid_basic($current_user);
|
|
}
|
|
}
|
|
else {
|
|
handle_user_setuid_basic($current_user);
|
|
if ( $opt{'sql-config'} ) {
|
|
unless ( handle_user_sql($current_user) ) {
|
|
service_unavailable_error("Error fetching user preferences via SQL");
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub got_clen_header {
|
|
my ( $hdrs, $header, $value ) = @_;
|
|
if ( $value !~ /^(\d*)$/ ) {
|
|
protocol_error("(Content-Length contains non-numeric bytes)");
|
|
return 0;
|
|
}
|
|
$hdrs->{expected_length} = $1;
|
|
return 1;
|
|
}
|
|
|
|
sub got_message_class_header {
|
|
my ($hdrs, $header, $value) = @_;
|
|
|
|
unless (lc($value) ne 'spam' || lc($value) ne 'ham') {
|
|
protocol_error("(Message-class header contains invalid class)");
|
|
return 0;
|
|
}
|
|
$hdrs->{message_class} = $value;
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub got_set_header {
|
|
my ($hdrs, $header, $value) = @_;
|
|
|
|
$hdrs->{set_local} = 0;
|
|
$hdrs->{set_remote} = 0;
|
|
|
|
if ($value =~ /local/i) {
|
|
$hdrs->{set_local} = 1;
|
|
}
|
|
|
|
if ($value =~ /remote/i) {
|
|
$hdrs->{set_remote} = 1;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub got_remove_header {
|
|
my ($hdrs, $header, $value) = @_;
|
|
|
|
$hdrs->{remove_local} = 0;
|
|
$hdrs->{remove_remote} = 0;
|
|
|
|
if ($value =~ /local/i) {
|
|
$hdrs->{remove_local} = 1;
|
|
}
|
|
|
|
if ($value =~ /remote/i) {
|
|
$hdrs->{remove_remote} = 1;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub got_compress_header {
|
|
my ($hdrs, $header, $value) = @_;
|
|
|
|
if ($value =~ /zlib/i) {
|
|
eval { require Compress::Zlib; };
|
|
if ($@) {
|
|
protocol_error("(compression not supported, Compress::Zlib not installed: $@)");
|
|
return 0;
|
|
}
|
|
$hdrs->{compress_zlib} = 1;
|
|
dbg("spamd: compress header received: $value");
|
|
}
|
|
else {
|
|
protocol_error("(compression type not supported: $value)");
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub protocol_error {
|
|
my ($err) = @_;
|
|
my $resp = "EX_PROTOCOL";
|
|
syswrite($client, "SPAMD/1.0 $resphash{$resp} Bad header line: $err\r\n");
|
|
warn("spamd: bad protocol: header error: $err\n");
|
|
}
|
|
|
|
sub service_unavailable_error {
|
|
my ($err) = @_;
|
|
my $resp = "EX_UNAVAILABLE";
|
|
syswrite($client,
|
|
"SPAMD/1.0 $resphash{$resp} Service Unavailable: $err\r\n");
|
|
warn("spamd: service unavailable: $err\n");
|
|
}
|
|
|
|
sub service_timeout {
|
|
my ($err) = @_;
|
|
my $resp = "EX_TIMEOUT";
|
|
print $client "SPAMD/1.0 $resphash{$resp} Timeout: $err\r\n";
|
|
warn("spamd: timeout: $err\n");
|
|
}
|
|
|
|
###########################################################################
|
|
|
|
sub seteuid_to_user {
|
|
return if (am_running_on_windows() || $> != 0);
|
|
|
|
my $suidto = $opt{'username'} || $opt{'default-user'};
|
|
my ($name, $pwd, $uid, $gid, $quota, $comment, $gcos, $suiddir, $etc) = getpwnam($suidto);
|
|
|
|
if (!defined $uid) {
|
|
die "spamd: seteuid_to_user (getpwnam) unable to find user: '$suidto'\n";
|
|
}
|
|
|
|
$) = (get_user_groups($uid)); # change eGID
|
|
$> = $uid; # change eUID
|
|
if ( !defined($uid) || ( $> != $uid and $> != ( $uid - 2**32 ) ) ) {
|
|
# make it fatal to avoid security breaches
|
|
die("spamd: fatal error: setuid to $suidto failed");
|
|
}
|
|
}
|
|
|
|
sub restore_euid {
|
|
return if (am_running_on_windows());
|
|
|
|
if (($> != $<) && ($> != ($< - 2**32))) {
|
|
$) = "$( $("; # change eGID
|
|
$> = $<; # change eUID
|
|
# check again; ensure the change happened
|
|
if ($> != $< && ($> != ( $< - 2**32))) {
|
|
# make it fatal to avoid security breaches
|
|
die("spamd: return setuid failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
sub handle_user_setuid_basic {
|
|
my $username = shift;
|
|
|
|
# If $opt{'username'} in use, then look up userinfo for that uid;
|
|
# otherwise use what was passed via $username
|
|
#
|
|
my $suidto = $username;
|
|
if ( $opt{'username'} ) {
|
|
$suidto = $opt{'username'};
|
|
}
|
|
my ($name, $pwd, $uid, $gid, $quota, $comment, $gcos, $suiddir, $etc) =
|
|
am_running_on_windows() ? ('nobody') : getpwnam($suidto);
|
|
|
|
if (!defined $uid) {
|
|
my $errmsg =
|
|
"spamd: handle_user (getpwnam) unable to find user: '$suidto'";
|
|
die "$errmsg\n" if $spamtest->{'paranoid'};
|
|
# if we are given a username, but can't look it up, maybe name
|
|
# services are down? let's break out here to allow them to get
|
|
# 'defaults' when we are not running paranoid
|
|
info($errmsg);
|
|
return 0;
|
|
}
|
|
|
|
if ($setuid_to_user) {
|
|
$) = (get_user_groups($uid)); # change eGID
|
|
$> = $uid; # change eUID
|
|
if ( !defined($uid) || ( $> != $uid and $> != ( $uid - 2**32 ) ) ) {
|
|
# make it fatal to avoid security breaches
|
|
die("spamd: fatal error: setuid to $suidto failed");
|
|
}
|
|
else {
|
|
info("spamd: setuid to $suidto succeeded");
|
|
}
|
|
}
|
|
|
|
my $userdir;
|
|
|
|
# if $opt{'user-config'} is in use, read user prefs from the remote
|
|
# username's home dir (if it exists): bug 5611
|
|
if ( $opt{'user-config'} ) {
|
|
my $prefsfrom = $username; # the one passed, NOT $opt{username}
|
|
|
|
if ($prefsfrom eq $suidto) {
|
|
$userdir = $suiddir; # reuse the already-looked-up info, tainted
|
|
} elsif ( $opt{'vpopmail'} ) {
|
|
#
|
|
# If vpopmail config enabled then set $userdir to virtual homedir
|
|
#
|
|
my $username_untainted;
|
|
$username_untainted =
|
|
untaint_var($username) if $username =~ /^[-:,.=+A-Za-z0-9_\@~]+\z/;
|
|
my $vpopdir = $suiddir; # This should work with common vpopmail setups
|
|
$userdir = `$vpopdir/bin/vuserinfo -d \Q$username_untainted\E`;
|
|
if ($? == 0) {
|
|
chomp($userdir);
|
|
} else {
|
|
$userdir = handle_user_vpopmail($username_untainted,$vpopdir);
|
|
}
|
|
} else {
|
|
$userdir = (getpwnam($prefsfrom))[7];
|
|
}
|
|
|
|
# we *still* die if this can't be found
|
|
if (!defined $userdir) {
|
|
my $errmsg =
|
|
"spamd: handle_user (userdir) unable to find user: '$prefsfrom'\n";
|
|
die $errmsg if $spamtest->{'paranoid'};
|
|
# if we are given a username, but can't look it up, maybe name
|
|
# services are down? let's break out here to allow them to get
|
|
# 'defaults' when we are not running paranoid
|
|
info($errmsg);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
# call this anyway, regardless of --user-config, so that
|
|
# signal_user_changed() is called
|
|
handle_user_set_user_prefs(untaint_var($userdir), $username);
|
|
}
|
|
|
|
sub handle_user_vpopmail {
|
|
#
|
|
# If vuserinfo failed $username could be an alias
|
|
# As the alias could be an alias itself we'll try to resolve it recursively
|
|
# Because we're mistrusting vpopmail we'll set off an alarm
|
|
#
|
|
my $username = shift;
|
|
my $vpopdir = shift;
|
|
my $userdir;
|
|
my $vpoptimeout = 5;
|
|
my $vptimer = Mail::SpamAssassin::Timeout->new({ secs => $vpoptimeout });
|
|
|
|
$vptimer->run(sub {
|
|
my $vpopusername = $username;
|
|
my @aliases = split(/\n/, `$vpopdir/bin/valias \Q$vpopusername\E`);
|
|
while (@aliases) {
|
|
my $vpopusername_tainted = shift(@aliases);
|
|
local $1;
|
|
if ($vpopusername_tainted =~ /-> &?(.+)$/) {
|
|
$vpopusername = untaint_var($1);
|
|
if ($vpopusername =~ s{^(/.+)/Maildir/$}{$1}) {
|
|
# this is the path to a real mailbox
|
|
$userdir = $vpopusername;
|
|
} elsif ($vpopusername !~ /^[#| \t]/ &&
|
|
$vpopusername =~ /^[^@ \t]+\@[^@ \t]+\s*$/) {
|
|
# this is a forward to another e-mail address
|
|
$vpopusername =~ s{^.+ -> (.+)}{$1};
|
|
$vpopusername_tainted = `$vpopdir/bin/vuserinfo -d \Q$vpopusername\E`;
|
|
if ($? == 0 && $vpopusername_tainted ne '') {
|
|
$userdir = untaint_var($vpopusername_tainted);
|
|
} else {
|
|
unshift(@aliases,
|
|
split(/\n/, `$vpopdir/bin/valias \Q$vpopusername\E`));
|
|
}
|
|
}
|
|
last if defined $userdir;
|
|
}
|
|
}
|
|
});
|
|
|
|
if ($vptimer->timed_out()) {
|
|
dbg("spamd: timed out resolving vpopmail user/alias '%s'", $username);
|
|
undef $userdir;
|
|
} elsif (!defined($userdir)) {
|
|
dbg("spamd: failed to resolve vpopmail user/alias '%s'", $username);
|
|
} else {
|
|
chomp($userdir);
|
|
}
|
|
return $userdir;
|
|
}
|
|
|
|
sub handle_user_set_user_prefs {
|
|
my ($dir, $username) = @_;
|
|
|
|
# don't do this if we weren't passed a directory
|
|
if ($dir) {
|
|
my $cf_file = $dir . "/.spamassassin/user_prefs";
|
|
create_default_cf_if_needed( $cf_file, $username, $dir );
|
|
$spamtest->read_scoreonly_config($cf_file);
|
|
}
|
|
|
|
# signal_user_changed will ignore undef user_dirs, so this is ok
|
|
$spamtest->signal_user_changed(
|
|
{
|
|
username => $username,
|
|
user_dir => $dir
|
|
}
|
|
);
|
|
|
|
return 1;
|
|
}
|
|
|
|
# Handle user configs without the necessity of having individual users or a
|
|
# SQL/LDAP database.
|
|
sub handle_virtual_config_dir {
|
|
my ($username) = @_;
|
|
|
|
my $dir = $opt{'virtual-config-dir'};
|
|
my $userdir;
|
|
my $prefsfile;
|
|
|
|
if ( defined $dir ) {
|
|
my $safename = $username;
|
|
$safename =~ s/[^-A-Za-z0-9\+_\.\,\@\=]/_/gs;
|
|
my $localpart = '';
|
|
my $domain = '';
|
|
if ( $safename =~ /^(.*)\@(.*)$/ ) { $localpart = $1; $domain = $2; }
|
|
|
|
# Do userdir lookup exim-style.
|
|
# If a config for the full address exists, use that one
|
|
# else look for a domain default
|
|
if ($dir=~/%x/) {
|
|
($userdir=$dir)=~s/%x/${safename}/g;
|
|
|
|
$prefsfile=$userdir.'/user_prefs';
|
|
if (-f $prefsfile) {
|
|
$dir = $userdir;
|
|
|
|
} else {
|
|
$dir =~ s/%x/${domain}/g;
|
|
|
|
$prefsfile = $dir.'/user_prefs';
|
|
$userdir = $dir;
|
|
}
|
|
|
|
# Use the normal escaping
|
|
} else {
|
|
$dir =~ s/\%u/${safename}/g;
|
|
$dir =~ s/\%l/${localpart}/g;
|
|
$dir =~ s/\%d/${domain}/g;
|
|
$dir =~ s/\%\%/\%/g;
|
|
|
|
$userdir = $dir;
|
|
$prefsfile = $dir . '/user_prefs';
|
|
}
|
|
|
|
# Log that the default configuration is being used for a user.
|
|
info("spamd: using default config for $username: $prefsfile");
|
|
}
|
|
|
|
if ( -f $prefsfile ) {
|
|
|
|
# Found a config, load it.
|
|
$spamtest->read_scoreonly_config($prefsfile);
|
|
}
|
|
|
|
# assume that $userdir will be a writable directory we can
|
|
# use for Bayes dbs etc.
|
|
$spamtest->signal_user_changed(
|
|
{
|
|
username => $username,
|
|
userstate_dir => $userdir,
|
|
user_dir => $userdir
|
|
}
|
|
);
|
|
return 1;
|
|
}
|
|
|
|
sub handle_user_sql {
|
|
my ($username) = @_;
|
|
|
|
unless ( $spamtest->load_scoreonly_sql($username) ) {
|
|
return 0;
|
|
}
|
|
$spamtest->signal_user_changed(
|
|
{
|
|
username => $username,
|
|
user_dir => undef
|
|
}
|
|
);
|
|
return 1;
|
|
}
|
|
|
|
sub handle_user_ldap {
|
|
my $username = shift;
|
|
dbg("ldap: entering handle_user_ldap($username)");
|
|
$spamtest->load_scoreonly_ldap($username);
|
|
$spamtest->signal_user_changed(
|
|
{
|
|
username => $username,
|
|
user_dir => undef
|
|
}
|
|
);
|
|
return 1;
|
|
}
|
|
|
|
sub handle_user_setuid_with_sql {
|
|
my $username = shift;
|
|
|
|
# Bug 6313: interestingly, if $username is not tainted than $pwd, $gcos and
|
|
# $etc end up tainted but other fields not; if $username _is_ tainted,
|
|
# getpwnam does not complain, but all returned fields are tainted (which
|
|
# makes sense, but is worth remembering)
|
|
#
|
|
my ($name, $pwd, $uid, $gid, $quota, $comment, $gcos, $dir, $etc) =
|
|
getpwnam(untaint_var($username));
|
|
|
|
if (!$spamtest->{'paranoid'} && !defined($uid)) {
|
|
# if we are given a username, but can't look it up, maybe name
|
|
# services are down? let's break out here to allow them to get
|
|
# 'defaults' when we are not running paranoid
|
|
info("spamd: handle_user (sql) unable to find user: $username");
|
|
return 0;
|
|
}
|
|
|
|
if ($setuid_to_user) {
|
|
$) = (get_user_groups($uid)); # change eGID
|
|
$> = $uid; # change eUID
|
|
if (!defined($uid) || ($> != $uid and $> != ($uid - 2**32))) {
|
|
# make it fatal to avoid security breaches
|
|
die("spamd: fatal error: setuid to $username failed");
|
|
}
|
|
else {
|
|
info("spamd: setuid to $username succeeded, reading scores from SQL");
|
|
}
|
|
}
|
|
|
|
my $spam_conf_dir = $dir . '/.spamassassin'; # needed for Bayes, etc.
|
|
|
|
if ( ($opt{'user-config'} || defined $opt{'home_dir_for_helpers'})
|
|
&& ! -d $spam_conf_dir ) {
|
|
if (mkdir $spam_conf_dir, 0700) {
|
|
info("spamd: created $spam_conf_dir for $username");
|
|
}
|
|
else {
|
|
info("spamd: failed to create $spam_conf_dir for $username");
|
|
}
|
|
}
|
|
|
|
unless ($spamtest->load_scoreonly_sql($username)) {
|
|
return 0;
|
|
}
|
|
|
|
$spamtest->signal_user_changed( { username => $username } );
|
|
return 1;
|
|
}
|
|
|
|
sub handle_user_setuid_with_ldap {
|
|
my $username = shift;
|
|
my ($name, $pwd, $uid, $gid, $quota, $comment, $gcos, $dir, $etc) =
|
|
getpwnam($username);
|
|
|
|
if (!$spamtest->{'paranoid'} && !defined($uid)) {
|
|
# if we are given a username, but can't look it up, maybe name
|
|
# services are down? let's break out here to allow them to get
|
|
# 'defaults' when we are not running paranoid
|
|
info("spamd: handle_user (ldap) unable to find user: $username");
|
|
return 0;
|
|
}
|
|
|
|
if ($setuid_to_user) {
|
|
$) = (get_user_groups($uid)); # change eGID
|
|
$> = $uid; # change eUID
|
|
if (!defined($uid) || ($> != $uid and $> != ($uid - 2**32))) {
|
|
# make it fatal to avoid security breaches
|
|
die("spamd: fatal error: setuid to $username failed");
|
|
}
|
|
else {
|
|
info("spamd: setuid to $username succeeded, reading scores from LDAP");
|
|
}
|
|
}
|
|
|
|
my $spam_conf_dir = $dir . '/.spamassassin'; # needed for Bayes, etc.
|
|
if (! -d $spam_conf_dir) {
|
|
if (mkdir $spam_conf_dir, 0700) {
|
|
info("spamd: created $spam_conf_dir for $username");
|
|
}
|
|
else {
|
|
info("spamd: failed to create $spam_conf_dir for $username");
|
|
}
|
|
}
|
|
|
|
$spamtest->load_scoreonly_ldap($username);
|
|
|
|
$spamtest->signal_user_changed( { username => $username } );
|
|
return 1;
|
|
}
|
|
|
|
sub create_default_cf_if_needed {
|
|
my ( $cf_file, $username, $userdir ) = @_;
|
|
|
|
# Parse user scores, creating default .cf if needed:
|
|
if ( !-r $cf_file && !$spamtest->{'dont_copy_prefs'} ) {
|
|
info("spamd: creating default_prefs: $cf_file");
|
|
|
|
# If vpopmail config enabled then pass virtual homedir onto
|
|
# create_default_prefs via $userdir
|
|
$spamtest->create_default_prefs( $cf_file, $username, $userdir );
|
|
|
|
if (! -r $cf_file) {
|
|
info("spamd: failed to create readable default_prefs: $cf_file");
|
|
}
|
|
}
|
|
}
|
|
|
|
# sig handlers: parent process
|
|
sub setup_parent_sig_handlers {
|
|
$SIG{HUP} = \&restart_handler;
|
|
$SIG{CHLD} = \&child_handler;
|
|
$SIG{INT} = \&kill_handler;
|
|
$SIG{TERM} = \&kill_handler;
|
|
$SIG{PIPE} = 'IGNORE';
|
|
}
|
|
|
|
# sig handlers: child processes
|
|
sub setup_child_sig_handlers {
|
|
# note: all the signals changed in setup_parent_sig_handlers() must
|
|
# be reset to appropriate values here!
|
|
my $h = 'DEFAULT';
|
|
if (am_running_on_windows()) {
|
|
# on win32 the parent never receives SIGCHLD
|
|
$h = sub { my($sig) = @_;
|
|
info("spamd: child got SIG$sig, exiting");
|
|
kill QUIT => 0;
|
|
exit 0;
|
|
};
|
|
}
|
|
$SIG{$_} = $h foreach qw(HUP INT TERM CHLD);
|
|
$SIG{PIPE} = 'IGNORE';
|
|
}
|
|
|
|
sub kill_handler {
|
|
my ($sig) = @_;
|
|
info("spamd: server killed by SIG$sig, shutting down");
|
|
|
|
for my $socket_info (@listen_sockets) {
|
|
next if !$socket_info;
|
|
|
|
my $socket = $socket_info->{socket};
|
|
$socket->close if $socket; # ignoring status
|
|
|
|
my $path = $socket_info->{path};
|
|
if (defined $path) { # unlink a UNIX domain socket
|
|
unlink($path) or warn "spamd: cannot unlink $path: $!\n";
|
|
}
|
|
}
|
|
|
|
if (defined($opt{'pidfile'})) {
|
|
unlink($opt{'pidfile'})
|
|
or warn "spamd: cannot unlink $opt{'pidfile'}: $!\n";
|
|
}
|
|
|
|
$SIG{CHLD} = 'DEFAULT'; # we're going to kill our children
|
|
if ($scaling) {
|
|
$scaling->set_exiting_flag(); # don't start new ones
|
|
}
|
|
my $killsig = am_running_on_windows() ? 'KILL' : 'INT';
|
|
foreach my $pid (keys %children) {
|
|
kill($killsig, $pid)
|
|
or info("spamd: cannot send SIG$killsig to child process [$pid]: $!");
|
|
}
|
|
exit 0;
|
|
}
|
|
|
|
# takes care of dead children
|
|
sub child_handler {
|
|
my ($sig) = @_;
|
|
|
|
# do NOT call syslog here unless the child's pid is in our list of known
|
|
# children. This is due to syslog-ng brokenness -- bugs 3625, 4237;
|
|
# see also bug 6745.
|
|
|
|
# clean up any children which have exited
|
|
for (;;) {
|
|
# waitpid returns a pid of the deceased process, or -1 if there is no
|
|
# such child process. On some systems, a value of 0 indicates that there
|
|
# are processes still running. Note that Windows uses negative pids for
|
|
# child processes - bug 6376, bug 6356.
|
|
#
|
|
my $pid = waitpid(-1, WNOHANG);
|
|
last if !$pid || $pid == -1;
|
|
push(@children_exited, [$pid, $?, $sig, time]);
|
|
}
|
|
|
|
$SIG{CHLD} = \&child_handler; # reset as necessary, should be at end
|
|
}
|
|
|
|
# takes care of dead children, as noted by a child_handler()
|
|
# called in a main program flow (not from a signal handler)
|
|
#
|
|
sub child_cleaner {
|
|
while (@children_exited) {
|
|
my $tuple = shift(@children_exited);
|
|
next if !$tuple; # just in case
|
|
my($pid, $child_stat, $sig, $timestamp) = @$tuple;
|
|
|
|
# ignore this child if we didn't realise we'd forked it. bug 4237
|
|
next if !defined $children{$pid};
|
|
|
|
# remove them from our child listing
|
|
delete $children{$pid};
|
|
|
|
if ($scaling) {
|
|
$scaling->child_exited($pid);
|
|
} else {
|
|
my $sock = $backchannel->get_socket_for_child($pid);
|
|
if ($sock) { $sock->close(); }
|
|
}
|
|
info("spamd: handled cleanup of child pid [%s]%s: %s",
|
|
$pid, (defined $sig ? " due to SIG$sig" : ""),
|
|
exit_status_str($child_stat,0));
|
|
}
|
|
}
|
|
|
|
sub restart_handler {
|
|
my ($sig) = @_;
|
|
info("spamd: server hit by SIG$sig, restarting");
|
|
|
|
$SIG{CHLD} = 'DEFAULT'; # we're going to kill our children
|
|
if ($scaling) {
|
|
$scaling->set_exiting_flag(); # don't start new ones
|
|
}
|
|
|
|
foreach (keys %children) {
|
|
kill 'INT' => $_;
|
|
my $pid = waitpid($_, 0);
|
|
my $child_stat = $pid > 0 ? $? : undef;
|
|
if ($scaling) {
|
|
$scaling->child_exited($pid);
|
|
}
|
|
info("spamd: child [%s] killed successfully: %s",
|
|
$pid, exit_status_str($child_stat,0));
|
|
}
|
|
%children = ();
|
|
|
|
for my $socket_info (@listen_sockets) {
|
|
next if !$socket_info;
|
|
my $socket = $socket_info->{socket};
|
|
next if !$socket;
|
|
my $socket_specs = $socket_info->{specs};
|
|
$socket->shutdown(2) if !$socket->eof;
|
|
$socket->close;
|
|
if ($socket->isa('IO::Socket::UNIX') && defined $socket_specs) {
|
|
unlink($socket_specs)
|
|
or warn "spamd: cannot unlink $socket_specs: $!\n";
|
|
}
|
|
info("spamd: server socket closed, type %s", ref $socket);
|
|
}
|
|
|
|
$got_sighup = 1;
|
|
}
|
|
|
|
sub backtrace_handler {
|
|
Carp::cluck("spamd: caught SIGUSR2 - dumping backtrace. ".
|
|
"most recent message: $current_msgid\n");
|
|
}
|
|
|
|
my $serverstarted = 0;
|
|
|
|
sub serverstarted {
|
|
$serverstarted = 1;
|
|
}
|
|
|
|
sub daemonize {
|
|
# bug 8036 - ensure ps legacy name shows up as spamd even if command line call was perl path_to_spamd
|
|
$0 = 'spamd' unless would_log("dbg");
|
|
|
|
# be a nice daemon and chdir to the root so we don't block any
|
|
# unmount attempts
|
|
chdir '/' or die "spamd: cannot chdir to /: $!\n";
|
|
|
|
# Redirect in and out to the bit bucket
|
|
open STDIN, "</dev/null" or die "spamd: cannot read from /dev/null: $!\n";
|
|
open STDOUT, ">/dev/null" or die "spamd: cannot write to /dev/null: $!\n";
|
|
|
|
# Remove the stderr logger
|
|
Mail::SpamAssassin::Logger::remove('stderr');
|
|
|
|
# Here we go...
|
|
$SIG{USR1} = \&serverstarted;
|
|
defined( my $pid = fork ) or die "spamd: cannot fork: $!\n";
|
|
if ($pid) {
|
|
my $child_stat;
|
|
# Bug 6191, Bug 6258: takes almost two minutes on a slow machine
|
|
# for a forked child process to report back, bump limit to 180 seconds
|
|
for (my $retry=180, my $waited=0;
|
|
$retry > 0 && !$serverstarted && $waited != $pid;
|
|
$retry--)
|
|
{
|
|
warn("waitpid failed: $waited $!") if $waited;
|
|
sleep 1;
|
|
$waited = waitpid($pid, WNOHANG);
|
|
$child_stat = $? if $waited > 0;
|
|
}
|
|
die sprintf("child process [%s] exited or timed out ".
|
|
"without signaling production of a PID file: %s",
|
|
$pid, exit_status_str($child_stat,0)) unless $serverstarted;
|
|
exit;
|
|
}
|
|
delete $SIG{USR1};
|
|
setsid or die "spamd: cannot start new session: $!\n";
|
|
|
|
# Now we can redirect the errors, too.
|
|
open STDERR, '>&STDOUT' or die "spamd: cannot duplicate stdout: $!\n";
|
|
|
|
dbg("spamd: successfully daemonized");
|
|
}
|
|
|
|
sub set_allowed_ip {
|
|
foreach (@_) {
|
|
my $ip = $_;
|
|
local($1,$2);
|
|
# strip optional square brackets
|
|
$ip =~ s{^ \[ (.*) \] \z}{$1}xs
|
|
|| $ip =~ s{^ \[ (.*) \] ( / \d+ ) \z}{$1$2}xs;
|
|
# dbg("spamd: set_allowed_ip %s", $ip);
|
|
$allowed_nets->add_cidr($ip)
|
|
or die "spamd: aborting due to add_cidr error\n";
|
|
}
|
|
}
|
|
|
|
sub ip_is_allowed {
|
|
$allowed_nets->contains_ip(@_);
|
|
}
|
|
|
|
sub preload_modules_with_tmp_homedir {
|
|
|
|
# set $ENV{HOME} in a temp directory while we compile and preload everything.
|
|
my $tmphome = secure_tmpdir();
|
|
|
|
# If TMPDIR isn't set, File::Spec->tmpdir() called by secure_tmpdir() may set it to undefined.
|
|
# that then breaks other things ...
|
|
# If this is really necessary shouldn't secure_tmpdir() be doing it?
|
|
delete $ENV{'TMPDIR'} if ( !defined $ENV{'TMPDIR'} );
|
|
|
|
my $tmpsadir = File::Spec->catdir( $tmphome, ".spamassassin" );
|
|
|
|
dbg("spamd: Preloading modules with HOME=$tmphome");
|
|
|
|
if (!-d $tmphome) {
|
|
die "spamd: cannot create temp directory $tmphome: $!";
|
|
}
|
|
|
|
# bug 5379: spamd won't start if the temp preloading dir exists; check if exists and remove it
|
|
# This check should be unnecessary now that $tmphome created using File::Temp, but leave it just in case
|
|
if (-d $tmpsadir) {
|
|
rmdir( $tmpsadir ) or die "spamd: $tmpsadir not empty: $!";
|
|
}
|
|
mkdir( $tmpsadir, 0700 ) or die "spamd: cannot create $tmpsadir: $!";
|
|
$ENV{HOME} = $tmphome;
|
|
|
|
$spamtest->compile_now(0,1); # ensure all modules etc. are loaded
|
|
$/ = "\n"; # argh, Razor resets this! Bad Razor!
|
|
|
|
# now clean up the stuff we just created, and make us taint-safe
|
|
delete $ENV{HOME};
|
|
|
|
# bug 2015, bug 2223: rmpath() is not taint safe, so we've got to implement
|
|
# our own poor man's rmpath. If it fails, we report only the first error.
|
|
my $err;
|
|
foreach my $d ( ( $tmpsadir, $tmphome ) ) {
|
|
opendir( TMPDIR, $d ) or $err ||= "open $d: $!";
|
|
unless ($err) {
|
|
foreach my $f ( File::Spec->no_upwards( readdir(TMPDIR) ) ) {
|
|
$f = untaint_file_path( File::Spec->catfile( $d, $f ) );
|
|
unlink($f) or $err ||= "remove $f: $!";
|
|
}
|
|
closedir(TMPDIR) or $err ||= "close $d: $!";
|
|
}
|
|
rmdir($d) or $err ||= "remove $d: $!";
|
|
}
|
|
|
|
# If the dir still exists, log a warning.
|
|
if ( -d $tmphome ) {
|
|
$err ||= "do something: $!";
|
|
warn "spamd: failed to remove $tmphome: could not $err\n";
|
|
}
|
|
}
|
|
|
|
# Keep calling syswrite until the entire buffer is written out
|
|
# Retry if EAGAIN/EWOULDBLOCK or when partial buffer is written
|
|
# Limit the number of retries to keep the execution time bounded
|
|
sub syswrite_full_buffer {
|
|
my ($sock, $buf, $numretries) = @_;
|
|
$numretries ||= 10; # default 10 retries
|
|
my $length = length($buf);
|
|
my $written = 0;
|
|
my $try = 0;
|
|
|
|
while (($try < $numretries) && ($length > $written)) {
|
|
my $nbytes = syswrite($sock, $buf, $length - $written, $written);
|
|
if (!defined $nbytes) {
|
|
unless ((exists &Errno::EAGAIN && $! == &Errno::EAGAIN)
|
|
|| (exists &Errno::EWOULDBLOCK && $! == &Errno::EWOULDBLOCK))
|
|
{
|
|
# an error that wasn't non-blocking I/O-related. that's serious
|
|
return;
|
|
}
|
|
# errcode says to try again
|
|
}
|
|
else {
|
|
|
|
if ($nbytes == 0) {
|
|
return $written; # return early if no error but nothing was written
|
|
}
|
|
|
|
$written += $nbytes;
|
|
}
|
|
$try++;
|
|
}
|
|
|
|
return $written; # it's complete, we can return
|
|
}
|
|
|
|
sub map_server_sockets {
|
|
|
|
$server_select_mask = '';
|
|
for my $socket_info (@listen_sockets) {
|
|
next if !$socket_info;
|
|
my $fd = $socket_info->{fd};
|
|
vec($server_select_mask, $fd, 1) = 1 if defined $fd;
|
|
}
|
|
dbg("spamd: server listen sockets fd bit field: %s",
|
|
unpack('b*', $server_select_mask));
|
|
|
|
my $back_selector = $server_select_mask;
|
|
$backchannel->set_selector(\$back_selector);
|
|
}
|
|
|
|
# do this in advance, since we want to minimize work when SIGHUP
|
|
# is received
|
|
my $perl_from_hashbang_line;
|
|
sub prepare_for_sighup_restart {
|
|
@ORIG_INC_OPTS =
|
|
map {
|
|
my $path = untaint_var($_);
|
|
(File::Spec->file_name_is_absolute($path) and (-d $path))?("-I", $path):()
|
|
}
|
|
@ORIG_INC_OPTS;
|
|
}
|
|
|
|
sub do_sighup_restart {
|
|
if (defined($opt{'pidfile'})) {
|
|
unlink($opt{'pidfile'}) || warn "spamd: cannot unlink $opt{'pidfile'}: $!\n";
|
|
}
|
|
|
|
# leave Client fds active, and do not kill children; they can still
|
|
# service clients until they exit. But restart the listener anyway.
|
|
# And close the logfile, so the new instance can reopen it.
|
|
Mail::SpamAssassin::Logger::close_log();
|
|
chdir($ORIG_CWD)
|
|
or die "spamd: restart failed: chdir failed: ${ORIG_CWD}: $!\n";
|
|
|
|
# Close GeoDB, Geo::IP leaks fds on restart (Bug 8127)
|
|
delete $spamtest->{geodb};
|
|
|
|
# ensure we re-run spamd using the right perl interpreter, and
|
|
# with the right switches (taint mode and warnings) (bug 5255)
|
|
# Also need -I options (bug 8030) because there is no way
|
|
# to determine if everything in @INC came from this perl's defaults
|
|
my $perl = untaint_var($^X);
|
|
my @execs = ( $perl, "-T", "-w", @ORIG_INC_OPTS, $ORIG_ARG0, @ORIG_ARGV );
|
|
|
|
# bug 8030 - removed code that in some cases just exec'd the script
|
|
# Can't ever exec the script in case the perl -I options are necessary
|
|
|
|
warn "spamd: restarting using '" . join (' ', @execs) . "'\n";
|
|
exec @execs;
|
|
|
|
# should not get past that...
|
|
die "spamd: restart failed: exec failed: " . join (' ', @execs) . ": $!\n";
|
|
}
|
|
|
|
__DATA__
|
|
|
|
=head1 NAME
|
|
|
|
spamd - daemonized version of spamassassin
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
spamd [options]
|
|
|
|
Options:
|
|
|
|
-l, --allow-tell Allow learning/reporting
|
|
-c, --create-prefs Create user preferences files
|
|
-C path, --configpath=path Path for default config files
|
|
--siteconfigpath=path Path for site configs
|
|
--cf='config line' Additional line of configuration
|
|
--pre='config line' Additional line of ".pre" (prepended to configuration)
|
|
-d, --daemonize Daemonize
|
|
-h, --help Print usage message
|
|
-i [ip_or_name[:port]], --listen=[ip_or_name[:port]] Listen on IP addr and port
|
|
-p port, --port=port Listen on specified port, may be overridden by -i
|
|
-4, --ipv4-only, --ipv4 Use IPv4 where applicable, disables IPv6
|
|
-6 Use IPv6 where applicable, disables IPv4
|
|
-A host,..., --allowed-ips=..,.. Restrict to IP addresses which can connect
|
|
-m num, --max-children=num Allow maximum num children
|
|
--min-children=num Allow minimum num children
|
|
--min-spare=num Lower limit for number of spare children
|
|
--max-spare=num Upper limit for number of spare children
|
|
--max-conn-per-child=num Maximum connections accepted by child
|
|
before it is respawned
|
|
--round-robin Use traditional prefork algorithm
|
|
--timeout-tcp=secs Connection timeout for client headers
|
|
--timeout-child=secs Connection timeout for message checks
|
|
-q, --sql-config Enable SQL config (needs -x)
|
|
-Q, --setuid-with-sql Enable SQL config (needs -x,
|
|
enables use of -H)
|
|
--ldap-config Enable LDAP config (needs -x)
|
|
--setuid-with-ldap Enable LDAP config (needs -x,
|
|
enables use of -H)
|
|
--virtual-config-dir=dir Enable pattern based Virtual configs
|
|
(needs -x)
|
|
-r pidfile, --pidfile Write the process id to pidfile
|
|
-s facility, --syslog=facility Specify the syslog facility
|
|
--syslog-socket=type How to connect to syslogd
|
|
--log-timestamp-fmt=fmt strftime(3) format for timestamps, may be
|
|
empty to disable timestamps, or 'default'
|
|
-u username, --username=username Run as username
|
|
-g groupname, --groupname=groupname Run as groupname
|
|
-v, --vpopmail Enable vpopmail config
|
|
-x, --nouser-config Disable user config files
|
|
-U username, --default-user=username Fall back to this username if spamc user
|
|
is not found (default: nobody)
|
|
-D, --debug[=areas] Print debugging messages (for areas)
|
|
-L, --local Use local tests only (no DNS)
|
|
-P, --paranoid Die upon user errors
|
|
-H [dir], --helper-home-dir[=dir] Specify a different HOME directory
|
|
--ssl Enable SSL on TCP connections
|
|
--ssl-verify Request a client certificate and verify it
|
|
--ssl-ca-file cafile Certificate Authority certificate file
|
|
--ssl-ca-path capath Certificate Authority directory
|
|
--ssl-port port Override --port setting for SSL connections
|
|
--server-key keyfile Specify an SSL keyfile
|
|
--server-cert certfile Specify an SSL certificate
|
|
--socketpath=path Listen on a given UNIX domain socket
|
|
--socketowner=name Set UNIX domain socket file's owner
|
|
--socketgroup=name Set UNIX domain socket file's group
|
|
--socketmode=mode Set UNIX domain socket file's mode
|
|
--timing Enable timing and logging
|
|
-V, --version Print version and exit
|
|
|
|
The --listen option (or -i) may be specified multiple times, its syntax
|
|
is: [ ssl: ] [ host-name-or-IP-address ] [ : port ] or an absolute path
|
|
(filename) of a Unix socket. If port is omitted it defaults to --port or
|
|
to 783. Option --ssl implies a prefix 'ssl:'. An IPv6 address should be
|
|
enclosed in square brackets, e.g. [::1]:783, an IPv4 address may be but
|
|
need not be enclosed in square brackets. An asterisk '*' in place of a
|
|
hostname implies an unspecified address, ('0.0.0.0' or '::'), i.e. it
|
|
binds to all interfaces. An empty option value implies '*'. A default
|
|
is '--listen localhost', which binds to a loopback interface only.
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
The purpose of this program is to provide a daemonized version of the
|
|
spamassassin executable. The goal is improving throughput performance for
|
|
automated mail checking.
|
|
|
|
This is intended to be used alongside C<spamc>, a fast, low-overhead C client
|
|
program.
|
|
|
|
See the README file in the C<spamd> directory of the SpamAssassin distribution
|
|
for more details.
|
|
|
|
Note: Although C<spamd> will check per-user config files for every message, any
|
|
changes to the system-wide config files will require either restarting spamd
|
|
or forcing it to reload itself via B<SIGHUP> for the changes to take effect.
|
|
|
|
Note: If C<spamd> receives a B<SIGHUP>, it internally reloads itself, which
|
|
means that it will change its pid and might not restart at all if its
|
|
environment changed (ie. if it can't change back into its own directory). If
|
|
you plan to use B<SIGHUP>, you should always start C<spamd> with the B<-r>
|
|
switch to know its current pid.
|
|
|
|
=head1 OPTIONS
|
|
|
|
Options of the long form can be shortened as long as they remain
|
|
unambiguous. (i.e. B<--dae> can be used instead of B<--daemonize>)
|
|
Also, boolean options (like B<--user-config>) can be negated by
|
|
adding I<no> (B<--nouser-config>), however, this is usually unnecessary.
|
|
|
|
=over 4
|
|
|
|
=item B<-l>, B<--allow-tell>
|
|
|
|
Allow learning and forgetting (to a local Bayes database), reporting
|
|
and revoking (to a remote database) by spamd. The client issues a TELL
|
|
command to tell what type of message is being processed and whether
|
|
local (learn/forget) or remote (report/revoke) databases should be
|
|
updated.
|
|
|
|
Note that spamd always trusts the username passed in so clients could
|
|
maliciously learn messages for other users. (This is not usually a concern
|
|
with an SQL Bayes store as users will typically have read-write access
|
|
directly to the database, and can also use C<sa-learn> with the B<-u> option
|
|
to achieve the same result.)
|
|
|
|
=item B<-c>, B<--create-prefs>
|
|
|
|
Create user preferences files if they don't exist (default: don't).
|
|
|
|
=item B<-C> I<path>, B<--configpath>=I<path>
|
|
|
|
Use the specified path for locating the distributed configuration files.
|
|
Ignore the default directories (usually C</usr/share/spamassassin> or similar).
|
|
|
|
=item B<--siteconfigpath>=I<path>
|
|
|
|
Use the specified path for locating site-specific configuration files. Ignore
|
|
the default directories (usually C</etc/mail/spamassassin> or similar).
|
|
|
|
=item B<--cf='config line'>
|
|
|
|
Add additional lines of configuration directly from the command-line, parsed
|
|
after the configuration files are read. Multiple B<--cf> arguments can be
|
|
used, and each will be considered a separate line of configuration.
|
|
|
|
=item B<--pre='config line'>
|
|
|
|
Add additional lines of .pre configuration directly from the command-line,
|
|
parsed before the configuration files are read. Multiple B<--pre> arguments
|
|
can be used, and each will be considered a separate line of configuration.
|
|
|
|
=item B<-d>, B<--daemonize>
|
|
|
|
Detach from starting process and run in background (daemonize).
|
|
|
|
=item B<-h>, B<--help>
|
|
|
|
Print a brief help message, then exit without further action.
|
|
|
|
=item B<-V>, B<--version>
|
|
|
|
Print version information, then exit without further action.
|
|
|
|
=item B<-i> [I<ipaddress>[:E<lt>portE<gt>]], B<--listen>[=I<ipaddress>[:E<lt>portE<gt>]]
|
|
|
|
Additional alias names for this option are --listen-ip and --ip-address.
|
|
Tells spamd to listen on the specified IP address, defaults to a loopback
|
|
interface, i.e. C<--listen localhost>). If no value is specified after the
|
|
switch, or if an asterisk '*' stands in place of an E<lt>ipaddressE<gt>, spamd will
|
|
listen on all interfaces - this is equivalent to address '0.0.0.0' for IPv4
|
|
and to '::' for IPv6. You can also use a valid hostname which will make spamd
|
|
listen on all addresses that a name resolves to. The option may be specified
|
|
multiple times. See also options -4 and -6 for restricting address family
|
|
to IPv4 or to IPv6. If a port is specified it overrides for this socket the
|
|
global --port (and --ssl-port) setting. An IPv6 addresses should be enclosed
|
|
in square brackets, e.g. [::1]:783. For compatibility square brackets on an
|
|
IPv6 address may be omitted if a port number specification is also omitted.
|
|
|
|
=item B<-p> I<port>, B<--port>=I<port>
|
|
|
|
Optionally specifies the port number for the server to listen on (default: 783).
|
|
|
|
If the B<--ssl> switch is used, and B<--ssl-port> is not supplied, then this
|
|
port will be used to accept SSL connections instead of unencrypted connections.
|
|
If the B<--ssl> switch is used, and B<--ssl-port> is set, then unencrypted
|
|
connections will be accepted on the B<--port> at the same time as encrypted
|
|
connections are accepted at B<--ssl-port>.
|
|
|
|
=item B<-q>, B<--sql-config>
|
|
|
|
Turn on SQL lookups even when per-user config files have been disabled
|
|
with B<-x>. this is useful for spamd hosts which don't have user's
|
|
home directories but do want to load user preferences from an SQL
|
|
database.
|
|
|
|
If your spamc client does not support sending the C<User:> header,
|
|
like C<exiscan>, then the SQL username used will always be B<nobody>.
|
|
|
|
This inhibits the setuid() behavior, so the C<-u> option is
|
|
required. If you want the setuid() behaviour, use C<-Q> or
|
|
C<--setuid-with-sql> instead.
|
|
|
|
=item B<--ldap-config>
|
|
|
|
Turn on LDAP lookups. This is completely analog to C<--sql-config>,
|
|
only it is using an LDAP server.
|
|
|
|
Like C<--sql-config>, this disables the setuid behavior, and requires
|
|
C<-u>. If you want it, use C<--setuid-with-ldap> instead.
|
|
|
|
=item B<-Q>, B<--setuid-with-sql>
|
|
|
|
Turn on SQL lookups even when per-user config files have been disabled
|
|
with B<-x> and also setuid to the user. This is useful for spamd hosts
|
|
which want to load user preferences from an SQL database but also wish to
|
|
support the use of B<-H> (Helper home directories.)
|
|
|
|
=item B<--setuid-with-ldap>
|
|
|
|
Turn on LDAP lookups even when per-user config files have been disabled
|
|
with B<-x> and also setuid to the user. This is again completely analog
|
|
to C<--setuid-with-sql>, only it is using an LDAP server.
|
|
|
|
=item B<--virtual-config-dir>=I<pattern>
|
|
|
|
This option specifies where per-user preferences can be found for virtual
|
|
users, for the B<-x> switch. The I<pattern> is used as a base pattern for the
|
|
directory name. Any of the following escapes can be used:
|
|
|
|
=over 4
|
|
|
|
=item %u -- replaced with the full name of the current user, as sent by spamc.
|
|
|
|
=item %l -- replaced with the 'local part' of the current username. In other
|
|
words, if the username is an email address, this is the part before the C<@>
|
|
sign.
|
|
|
|
=item %d -- replaced with the 'domain' of the current username. In other
|
|
words, if the username is an email address, this is the part after the C<@>
|
|
sign.
|
|
|
|
=item %x -- replaced with the full name of the current user, as sent by spamc.
|
|
If the resulting config directory does not exist, replace with the domain part
|
|
to use a domain-wide default.
|
|
|
|
=item %% -- replaced with a single percent sign (%).
|
|
|
|
=back
|
|
|
|
So for example, if C</vhome/users/%u/spamassassin> is specified, and spamc
|
|
sends a virtual username of C<jm@example.com>, the directory
|
|
C</vhome/users/jm@example.com/spamassassin> will be used.
|
|
|
|
The set of characters allowed in the virtual username for this path are
|
|
restricted to:
|
|
|
|
A-Z a-z 0-9 - + _ . , @ =
|
|
|
|
All others will be replaced by underscores (C<_>).
|
|
|
|
This path must be a writable directory. It will be created if it does not
|
|
already exist. If a file called B<user_prefs> exists in this directory (note:
|
|
B<not> in a C<.spamassassin> subdirectory!), it will be loaded as the user's
|
|
preferences. The Bayes databases for that user will be stored in this directory.
|
|
|
|
Note that this B<requires> that B<-x> is used, and cannot be combined with
|
|
SQL- or LDAP-based configuration.
|
|
|
|
The pattern B<must> expand to an absolute directory when spamd is running
|
|
daemonized (B<-d>).
|
|
|
|
Currently, use of this without B<-u> is not supported. This inhibits setuid.
|
|
|
|
=item B<-r> I<pidfile>, B<--pidfile>=I<pidfile>
|
|
|
|
Write the process ID of the spamd parent to the file specified by I<pidfile>.
|
|
The file will be unlinked when the parent exits. Note that when running
|
|
with the B<-u> option, the file must be writable by that user.
|
|
|
|
=item B<-v>, B<--vpopmail>
|
|
|
|
Enable vpopmail config. If specified with B<-u> set to the vpopmail user,
|
|
this allows spamd to lookup/create user_prefs in the vpopmail user's own
|
|
maildir. This option is useful for vpopmail virtual users who do not have an
|
|
entry in the system /etc/passwd file.
|
|
|
|
Currently, use of this without B<-u> is not supported. This inhibits setuid.
|
|
|
|
=item B<-s> I<facility>, B<--syslog>=I<facility>
|
|
|
|
Specify the syslog facility to use (default: mail). If C<stderr> is specified,
|
|
output will be written to stderr. (This is useful if you're running C<spamd>
|
|
under the C<daemontools> package.) With a I<facility> of C<file>, all output
|
|
goes to spamd.log. I<facility> is interpreted as a file name to log to if it
|
|
contains any characters except a-z and 0-9. C<null> disables logging completely
|
|
(used internally).
|
|
|
|
Examples:
|
|
|
|
spamd -s mail # use syslog, facility mail (default)
|
|
spamd -s ./mail # log to file ./mail
|
|
spamd -s stderr 2>/dev/null # log to stderr, throw messages away
|
|
spamd -s null # the same as above
|
|
spamd -s file # log to file ./spamd.log
|
|
spamd -s /var/log/spamd.log # log to file /var/log/spamd.log
|
|
|
|
If logging to a file is enabled and that log file is rotated, the spamd server
|
|
must be restarted with a SIGHUP. (If the log file is just truncated, this is
|
|
not needed but still recommended.)
|
|
|
|
Note that logging to a file does not use locking, so you cannot intermix
|
|
logging from spamd and other processes into the same file. If you want
|
|
to mix logging like this, use syslog instead.
|
|
|
|
If you use syslog logging, it is essential to send a SIGHUP to the spamd daemon
|
|
when you restart the syslogd daemon. (This is due to a shortcoming in Perl's
|
|
syslog handling, where the disappearance of the connection to the syslogd is
|
|
considered a fatal error.)
|
|
|
|
=item B<--syslog-socket>=I<type>
|
|
|
|
Specify how spamd should send messages to syslogd. The I<type> can be any
|
|
of the socket types or logging mechanisms as accepted by the subroutine
|
|
Sys::Syslog::setlogsock(). Depending on a version of Sys::Syslog and on the
|
|
underlying operating system, one of the following values (or their subset) can
|
|
be used: C<native>, C<eventlog>, C<tcp>, C<udp>, C<inet>, C<unix>, C<stream>,
|
|
C<pipe>, or C<console>. The value C<eventlog> is specific to Win32 events
|
|
logger and requires a perl module Win32::EventLog to be installed.
|
|
For more information please consult the Sys::Syslog documentation.
|
|
|
|
A historical setting --syslog-socket=none is mapped to --syslog=stderr.
|
|
|
|
A default for Windows platforms is C<none>, otherwise the default is
|
|
to try C<unix> first, falling back to C<inet> if perl detects errors
|
|
in its C<unix> support.
|
|
|
|
Some platforms, or versions of perl, are shipped with old or dysfunctional
|
|
versions of the B<Sys::Syslog> module which do not support some socket types,
|
|
so you may need to set this option explicitly. If you get error messages
|
|
regarding B<__PATH_LOG> or similar spamd, try changing this setting.
|
|
|
|
The socket types C<file> is used internally and should not be specified.
|
|
Use the C<-s> switch instead.
|
|
|
|
=item B<--log-timestamp-fmt>=I<format>
|
|
|
|
The --log-timestamp-fmt option can provide a POSIX strftime(3) format for
|
|
timestamps included in each logged message. Each logger (stderr, file,
|
|
syslog) has its own default value for a timestamp format, which applies when
|
|
--log-timestamp-fmt option is not given, or with --log-timestamp-fmt=default .
|
|
Timestamps can be turned off by specifying an empty string with this
|
|
option, e.g. --log-timestamp-fmt='' or just --log-timestamp-fmt= .
|
|
Typical use: --log-timestamp-fmt='%a %b %e %H:%M:%S %Y' (provides
|
|
localized weekday and month names in the ctime(3) style),
|
|
or '%a, %e %b %Y %H:%M:%S %z (%Z)' for a RFC 2822 format,
|
|
or maybe '%Y-%m-%d %H:%M:%S%z' for an ISO 8601 (EN 28601) format,
|
|
or just '%Y%m%dT%H%M%S' .
|
|
|
|
=item B<-u> I<username>, B<--username>=I<username>
|
|
|
|
Run as the named user. If this option is not set, the default behaviour
|
|
is to setuid() to the user running C<spamc>, if C<spamd> is running
|
|
as root.
|
|
|
|
Note: "--username=root" is not a valid option. If specified, C<spamd> will
|
|
exit with a fatal error on startup.
|
|
|
|
=item B<-g> I<groupname>, B<--groupname>=I<groupname>
|
|
|
|
Run as the named group if --username is being used. If this option is
|
|
not set when --username is used then the primary group for the user
|
|
given to --username is used.
|
|
|
|
=item B<-x>, B<--nouser-config>, B<--user-config>
|
|
|
|
Turn off (on) reading of per-user configuration files (user_prefs) from the
|
|
user's home directory. The default behaviour is to read per-user
|
|
configuration from the user's home directory (B<--user-config>).
|
|
|
|
This option does not disable or otherwise influence the SQL, LDAP or
|
|
Virtual Config Dir settings.
|
|
|
|
=item B<-U> I<username>, B<--default-user>=I<username>
|
|
|
|
Fall back to this username, if the username provided by spamc is not found.
|
|
Default is I<nobody>, which might not exist or not have a usable home
|
|
directory, use this setting to define a suitable user if needed.
|
|
|
|
=item B<-A> I<host,...>, B<--allowed-ips>=I<host,...>
|
|
|
|
Specify a comma-separated list of authorized hosts or networks which
|
|
can connect to this spamd instance. Each element of the list is either a
|
|
single IP addresses, or a range of IP addresses in address/masklength CIDR
|
|
notation, or ranges of IPv4 addresses by specifying 3 or less octets with
|
|
a trailing dot. Hostnames are not supported, only IPv4 or IPv6 addresses.
|
|
This option can be specified multiple times, or can take a list of addresses
|
|
separated by commas. IPv6 addresses may be (but need not be) enclosed
|
|
in square brackets for consistency with option B<--listen>. Examples:
|
|
|
|
B<-A 10.11.12.13> -- only allow connections from C<10.11.12.13>.
|
|
|
|
B<-A 10.11.12.13,10.11.12.14> -- only allow connections from C<10.11.12.13> and
|
|
C<10.11.12.14>.
|
|
|
|
B<-A 10.200.300.0/24> -- allow connections from any machine in the range
|
|
C<10.200.300.*>.
|
|
|
|
B<-A 10.> -- allow connections from any machine in the range C<10.*.*.*>.
|
|
|
|
B<-A [2001:db8::]/32,192.0.2.0/24,::1,127.0.0.0/8> -- only accept
|
|
connections from specified test networks and from localhost.
|
|
|
|
In absence of the B<-A> option, connections are only accepted from
|
|
IP address 127.0.0.1 or ::1, i.e. from localhost on a loopback interface.
|
|
|
|
=item B<-D> [I<area,...>], B<--debug> [I<area,...>]
|
|
|
|
Produce debugging output. If no areas are listed, all debugging information is
|
|
printed. Diagnostic output can also be enabled for each area individually;
|
|
I<area> is the area of the code to instrument. For example, to produce
|
|
diagnostic output on bayes, learn, and dns, use:
|
|
|
|
spamassassin -D bayes,learn,dns
|
|
|
|
Higher priority informational messages that are suitable for logging in normal
|
|
circumstances are available with an area of "info".
|
|
|
|
For more information about which areas (also known as channels) are available,
|
|
please see the documentation at:
|
|
|
|
C<https://wiki.apache.org/spamassassin/DebugChannels>
|
|
|
|
=item B<-4>, B<--ipv4only>, B<--ipv4-only>, B<--ipv4>
|
|
|
|
Use IPv4 where applicable, do not use IPv6.
|
|
The option affects a set of listen sockets (see option C<--listen>)
|
|
and disables IPv6 for DNS tests.
|
|
|
|
=item B<-6>
|
|
|
|
Use IPv6 where applicable, do not use IPv4.
|
|
The option affects a set of listen sockets (see option C<--listen>)
|
|
and disables IPv4 for DNS tests. Installing a module IO::Socket::IP
|
|
is recommended if spamd is expected to receive requests over IPv6.
|
|
|
|
=item B<-L>, B<--local>
|
|
|
|
Perform only local tests on all mail. In other words, skip DNS and other
|
|
network tests. Works the same as the C<-L> flag to C<spamassassin(1)>.
|
|
|
|
=item B<-P>, B<--paranoid>
|
|
|
|
Die on user errors (for the user passed from spamc) instead of falling back
|
|
to user C<--default-user> and using the default configuration.
|
|
|
|
=item B<-m> I<number> , B<--max-children>=I<number>
|
|
|
|
This option specifies the maximum number of children to spawn.
|
|
Spamd will spawn that number of children, then sleep in the background
|
|
until a child dies, wherein it will go and spawn a new child.
|
|
|
|
Incoming connections can still occur if all of the children are busy,
|
|
however those connections will be queued waiting for a free child.
|
|
The minimum value is C<1>, the default value is C<5>.
|
|
|
|
Please note that there is a OS specific maximum of connections that can be
|
|
queued (Try C<perl -MSocket -e'print SOMAXCONN'> to find this maximum).
|
|
|
|
Note that if you run too many servers for the amount of free RAM available, you
|
|
run the danger of hurting performance by causing a high swap load as server
|
|
processes are swapped in and out continually.
|
|
|
|
=item B<--min-children>=I<number>
|
|
|
|
The minimum number of children that will be kept running. The minimum value is
|
|
C<1>, the default value is C<1>. If you have lots of free RAM, you may want to
|
|
increase this.
|
|
|
|
=item B<--min-spare>=I<number>
|
|
|
|
The lower limit for the number of spare children allowed to run. A
|
|
spare, or idle, child is one that is not handling a scan request. If
|
|
there are too few spare children available, a new server will be started
|
|
every second or so. The default value is C<1>.
|
|
|
|
=item B<--max-spare>=I<number>
|
|
|
|
The upper limit for the number of spare children allowed to run. If there
|
|
are too many spare children, one will be killed every second or so until
|
|
the number of idle children is in the desired range. The default value
|
|
is C<2>.
|
|
|
|
=item B<--max-conn-per-child>=I<number>
|
|
|
|
This option specifies the maximum number of connections each child
|
|
should process before dying and letting the master spamd process spawn
|
|
a new child. The minimum value is C<1>, the default value is C<200>.
|
|
|
|
=item B<--round-robin>
|
|
|
|
By default, C<spamd> will attempt to keep a small number of "hot" child
|
|
processes as busy as possible, and keep any others as idle as possible, using
|
|
something similar to the Apache httpd server scaling algorithm. This is
|
|
accomplished by the master process coordinating the activities of the children.
|
|
This switch will disable this scaling algorithm, and the behaviour seen in
|
|
the 3.0.x versions will be used instead, where all processes receive an
|
|
equal load and no scaling takes place.
|
|
|
|
=item B<--timeout-tcp>=I<number>
|
|
|
|
This option specifies the number of seconds to wait for headers from a
|
|
client (spamc) before closing the connection. The minimum value is C<1>,
|
|
the default value is C<30>, and a value of C<0> will disable socket
|
|
timeouts completely.
|
|
|
|
=item B<--timeout-child>=I<number>
|
|
|
|
This option specifies the number of seconds to wait for a spamd child to
|
|
process or check a message. The minimum value is C<1>, the default
|
|
value is C<300>, and a value of C<0> will disable child timeouts completely.
|
|
|
|
=item B<-H> I<directory>, B<--helper-home-dir>=I<directory>
|
|
|
|
Specify that external programs such as Razor, DCC, and Pyzor should have
|
|
a HOME environment variable set to a specific directory. The default
|
|
is to use the HOME environment variable setting from the shell running
|
|
spamd. By specifying no argument, spamd will use the spamc caller's
|
|
home directory instead.
|
|
|
|
=item B<--ssl>
|
|
|
|
Accept only SSL connections on the associated port.
|
|
The B<IO::Socket::SSL> perl module must be installed.
|
|
|
|
If the B<--ssl> switch is used, and B<--ssl-port> is not supplied, then
|
|
B<--port> port will be used to accept SSL connections instead of unencrypted
|
|
connections. If the B<--ssl> switch is used, and B<--ssl-port> is set, then
|
|
unencrypted connections will be accepted on the B<--port>, at the same time as
|
|
encrypted connections are accepted at B<--ssl-port>.
|
|
|
|
=item B<--ssl-verify>
|
|
|
|
Implies B<--ssl>. Request a client certificate and verify the certificate.
|
|
Requires B<--ssl-ca-file> or B<--ssl-ca-path>.
|
|
|
|
=item B<--ssl-ca-file>=I<cafile>
|
|
|
|
Implies B<--ssl-verify>. Use the specified Certificate Authority
|
|
certificate to verify the client certificate. The client certificate must
|
|
be signed by this certificate.
|
|
|
|
=item B<--ssl-ca-path>=I<capath>
|
|
|
|
Implies B<--ssl-verify>. Use the Certificate Authority certificate files in
|
|
the specified set of directories to verify the client certificate. The
|
|
client certificate must be signed by one of these Certificate Authorities.
|
|
See the man page for B<IO::Socket::SSL> for additional details.
|
|
|
|
=item B<--ssl-port>=I<port>
|
|
|
|
Optionally specifies the port number for the server to listen on for
|
|
SSL connections (default: whatever --port uses). See B<--ssl> for
|
|
more details.
|
|
|
|
=item B<--server-key> I<keyfile>
|
|
|
|
Specify the SSL key file to use for SSL connections.
|
|
|
|
=item B<--server-cert> I<certfile>
|
|
|
|
Specify the SSL certificate file to use for SSL connections.
|
|
|
|
=item B<--socketpath> I<pathname>
|
|
|
|
Listen on a UNIX domain socket at path I<pathname>, in addition to
|
|
sockets specified with a C<--listen> option. This option is provided
|
|
for compatibility with older versions of spamd. Starting with version
|
|
3.4.0 the C<--listen> option can also take a UNIX domain socket as its
|
|
value (an absolute path name). Unlike C<--socketpath>, the C<--listen>
|
|
option may be specified multiple times if spamd needs to listen on
|
|
multiple UNIX or INET or INET6 sockets.
|
|
|
|
Warning: the Perl support on BSD platforms for UNIX domain sockets seems to
|
|
have a bug regarding paths of over 100 bytes or so (SpamAssassin bug 4380).
|
|
If you see a 'could not find newly-created UNIX socket' error message, and
|
|
the path appears truncated, this may be the cause. Try using a shorter path
|
|
to the socket.
|
|
|
|
By default, use of B<--socketpath> without B<--listen> will inhibit
|
|
SSL connections and unencrypted TCP connections. To add other sockets,
|
|
specify them with B<--listen>, e.g. '--listen=:' or '--listen=*:'
|
|
|
|
=item B<--socketowner> I<name>
|
|
|
|
Set UNIX domain socket to be owned by the user named I<name>. Note
|
|
that this requires that spamd be started as C<root>, and if C<-u>
|
|
is used, that user should have write permissions to unlink the file
|
|
later, for when the C<spamd> server is killed.
|
|
|
|
=item B<--socketgroup> I<name>
|
|
|
|
Set UNIX domain socket to be owned by the group named I<name>. See
|
|
C<--socketowner> for notes on ownership and permissions.
|
|
|
|
=item B<--socketmode> I<mode>
|
|
|
|
Set UNIX domain socket to use the octal mode I<mode>. Note that if C<-u> is
|
|
used, that user should have write permissions to unlink the file later, for
|
|
when the C<spamd> server is killed.
|
|
|
|
|
|
=item B<--timing>
|
|
|
|
Enable timing measurements and output the information for logging. This
|
|
is the same information as provided by the TIMING tag.
|
|
|
|
=back
|
|
|
|
=head1 SEE ALSO
|
|
|
|
spamc(1)
|
|
spamassassin(1)
|
|
Mail::SpamAssassin::Conf(3)
|
|
Mail::SpamAssassin(3)
|
|
|
|
=head1 PREREQUISITES
|
|
|
|
C<Mail::SpamAssassin>
|
|
|
|
=head1 AUTHORS
|
|
|
|
The SpamAssassin(tm) Project (https://spamassassin.apache.org/)
|
|
|
|
=head1 LICENSE
|
|
|
|
SpamAssassin is distributed under the Apache License, Version 2.0, as
|
|
described in the file C<LICENSE> included with the distribution.
|
|
|
|
=cut
|