| #!/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 { |
| $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; |
| }; |
| |
| require Socket; |
| 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>[:<port>]], B<--listen>[=I<ipaddress>[:<port>]] |
| |
| 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 <ipaddress>, 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 |