my $PREFIX = '@@PREFIX@@'; my $DEF_RULES_DIR = '@@DEF_RULES_DIR@@'; my $LOCAL_RULES_DIR = '@@LOCAL_RULES_DIR@@'; use lib '@@INSTALLSITELIB@@';
BEGIN { if ( -e '../blib/lib/Mail/SpamAssassin.pm' ) {
unshift(@INC, '../blib/lib');
}
else {
unshift(@INC, '../lib');
}
}
use strict;
use Config;
use IO::Socket;
use IO::Handle;
use IO::Pipe;
use Mail::SpamAssassin;
use Mail::SpamAssassin::NoMailAudit;
use Mail::SpamAssassin::NetSet;
use Getopt::Long;
use Pod::Usage;
use Sys::Syslog qw(:DEFAULT setlogsock);
use POSIX qw(:sys_wait_h);
use POSIX qw(setsid);
use Errno;
use Cwd ();
use File::Spec 0.8;
use File::Path;
BEGIN {
eval { require Time::HiRes };
Time::HiRes->import( qw(time) ) unless $@;
}
sub spawn; sub logmsg;
my %resphash = (
EX_OK => 0, EX_USAGE => 64, EX_DATAERR => 65, EX_NOINPUT => 66, EX_NOUSER => 67, EX_NOHOST => 68, EX_UNAVAILABLE => 69, EX_SOFTWARE => 70, EX_OSERR => 71, EX_OSFILE => 72, EX_CANTCREAT => 73, EX_IOERR => 74, EX_TEMPFAIL => 75, EX_PROTOCOL => 76, EX_NOPERM => 77, EX_CONFIG => 78, );
my %opt = (
'user-config' => 1,
'ident-timeout' => 5.0,
);
Mail::SpamAssassin::Util::am_running_in_taint_mode();
Mail::SpamAssassin::Util::clean_path_in_taint_mode();
Mail::SpamAssassin::Util::untaint_var(\%ENV);
my $ORIG_ARG0 = Mail::SpamAssassin::Util::untaint_var($0);
my @ORIG_ARGV = Mail::SpamAssassin::Util::untaint_var(\@ARGV);
my $ORIG_CWD = Mail::SpamAssassin::Util::untaint_var(Cwd::cwd());
Getopt::Long::Configure ("bundling");
GetOptions(
'socketpath=s' => \$opt{'socketpath'},
'auto-whitelist|whitelist|a' => \$opt{'auto-whitelist'},
'create-prefs!' => \$opt{'create-prefs'},
'c' => \$opt{'create-prefs'},
'daemonize!' => \$opt{'daemonize'},
'd' => \$opt{'daemonize'},
'help|h' => \$opt{'help'},
'listen-ip|ip-address|i=s' => \$opt{'listen-ip'},
'max-children|m=i' => \$opt{'max-children'},
'port|p=i' => \$opt{'port'},
'sql-config!' => \$opt{'sql-config'},
'q' => \$opt{'sql-config'},
'setuid-with-sql' => \$opt{'setuid-with-sql'},
'Q' => \$opt{'setuid-with-sql'},
'virtual-config|V=s' => \$opt{'virtual-config'},
'virtual-config-dir=s' => \$opt{'virtual-config-dir'},
'pidfile|r=s' => \$opt{'pidfile'},
'syslog|s=s' => \$opt{'syslog'},
'syslog-socket=s' => \$opt{'syslog-socket'},
'username|u=s' => \$opt{'username'},
'vpopmail!' => \$opt{'vpopmail'},
'v' => \$opt{'vpopmail'},
'configpath|C=s' => \$opt{'configpath'},
'siteconfigpath=s' => \$opt{'siteconfigpath'},
'user-config' => \$opt{'user-config'},
'nouser-config|x' => sub{ $opt{'user-config'} = 0 },
'allowed-ips|A=s' => \@{$opt{'allowed-ip'}},
'debug!' => \$opt{'debug'},
'D' => \$opt{'debug'},
'local!' => \$opt{'local'},
'L' => \$opt{'local'},
'paranoid!' => \$opt{'paranoid'},
'P' => \$opt{'paranoid'},
'helper-home-dir|H:s' => \$opt{'home_dir_for_helpers'},
'auth-ident' => \$opt{'auth-ident'},
'ident-timeout=f' => \$opt{'ident-timeout'},
'ssl' => \$opt{'ssl'},
'server-key=s' => \$opt{'server-key'},
'server-cert=s' => \$opt{'server-cert'},
'add-from!' => sub { warn "The --add-from option has been removed\n" },
'F=i' => sub { warn "The -F option has been removed\n" },
'S' => sub { warn "The -S option has been removed\n" },
'stop-at-threshold!' => sub { warn "The --stop-at-threshold option has been removed\n" },
) or pod2usage(-exitval => $resphash{'EX_USAGE'}, -verbose => 0);
$opt{'help'} and pod2usage(-exitval => $resphash{'EX_USAGE'}, -verbose => 0, -message => 'For more details, use "man spamd"');
foreach my $opt (qw(
configpath
siteconfigpath
socketpath
pidfile
home_dir_for_helpers
virtual-config
)) {
$opt{$opt} = Mail::SpamAssassin::Util::untaint_file_path (
File::Spec->rel2abs($opt{$opt}) ) if($opt{$opt});
}
if ( defined $opt{'socketpath'}
and (( @{$opt{'allowed-ip'}} > 0 )
or defined $opt{'ssl'}
or defined $opt{'auth-ident'}
or defined $opt{'port'} ))
{
pod2usage(-exitval => $resphash{'EX_USAGE'}, -verbose => 0, -message =>
"ERROR: --socketpath mutually exclusive with --allowed-ip/--ssl/--port params");
}
my $allowed_nets = Mail::SpamAssassin::NetSet->new();
if ( not defined $opt{'socketpath'} )
{
if(@{$opt{'allowed-ip'}}) {
set_allowed_ip(split /,/, join(',', @{$opt{'allowed-ip'}}));
}
else {
set_allowed_ip('127.0.0.1');
}
}
if ($opt{'auth-ident'}) {
eval { require Net::Ident };
die "fatal: ident-based authentication requested, but Net::Ident is unavailable\n" if ($@);
$opt{'ident-timeout'} = undef if $opt{'ident-timeout'} <= 0.0;
import Net::Ident qw(ident_lookup);
}
$opt{'server-key'} ||= "$LOCAL_RULES_DIR/certs/server-key.pem";
$opt{'server-cert'} ||= "$LOCAL_RULES_DIR/certs/server-cert.pem";
if ($opt{'ssl'}) {
eval { require IO::Socket::SSL };
die "fatal: SSL encryption requested, but IO::Socket::SSL is unavailable\n" if ($@);
if (!-e $opt{'server-key'}) {
die "The server key file $opt{'server-key'} does not exist\n";
}
if (!-e $opt{'server-cert'}) {
die "The server certificate file $opt{'server-cert'} does not exist\n";
}
}
my $log_facility = $opt{'syslog'} || 'mail';
my $log_socket = $opt{'syslog-socket'} || 'unix';
$log_facility = 'stderr' if $log_socket eq 'none';
$log_facility = 'null' if $log_facility eq 'stderr' and $opt{'debug'};
my $dontcopy = 1;
if ($opt{'create-prefs'}) { $dontcopy = 0; }
my $extrapid = 5000;
undef($opt{'max-children'}) unless defined($opt{'max-children'}) && $opt{'max-children'} > 0;
$extrapid = $opt{'max-children'} if defined($opt{'max-children'});
if (defined $opt{'pidfile'}) {
$opt{'pidfile'} = Mail::SpamAssassin::Util::untaint_file_path($opt{'pidfile'});
}
my $orighome;
if (defined $ENV{'HOME'}) {
if ( defined $opt{'username'} ) { if ( my $nh = (getpwnam($opt{'username'}))[7] ) {
$ENV{'HOME'} = $nh;
}
else {
die "Can't determine home directory for user '".$opt{'username'}."'!\n";
}
}
$orighome = $ENV{'HOME'}; delete $ENV{'HOME'}; }
my $spamtest = Mail::SpamAssassin->new({
dont_copy_prefs => $dontcopy,
rules_filename => ($opt{'configpath'} || 0),
site_rules_filename => ($opt{'siteconfigpath'} || 0),
local_tests_only => ($opt{'local'} || 0),
debug => ($opt{'debug'} || 0),
paranoid => ($opt{'paranoid'} || 0),
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
});
if ($log_facility ne 'stderr') {
eval {
setlogsock($log_socket);
syslog('debug', "spamd starting"); };
if ($@) {
eval {
setlogsock('inet');
syslog('debug', "spamd starting");
};
$log_socket = 'inet' unless $@;
}
if ($@) {
warn "failed to setlogsock(${log_socket}) on this platform; reporting logs to stderr\n";
$log_facility = 'stderr';
}
}
my($port, $addr, $proto);
my($listeninfo);
if ( defined $opt{'socketpath'} )
{
$listeninfo = "UNIX domain socket " . $opt{'socketpath'};
}
else
{
$port = $opt{'port'} || 783;
$addr = (gethostbyname($opt{'listen-ip'} || '127.0.0.1'))[0];
$proto = getprotobyname('tcp');
($port) = $port =~ /^(\d+)$/ or die "invalid port\n";
$listeninfo = "port $port/tcp";
}
my $server;
if ( $opt{'socketpath'} ) {
my $path = $opt{'socketpath'};
if ( -e $path )
{
if ( new IO::Socket::UNIX(Peer => $path, Type => SOCK_STREAM) )
{
undef $opt{'socketpath'};
die "spamd already running on $path, exiting\n";
}
else
{
unlink $path;
}
}
$server = new IO::Socket::UNIX(Local => $path,
Type => SOCK_STREAM,
Listen => SOMAXCONN
) || die "Could not create UNIX socket on $path: $! $@\n";
chmod 0666, $path; }
elsif ($opt{'ssl'}) {
$server = new IO::Socket::SSL(LocalAddr => $addr,
LocalPort => $port,
Proto => $proto,
Type => SOCK_STREAM,
ReuseAddr => 1,
Listen => SOMAXCONN,
SSL_verify_mode => 0x00,
SSL_key_file => $opt{'server-key'},
SSL_cert_file => $opt{'server-cert'}
) || die "Could not create SSL socket: $! $@\n";
}
else {
$server = new IO::Socket::INET(LocalAddr => $addr,
LocalPort => $port,
Proto => $proto,
Type => SOCK_STREAM,
ReuseAddr => 1,
Listen => SOMAXCONN
) || die "Could not create INET socket: $! $@\n";
}
$opt{'daemonize'} and daemonize();
if(defined($opt{'pidfile'})) {
open PIDF,">$opt{'pidfile'}" or warn "Can't write to PID file: $!";
print PIDF "$$\n";
close PIDF;
}
my $setuid_to_user = 0;
if ($opt{'username'}) {
my ($uuid,$ugid) = (getpwnam($opt{'username'}))[2,3];
if (!defined $uuid || $uuid == 0) {
die "fatal: cannot run as nonexistent user or root with -u option\n";
}
$uuid =~ /^(\d+)$/ and $uuid = $1; $ugid =~ /^(\d+)$/ and $ugid = $1;
if (defined $opt{'pidfile'}) {
chown $uuid, -1, $opt{'pidfile'}
|| die "fatal: could not chown '$opt{'pidfile'}' to uid $uuid\n";
}
if (defined $opt{'socketpath'}) {
chown $uuid, -1, $opt{'socketpath'}
|| die "fatal: could not chown '$opt{'socketpath'}' to uid $uuid\n";
}
$) = "$ugid $ugid"; $( = $ugid;
$> = $uuid; $< = $uuid; if ($> != $uuid and $> != ($uuid-2**32)) {
die "fatal: setuid to uid $uuid failed\n";
}
} elsif ($> == 0) {
if ( !$opt{'vpopmail'} ) {
$setuid_to_user = 1;
}
}
$opt{'auto-whitelist'} and eval
{
require Mail::SpamAssassin::DBBasedAddrList;
my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
$spamtest->set_persistent_address_list_factory ($addrlistfactory);
};
my $got_sighup;
$SIG{HUP} = \&restart_handler;
preload_modules_with_tmp_homedir();
$SIG{CHLD} = 'DEFAULT';
$SIG{INT} = \&kill_handler;
$SIG{TERM} = \&kill_handler;
if ($opt{'debug'}) {
warn "server started on $listeninfo (running version ". Mail::SpamAssassin::Version().")\n";
warn "server pid: $$\n";
}
logmsg("server started on $listeninfo (running version ". Mail::SpamAssassin::Version(). ")");
my $current_user;
my $client;
my $assign = "/var/qmail/users/assign"; my (%qmailu, $qu_load);
my $readvec = "";
my %pipes = ();
vec($readvec, $server->fileno, 1) = 1;
$SIG{CHLD} = 'IGNORE';
while (1) {
if (defined $client) { $client->close; } cleanupchildren();
my $n = select(my $rd=$readvec, undef, undef, undef);
if ($got_sighup) {
defined($opt{'pidfile'}) and unlink($opt{'pidfile'});
chdir($ORIG_CWD) || die "spamd restart failed: chdir failed: ${ORIG_CWD}: $!\n";
exec ($ORIG_ARG0, @ORIG_ARGV);
die "spamd restart failed: exec failed: " . join (' ', $ORIG_ARG0, @ORIG_ARGV) . ": $!\n";
}
next if ($n == -1 && $! == &Errno::EINTR);
die "select failed: $!" if ($n <= 0);
if ($opt{'max-children'}) {
while (my ($kid, $pipe) = each %pipes) {
next unless vec($rd, $pipe->fileno(), 1);
vec($readvec, $pipe->fileno(), 1) = 0;
delete ($pipes{$kid});
$extrapid++;
Mail::SpamAssassin::dbg("cleaned up kid $kid, pool=$extrapid");
vec($readvec, $server->fileno, 1) = 1;
}
}
if (vec($rd, $server->fileno, 1)) {
if ($opt{'max-children'} && $extrapid <= 0) {
vec($readvec, $server->fileno, 1) = 0;
logmsg "hit max-children limit (".$opt{'max-children'}."): waiting for some to exit";
next;
}
$client = $server->accept;
if (!$client) {
if ($! == &Errno::EINTR) {
next;
} elsif ($! == 0 && $opt{'ssl'}) {
logmsg("SSL failure: " . &IO::Socket::SSL::errstr());
next;
} else {
die "accept failed: $!";
}
}
my $start = time;
if ( $opt{'socketpath'} )
{
logmsg("got connection over " . $opt{'socketpath'});
} else {
my($port, $ip) = sockaddr_in($client->peername);
my $name = gethostbyaddr($ip, AF_INET);
$ip = inet_ntoa($ip);
$name ||= $ip;
if (ip_is_allowed($ip)) {
logmsg("connection from $name [$ip] at port $port");
} else {
logmsg("unauthorized connection from $name [$ip] at port $port");
$client->close;
next;
}
}
spawn sub {
$client->autoflush(1);
local ($_) = $client->getline;
if (!defined $_) {
protocol_error ("(closed before headers)");
return 1;
}
chomp;
if (/SKIP SPAMC\/(.*)/)
{
logmsg "skipped large message in ".
sprintf("%3d", time - $start) ." seconds.";
return 0;
}
elsif (/(PROCESS|CHECK|SYMBOLS|REPORT|REPORT_IFSPAM) SPAMC\/(.*)/)
{
check ($1, $2, $start);
}
else
{
protocol_error ($_);
}
};
}
}
sub check {
my ($method, $version, $start) = @_;
local ($_);
my $expected_length;
if($version > 1.0)
{
while(1)
{
my $line = $client->getline;
if(!defined $line)
{
protocol_error ("(EOF during headers)");
return 1;
}
$line =~ s/\r\n$//;
last unless $line;
my($header, $value) = split(/: /, $line, 2);
if (!defined $value)
{
protocol_error("(header not in 'Name: value' format)");
return 1;
}
if ($header eq 'User')
{
if ($value !~ /^([\x20-\xFF]*)$/)
{
protocol_error ("(User header contains control chars)");
return 1;
}
$current_user = $1;
auth_ident($current_user) if $opt{'auth-ident'};
if (!$opt{'user-config'})
{
if ($opt{'sql-config'}) {
handle_user_sql($current_user);
} elsif ($opt{'virtual-config'} || $opt{'virtual-config-dir'}) {
handle_virtual_user($current_user);
} elsif ($opt{'setuid-with-sql'}) {
handle_user_setuid_with_sql($current_user);
$setuid_to_user = 1; }
} else {
handle_user($current_user);
if ($opt{'sql-config'}) {
handle_user_sql($current_user);
}
}
}
elsif ($header eq 'Content-length')
{
if ($value !~ /^(\d*)$/)
{
protocol_error ("(Content-Length contains non-numeric bytes)");
return 1;
}
$expected_length = $1;
}
}
}
if ( $setuid_to_user && $> == 0 )
{
if ($spamtest->{paranoid}) {
logmsg "PARANOID: still running as root, closing connection.";
die;
}
logmsg "Still running as root: user not specified with -u, ".
"not found, or set to root. Fall back to nobody.";
my ($uid,$gid) = (getpwnam('nobody'))[2,3];
$uid =~ /^(\d+)$/ and $uid = $1; $gid =~ /^(\d+)$/ and $gid = $1;
$) = "$gid $gid"; $> = $uid; if ( !defined($uid) || ($> != $uid and $> != ($uid-2**32))) {
logmsg "fatal: setuid to nobody failed";
die;
}
}
if ($opt{'sql-config'} && !defined($current_user)) {
handle_user_sql('nobody');
}
my $resp = "EX_OK";
my @msglines;
my $actual_length = 0;
my $msgid;
while ($_ = $client->getline()) {
if (($actual_length == 0) .. /^$/) { if (/^Message-Id:\s+(.*?)\s*$/i) {
$msgid = $1;
while($msgid =~ s/\([^\(\)]*\)//) {}; # remove comments and
$msgid =~ s/^\s+|\s+$//g; # leading and trailing spaces
$msgid =~ s/\s.*$//; # keep only the first token
}
}
push(@msglines, $_);
$actual_length += length;
last if (defined($expected_length) && ($actual_length == $expected_length));
}
$msgid ||= "(unknown)";
$current_user ||= "(unknown)";
logmsg(
($method eq 'PROCESS' ? "processing" : "checking") .
" message $msgid for $current_user:$>."
);
my $mail = Mail::SpamAssassin::NoMailAudit->new (
data => \@msglines
);
if($expected_length && ($actual_length != $expected_length)) {
protocol_error ("(Content-Length mismatch: Expected $expected_length bytes, got $actual_length bytes)");
return 1;
}
my $status = $spamtest->check($mail);
my $msg_score = sprintf("%.1f",$status->get_hits);
my $msg_threshold = sprintf("%.1f",$status->get_required_hits);
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') {
$status->rewrite_mail;
my $msg_resp = join '',$mail->header,"\n",@{$mail->body};
my $msg_resp_length = length($msg_resp);
if($version >= 1.3) {
print $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) {
print $client "SPAMD/1.1 $resphash{$resp} $resp\r\n",
"Content-length: $msg_resp_length\r\n",
"\r\n", $msg_resp;
}
else {
print $client "SPAMD/1.0 $resphash{$resp} $resp\r\n", $msg_resp;
}
}
else {
print $client "SPAMD/1.1 $resphash{$resp} $resp\r\n";
if($method eq "CHECK") {
print $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") {
}
elsif($method eq "SYMBOLS") {
$msg_resp = $status->get_names_of_tests_hit;
$msg_resp .= "\r\n" if ($version < 1.3);
}
else {
die "unknown method $method";
}
if($version >= 1.3) {
printf $client "Content-length: %d\r\n%s\r\n\r\n%s",
length($msg_resp), $spamhdr, $msg_resp;
}
else {
printf $client "%s\r\n\r\n%s",
$spamhdr, $msg_resp;
}
}
}
logmsg "$was_it_spam ($msg_score/$msg_threshold) for $current_user:$> in ".
sprintf("%.1f", time - $start) ." seconds, $actual_length bytes.";
$status->finish(); }
sub protocol_error {
local $_ = shift;
my $resp = "EX_PROTOCOL";
print $client "SPAMD/1.0 $resphash{$resp} Bad header line: $_\r\n";
logmsg "bad protocol: header error: $_";
}
sub spawn {
my $coderef = shift;
unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') {
warn "usage: spawn CODEREF";
}
my $pid;
my $pipe;
if ($opt{'max-children'}) {
$pipe=IO::Pipe->new();
if (!defined($pipe)) {
logmsg "pipe failed: $!";
return;
}
}
$extrapid--;
if (!defined($pid = fork)) {
logmsg "cannot fork: $!";
$extrapid++;
return;
} elsif ($pid) {
if ($opt{'max-children'}) {
$pipe->reader();
$pipes{$pid} = $pipe;
vec($readvec, $pipe->fileno(), 1) = 1;
}
return; }
if ($opt{'max-children'}) {
$pipe->writer();
%pipes = (); }
$server->close;
select STDERR;
$SIG{CHLD} = 'DEFAULT';
exit &$coderef();
}
sub auth_ident
{
my $username = shift;
my $ident_username = ident_lookup($client, $opt{'ident-timeout'});
my $dn = $ident_username || 'NONE'; warn "ident_username = $dn, spamc_username = $username\n" if $opt{'debug'};
if ($username ne $ident_username) {
logmsg "fatal: ident username ($dn) does not match " .
"spamc username ($username)";
exit 1;
}
}
sub handle_user
{
my $username = shift;
my $userid = '';
if ($opt{'vpopmail'} && $opt{'username'}) {
$userid = $opt{'username'};
} elsif ( $opt{'vpopmail'} ) {
$userid = "vpopmail";
} else {
$userid = $username;
}
my ($name,$pwd,$uid,$gid,$quota,$comment,$gcos,$dir,$etc) =
getpwnam($userid);
if ( !$spamtest->{'paranoid'} && !defined($uid) ) {
logmsg "handle_user: unable to find user '$userid'!";
return 0;
}
$uid =~ /^(\d+)$/ and $uid = $1; $gid =~ /^(\d+)$/ and $gid = $1;
if ($setuid_to_user) {
$) = "$gid $gid"; $> = $uid; if ( !defined($uid) || ($> != $uid and $> != ($uid-2**32))) {
logmsg "fatal: setuid to $username failed";
die; } else {
logmsg "info: setuid to $username succeeded";
}
}
if ($opt{'vpopmail'}) {
$dir = `$dir/bin/vuserinfo -d $username`;
chomp ($dir);
}
my $cf_file = $dir."/.spamassassin/user_prefs";
if ($opt{'vpopmail'}) {
if ($opt{'username'}) {
create_default_cf_if_needed ($cf_file, $username, $dir);
$spamtest->read_scoreonly_config ($cf_file);
$spamtest->signal_user_changed ({ username => $username,
user_dir => "$dir" });
} else {
my $sysnam = get_user_from_address ($username);
$spamtest->read_scoreonly_config ($cf_file);
$spamtest->signal_user_changed ({ username => $sysnam,
user_dir => "$dir" })
}
} else {
create_default_cf_if_needed ($cf_file, $username);
$spamtest->read_scoreonly_config ($cf_file);
$spamtest->signal_user_changed ({ username => $username,
user_dir => $dir });
}
return 1;
}
sub handle_virtual_user
{
my $username = shift;
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; }
$dir =~ s/\%u/${safename}/g;
$dir =~ s/\%l/${localpart}/g;
$dir =~ s/\%d/${domain}/g;
$userdir = $dir;
$prefsfile = $dir.'/user_prefs';
logmsg("Using default config for $username: $prefsfile");
} else {
$dir=$opt{'virtual-config'};
my $safename = $username; $safename =~ s/[^-A-Za-z0-9\+_\.\,\@\=]/_/gs;
$userdir = $dir.'/'.$safename;
$prefsfile = $dir.'/'.$safename.'.prefs';
if(! -f $prefsfile) {
$prefsfile="$dir/default.prefs";
if(! -f $prefsfile) {
logmsg("Couldn't find a virtual directory or defaults "
. "for $username in $dir: $prefsfile");
return(0);
} else {
logmsg("Using default config for $username: $prefsfile");
}
}
}
if (-f $prefsfile) {
$spamtest->read_scoreonly_config($prefsfile);
}
$spamtest->signal_user_changed ({ username => $username,
user_dir => $userdir });
return(1);
}
sub handle_user_sql
{
my $username = shift;
$spamtest->load_scoreonly_sql ($username);
return 1;
}
sub handle_user_setuid_with_sql
{
my $username = shift;
my ($name,$pwd,$uid,$gid,$quota,$comment,$gcos,$dir,$etc) = getpwnam($username);
if ( !$spamtest->{'paranoid'} && !defined($uid) ) {
logmsg "handle_user() -> unable to find user [$username]!\n";
return 0;
}
$uid =~ /^(\d+)$/ and $uid = $1; $gid =~ /^(\d+)$/ and $gid = $1;
if ($setuid_to_user) {
$) = "$gid $gid"; $> = $uid; if ( !defined($uid) || ($> != $uid and $> != ($uid-2**32))) {
logmsg "fatal: setuid to $username failed";
die; }
else
{
logmsg "info: setuid to $username succeeded, reading scores from SQL.";
}
}
my $spam_conf_dir = $dir . '/.spamassassin'; if ( ! -d $spam_conf_dir )
{
if ( mkdir $spam_conf_dir, 0700 )
{
logmsg "info: created $spam_conf_dir for $username.";
}
else
{
logmsg "info: failed to create $spam_conf_dir for $username.";
}
}
$spamtest->load_scoreonly_sql ($username);
$spamtest->signal_user_changed ({ username => $username });
return 1;
}
sub create_default_cf_if_needed {
my ($cf_file, $username, $userdir) = @_;
if( ! -r $cf_file && ! $spamtest->{'dont_copy_prefs'})
{
logmsg "Creating default_prefs [$cf_file]";
$spamtest->create_default_prefs ($cf_file,$username,$userdir);
if ( ! -r $cf_file ) {
logmsg "Couldn't create readable default_prefs for [$cf_file]";
}
}
}
sub get_user_from_address {
my ($user, $domain) = split(/@/, $_[0]);
my $dom = lc($domain);
if ( $qmailu{$dom} ne "" ) {
warn "returning result from cache.\n" if ($opt{'debug'});
my $nam = getpwuid($qmailu{$dom});
return $nam;
} else {
warn "cache miss\n" if ($opt{'debug'});
&fill_qmailu_cache($assign);
if ( $qmailu{$dom} ne "" ) {
my $nam = getpwuid($qmailu{$dom});
return $nam;
} else {
return 0;
}
}
}
sub fill_qmailu_cache {
my ($READ, $WRITE) = (stat($_[0]))[8,9];
if ( $WRITE > $qu_load ) {
undef %qmailu;
$qu_load = time;
open(ASSIGN, $_[0]) || die "couldn't open $_[0]: $!\n";
warn "loading $_[0] into cache...." if ($opt{'debug'});
while(<ASSIGN>) {
my @data = split(/:/, $_);
$qmailu{$data[1]} = $data[2];
};
warn "done.\n" if ($opt{'debug'});
close(ASSIGN);
} else {
warn "$_[0] already cached.\n" if ($opt{'debug'});
}
}
sub logmsg
{
my $orig_sigpipe_handler = $SIG{'PIPE'};
$SIG{'PIPE'} = sub { $main::SIGPIPE_RECEIVED++; };
my $msg = join(" ", @_);
chomp($msg);
warn "logmsg: $msg\n" if $opt{'debug'};
if ($log_facility eq 'stderr') {
print STDERR "$msg\n";
}
elsif ($log_facility ne 'null') {
openlog('spamd', 'cons,pid', $log_facility);
eval { syslog('info', "%s", $msg); };
if ($@) {
warn "syslog() failed, try using --syslog-socket switch ($@)\n";
}
if ($main::SIGPIPE_RECEIVED) {
closelog();
openlog('spamd', 'cons,pid', $log_facility);
syslog('info', "%s", $msg);
$msg = "SIGPIPE received - reopening log socket";
warn "logmsg: $msg\n" if $opt{'debug'};
syslog('warning', "%s", $msg);
if ($main::SIGPIPE_RECEIVED > 1) {
warn "logging failure: multiple SIGPIPEs received\n";
}
$main::SIGPIPE_RECEIVED = 0;
}
}
$SIG{'PIPE'} = $orig_sigpipe_handler if defined($orig_sigpipe_handler);
}
sub kill_handler
{
my ($sig) = @_;
logmsg "server killed by SIG$sig, shutting down";
$server->close;
defined($opt{'pidfile'}) and unlink($opt{'pidfile'});
defined($opt{'socketpath'}) and unlink($opt{'socketpath'});
exit 0;
}
sub restart_handler {
my ($sig) = @_;
logmsg "server hit by SIG$sig, restarting";
unless ($server->eof) {
$server->shutdown (2);
$server->close;
defined($opt{'socketpath'}) and unlink($opt{'socketpath'});
warn "server socket closed\n" if $opt{'debug'};
}
$got_sighup = 1;
}
use POSIX 'setsid';
sub daemonize
{
$0 = join(' ', $ORIG_ARG0, @ORIG_ARGV) unless($opt{'debug'});
chdir '/' or die "Can't chdir to /: $!\n";
$SIG{__WARN__} = sub { logmsg($_[0]); };
open STDIN, "</dev/null" or die "Can't read from /dev/null: $!\n";
open STDOUT, ">/dev/null" or die "Can't write to /dev/null: $!\n";
defined(my $pid=fork) or die "Can't fork: $!\n";
exit if $pid;
setsid or die "Can't start new session: $!\n";
open STDERR,'>&STDOUT' or die "Can't duplicate stdout: $!\n";
Mail::SpamAssassin::dbg('daemonized.');
}
sub set_allowed_ip {
foreach (@_) {
$allowed_nets->add_cidr ($_) or die "Aborting.\n";
}
}
sub ip_is_allowed {
$allowed_nets->contains_ip (@_);
}
sub preload_modules_with_tmp_homedir {
my $tmpdir = File::Spec->tmpdir();
if (!$tmpdir) {
die "cannot find writable tmp dir! set TMP or TMPDIR in env";
}
delete $ENV{'TMPDIR'} if ( !defined $ENV{'TMPDIR'} );
my $tmphome = File::Spec->catdir ($tmpdir, "spamd-$$-init");
$tmphome = Mail::SpamAssassin::Util::untaint_file_path ($tmphome);
my $tmpsadir = File::Spec->catdir ($tmphome, ".spamassassin");
Mail::SpamAssassin::dbg("Preloading modules with HOME=$tmphome");
mkdir($tmphome, 0700) or die "fatal: Can't create $tmphome: $!";
mkdir($tmpsadir, 0700) or die "fatal: Can't create $tmpsadir: $!";
$ENV{HOME} = $tmphome;
$spamtest->compile_now(0); $/ = "\n";
delete $ENV{HOME};
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 = Mail::SpamAssassin::Util::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 (-d $tmphome) {
$err ||= "do something: $!";
warn "Failed to remove $tmphome: Could not $err\n";
}
}
sub cleanupchildren {
if ($^O !~ /linux/) {
my $kid;
do {
$kid = waitpid(-1,&WNOHANG);
} while ($kid > 0);
}
}
__DATA__
=head1 NAME
spamd - daemonized version of spamassassin
=head1 SYNOPSIS
spamd [options]
Options:
-a, --auto-whitelist, --whitelist Use auto-whitelists
-c, --create-prefs Create user preferences files
-C path, --configpath=path Path for default config files
--siteconfigpath=path Path for site configs (def: /etc/mail/spamassassin)
-d, --daemonize Daemonize
-h, --help Print usage message.
-i ipaddr, --listen-ip=ipaddr,... Listen on the IP ipaddr (default: 127.0.0.1)
-m num, --max-children num Allow maximum num children
-p port, --port Listen on specified port (default: 783)
-q, --sql-config Enable SQL config (only useful with -x)
-Q, --setuid-with-sql Enable SQL config (only useful with -x,
enables use of -a and -H)
-V, --virtual-config=dir Enable Virtual configs (needs -x)
--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 (default: mail)
--syslog-socket=type How to connect to syslogd (default: unix)
-u username, --username=username Run as username
-v, --vpopmail Enable vpopmail config
-x, --nouser-config Disable user config files
--auth-ident Use ident to authenticate spamc user
--ident-timeout=timeout Timeout for ident connections
-A host,..., --allowed-ips=..,.. Limit ip addresses which can connect
-D, --debug Print debugging messages
-L, --local Use local tests only (no DNS)
-P, --paranoid Die upon user errors
-H dir Specify a different HOME directory, path optional
--ssl Run an SSL server
--server-key keyfile Specify an SSL keyfile
--server-cert certfile Specify an SSL certificate
--socketpath=path Listen on given UNIX domain socket
=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<--auto-whitelist>) can be negated by
adding I<--no> (B<--noauto-whitelist>), however, this is usually unnecessary.
=over 4
=item B<-a>, B<--auto-whitelist>, B<--whitelist>
Use auto-whitelists. Auto-whitelists track the long-term average score for
each sender and then shift the score of new messages toward that long-term
average. This can increase or decrease the score for messages, depending on
the long-term behavior of the particular correspondent. See the README file
for more details.
=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<-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<-i> I<ipaddress>, B<--listen-ip>=I<ipaddress>, B<--ip-address>=I<ipaddress>
Tells spamd to listen on the specified IP address [defaults to 127.0.0.1]. Use
0.0.0.0 to listen on all interfaces.
=item B<-p> I<port>, B<--port>=I<port>
Optionally specifies the port number for the server to listen on.
=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>.
=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<-a> (AWL) and B<-H> (Helper home directories.)
=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. If this and the B<--virtual-config> switch are
both used, this will take precedence.
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.
=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, it
will be loaded as the user's preferences. The auto-whitelist and/or 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-based configuration.
The pattern B<must> expand to an absolute directory when spamd is running
daemonized (B<-d>).
=item B<-V>=I<directory>, B<--virtual-config>=I<directory>
This option specifies where per-user preferences can be found for virtual
users, for the B<-x> switch.
The files are in the format of B<I<username>.prefs>. A B<default.prefs> file
will be used if an individual user config is not found.
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<_>).
Note that this B<requires> that B<-x> is used, and cannot be combined with
SQL-based configuration.
If a subdirectory is found in that directory, called B<I<username>>, and it is
writable, it will be used to store auto-whitelist and/or Bayes databases for
that user.
=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 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.
If specified without B<-u>, then it allows every mail account on a vpopmail
virtual domain setup to have their own user-customizable spamassassin
preferences, assuming they have their own home directory set.
=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.
=item B<--syslog-socket>=I<type>
Specify how spamd should send messages to syslogd. The options are C<unix>,
C<inet> or C<none>. 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 dysfunctional versions of
the B<Sys::Syslog> package which do not support some socket types, so you may
need to set this. If you get error messages regarding B<__PATH_LOG> or similar
from spamd, try changing this setting.
=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" disables the setuid() functionality and leaves
spamd running as root.
=item B<-x>, B<--nouser-config>, B<--user-config>
Turn off(on) per-user config files. All users will just get the default
configuration. The default behaviour is for per-user configuration
to be off.
=item B<--auth-ident>
Verify the username provided by spamc using ident. This is only
useful if connections are only allowed from trusted hosts (because an
identd that lies is trivial to create) and if spamc REALLY SHOULD be
running as the user it represents. Connections are terminated
immediately if authentication fails. In this case, spamc will pass
the mail through unchecked. Failure to connect to an ident server,
and response timeouts are considered authentication failures. This
requires that Net::Ident be installed.
=item B<--ident-timeout>=I<timeout>
Wait at most I<timeout> seconds for a response to ident queries.
Authentication that takes long that I<timeout> seconds will fail, and
mail will not be processed. Setting this to 0.0 or less results in no
timeout, which is STRONGLY discouraged. The default is 5 seconds.
=item B<-A> I<host,...>, B<--allowed-ips>=I<host,...>
Specify a list of authorized hosts or networks which can connect to this spamd
instance. Single IP addresses can be given, ranges of IP addresses in
address/masklength CIDR format, or ranges of IP addresses by listing 3 or less
octets with a trailing dot. Hostnames are not supported, only IP addresses.
This option can be specified multiple times, or can take a list of addresses
separated by commas. 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.*.*.*>.
By default, connections are only accepted from localhost [127.0.0.1].
=item B<-D>, B<--debug>
Print debugging messages
=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 I<nobody> and using the default configuration.
=item B<-m> I<number>, B<--max-children>=I<number>
Specify a maximum number of children to spawn. Spamd will wait until another
child finishes before forking again. Meanwhile, incoming connections will be
queued.
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). Also,
this option causes spamd to create an extra pipe for each child.
=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. The B<IO::Socket::SSL> perl module must be
installed.
=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 UNIX domain path I<pathname> instead of a TCP socket.
=back
=head1 BUGS
Perl 5.005_03 seems to have a bug, which spamd triggers, causing messages to
pass through unscanned. Upgrading to Perl 5.6 seems to fix the problem, so
that's the current workaround. More information can be found at
http://bugzilla.spamassassin.org/show_bug.cgi?id=497
The module IO::Socket::INET from Perl 5.005 needs too much time to shut down
the port, so when spamd receives the HUP signal to reload itself, it will die
because it can't open that port. Updating IO::Socket or (better) to Perl 5.6
or later should help.
The C<-m> switch seems to trigger signal-handling bugs in many versions
of Perl.
=head1 SEE ALSO
spamc(1)
spamassassin(1)
Mail::SpamAssassin(3)
Mail::SpamAssassin::Conf(3)
=head1 AUTHOR
Craig R Hughes E<lt>craig@hughes-family.orgE<gt>
=head1 PREREQUISITES
C<Mail::SpamAssassin>
=cut