PAL.C.T MINI SHELL
# Functions for turning SpamAssassin filtering on or off on a per-domain basis
sub init_spam
{
$domain_lookup_cmd = "$module_config_directory/lookup-domain.pl";
$procmail_spam_dir = "$module_config_directory/procmail";
$spam_config_dir = "$module_config_directory/spam";
$quota_spam_margin = 5*1024*1024;
$spamassassin_lock_file = "/tmp/virtualmin.spamassassin";
}
sub require_spam
{
return if ($require_spam++);
&foreign_require("procmail", "procmail-lib.pl");
&foreign_require("spam", "spam-lib.pl");
}
sub check_depends_spam
{
if (!$_[0]->{'mail'}) {
# Mail must be enabled for spam filtering to work!
return $text{'setup_edepspam'};
}
if ($config{'mail_system'} == 5) {
# Not implemented for VPopMail
return $text{'setup_edepspamvpop'};
}
return undef;
}
# setup_spam(&domain)
# Adds the master procmail entry for domain-specific spam filtering, plus an
# include file for this domain.
sub setup_spam
{
&$first_print($text{'setup_spam'});
&require_spam();
&foreign_require("cron", "cron-lib.pl");
# Create the needed directories now, so we can lock files in them
if (!-d $procmail_spam_dir) {
&make_dir($procmail_spam_dir, 0755);
&set_ownership_permissions(undef, undef, 0755, $procmail_spam_dir);
}
if (!-d $spam_config_dir) {
&make_dir($spam_config_dir, 0755);
&set_ownership_permissions(undef, undef, 0755, $spam_config_dir);
}
local $spamdir = "$spam_config_dir/$_[0]->{'id'}";
&make_dir($spamdir, 0750);
&set_ownership_permissions($_[0]->{'uid'}, $_[0]->{'gid'}, 0750, $spamdir);
&obtain_lock_spam($_[0]);
&obtain_lock_cron($_[0]);
# Add the procmail entry to get the VIRTUALMIN variable
local @recipes = &procmail::get_procmailrc();
local ($r, $gotvirt, $gotdef);
foreach $r (@recipes) {
if ($r->{'type'} eq '=' &&
$r->{'action'} =~ /^VIRTUALMIN=/) {
$gotvirt++;
}
elsif ($r->{'name'} eq "DEFAULT") {
$gotdef++;
}
}
if (!$gotvirt) {
# Need to add entries to lookup the domain, and run it's include file
local $var1 = { 'flags' => [ 'w', 'i' ],
'conds' => [ ],
'type' => '=',
'action' => "VIRTUALMIN=|$domain_lookup_cmd \$LOGNAME" };
local $testcmd = &has_command("test") || "test";
local $var2 = { 'flags' => [ ],
'conds' => [ [ "?", "$testcmd \"\$VIRTUALMIN\" != \"\"" ] ],
'block' => "INCLUDERC=$procmail_spam_dir/\$VIRTUALMIN",
};
if ($gconfig{'os_type'} eq 'solaris') {
# Need to call sh as shell explicitly
$var2->{'conds'} =
[ [ "?", "sh -c \"$testcmd '\$VIRTUALMIN' != ''\"" ] ];
}
# If the procmailrc file is empty, add at the end.
# If there is a TRAP variable, add after it (so we do logging properly)
# Otherwise, add at the top
if (@recipes) {
# Has some recipes .. check if there is a TRAP
local ($trap, $aftertrap);
for(my $i=0; $i<@recipes; $i++) {
if ($recipes[$i]->{'name'} eq 'TRAP') {
$trap = $recipes[$i];
$trapafter = $recipes[$i+1];
}
}
if ($trapafter) {
# Add before the recipe that is after TRAP
&procmail::create_recipe_before($var1, $trapafter);
&procmail::create_recipe_before($var2, $trapafter);
}
elsif ($trap) {
# Nothing after TRAP, so just add at end
&procmail::create_recipe($var1);
&procmail::create_recipe($var2);
}
else {
# Just add at start
&procmail::create_recipe_before($var1, $recipes[0]);
&procmail::create_recipe_before($var2, $recipes[0]);
}
}
else {
&procmail::create_recipe($var1);
&procmail::create_recipe($var2);
}
# Fix up bad quoted VIRTUAMIN= line, introduced by Webmin 1.410
local $lref = &read_file_lines($procmail::procmailrc);
foreach my $l (@$lref) {
if ($l =~ /^\"(VIRTUALMIN=.*)\"$/) {
$l = $1;
}
}
&flush_file_lines($procmail::procmailrc);
}
# Add procmail rule to bounce mail if quota is full
&setup_quota_full_bounce();
# Create the lookup-domain.pl wrapper script, and hack it to turn off setuid
&cron::create_wrapper($domain_lookup_cmd, $module_name,
"lookup-domain.pl");
local $lref = &read_file_lines($domain_lookup_cmd);
splice(@$lref, 1, 0, "delete(\$ENV{'IFS'});",
"delete(\$ENV{'CDPATH'});",
"delete(\$ENV{'ENV'});",
"delete(\$ENV{'BASH_ENV'});",
"\$ENV{'PATH'} = '/bin:/usr/bin';",
"\$< = \$>;",
"\$( = \$);");
&flush_file_lines($domain_lookup_cmd);
# Build spamassassin command to call
local $cmd = &spamassassin_client_command($_[0]);
# Create recipes to call spamassassin
local $spamrc = "$procmail_spam_dir/$_[0]->{'id'}";
local $recipe0 = { 'name' => 'DROPPRIVS', # Run all commands as user
'value' => 'yes' };
local @conds;
if ($cmd =~ /spamassassin/ && $config{'spam_size'}) {
# Add condition for max message size
push(@conds, [ '<', $config{'spam_size'} ]);
}
local $recipe1 = { 'flags' => [ 'f', 'w' ], # Call spamassassin
'conds' => \@conds,
'type' => '|',
'action' => $cmd,
};
if ($config{'spam_lock'}) {
# Add locking to prevent concurrent runs
$recipe1->{'lockfile'} = $spamassassin_lock_file;
}
local ($recipe2, $recipe3);
local $varon = { 'name' => 'SPAMMODE', 'value' => 1 };
if ($config{'spam_level'}) {
# Recipe to delete high-score spam
local $stars = join("", map { "\\*" } (1..$config{'spam_level'}));
$recipe3 = { 'flags' => [ ],
'conds' => [ [ '', '^X-Spam-Level: '.$stars ] ],
'action' => '/dev/null' };
}
if ($config{'spam_delivery'}) {
# Receipe to deliver spam to some folder
$recipe2 = { 'flags' => [ ],
'conds' => [ [ '', '^X-Spam-Status: Yes' ] ],
'action' => $config{'spam_delivery'} };
}
local $varoff = { 'name' => 'SPAMMODE', 'value' => 0 };
&procmail::create_recipe($recipe0, $spamrc);
&procmail::create_recipe($recipe1, $spamrc);
if ($recipe2 || $recipe3) {
&procmail::create_recipe($varon, $spamrc);
&procmail::create_recipe($recipe3, $spamrc) if ($recipe3);
&procmail::create_recipe($recipe2, $spamrc) if ($recipe2);
&procmail::create_recipe($varoff, $spamrc);
}
&set_ownership_permissions(undef, undef, 0755, $spamrc);
# Link all files in the default directory (/etc/mail/spamassassin) to
# the domain's directory
&create_spam_config_links($_[0]);
# Create the config file for this server
&open_tempfile(TOUCH, ">$spamdir/virtualmin.cf", 0, 1);
&print_tempfile(TOUCH, "whitelist_from $d->{'emailto_addr'}\n");
&close_tempfile(TOUCH);
&set_ownership_permissions($_[0]->{'uid'}, $_[0]->{'gid'}, 0755,
"$spamdir/virtualmin.cf");
# Whitelist all domain mailboxes
if ($config{'spam_white'}) {
$_[0]->{'spam_white'} = 1;
&update_spam_whitelist($_[0]);
}
# Setup automatic spam clearing
local ($cmode, $cnum) = split(/\s+/, $tmpl->{'spamclear'});
if ($cmode eq 'days' || $cmode eq 'size') {
&save_domain_spam_autoclear($_[0], { $cmode => $cnum });
}
# Setup spamtrap aliases, if requested
if ($tmpl->{'spamtrap'} eq 'yes') {
&obtain_lock_mail($_[0]);
&setup_spamtrap_aliases($_[0]);
&release_lock_mail($_[0]);
}
&release_lock_cron($_[0]);
&release_lock_spam($_[0]);
&$second_print($text{'setup_done'});
}
# spamassassin_client_command(&domain, [client])
# Returns the command for calling spamassassin in some domain, plus args
sub spamassassin_client_command
{
local ($d, $client) = @_;
local $spamid = $d->{'parent'} || $d->{'id'};
$client ||= $config{'spam_client'};
local $cmd = &has_command($client);
if ($client eq 'spamc') {
local ($host, $port) = split(/:/, $config{'spam_host'});
if ($host) {
$cmd .= " -d $host";
if ($port) {
$cmd .= " -p $port";
}
}
if ($config{'spam_size'}) {
$cmd .= " -s $config{'spam_size'}";
}
}
else {
$cmd .= " --siteconfigpath $spam_config_dir/$spamid";
}
return $cmd;
}
# validate_spam(&domain)
# Make sure the domain's procmail config file exists
sub validate_spam
{
local ($d) = @_;
local $spamrc = "$procmail_spam_dir/$d->{'id'}";
return &text('validate_espamprocmail', "<tt>$spamrc</tt>") if (!-r $spamrc);
local $spamdir = "$spam_config_dir/$d->{'id'}";
return &text('validate_espamconfig', "<tt>$spamdir</tt>") if (!-d $spamdir);
&require_spam();
local @recs = &procmail::parse_procmail_file($spamrc);
local $cmd = $spam::config{'spamassassin'};
local $found;
foreach my $r (@recs) {
$found++ if ($r->{'action'} =~ /\Q$cmd\E|spamc|spamassassin/);
}
return &text('validate_espamcall', "<tt>$spamrc</tt>") if (!$found);
return undef;
}
# setup_default_delivery()
# Adds or removes a rule at the end of /etc/procmailrc delivering to $DEFAULT,
# depending on the config setting
sub setup_default_delivery
{
&require_spam();
&obtain_lock_spam();
local @recipes = &procmail::get_procmailrc();
my ($gotdef, $gotorgmail, $gotdel, $gotdrop);
foreach my $r (@recipes) {
if ($r->{'action'} eq '$DEFAULT' && !@{$r->{'conds'}}) {
$gotdel = $r;
}
}
# The rule to deliver to $DEFAULT is needed to prevent users from creating
# their own .procmailrc files
if ($config{'default_procmail'} && !$gotdel) {
# Append default delivery rule
my $rec = { 'flags' => [ ],
'conds' => [ ],
'action' => '$DEFAULT' };
&procmail::create_recipe($rec);
}
elsif (!$config{'default_procmail'} && $gotdel) {
# Remove default delivery rule
&procmail::delete_recipe($gotdel);
}
# Find the DEFAULT variable setting
@recipes = &procmail::get_procmailrc();
foreach my $r (@recipes) {
if ($r->{'name'} eq 'DEFAULT') {
$gotdef = $r;
}
}
# The DEFAULT destination needs to be set to match the mail server, as procmail
# will deliver to /var/mail/USER by default
local ($dir, $style, $mailbox, $maildir) = &get_mail_style();
local $maildef = $dir && $dir =~ /^(.*)\/$/ ? "$1/\$LOGNAME/" :
$dir ? "$dir/\$LOGNAME" :
$maildir ? "\$HOME/$maildir/" :
$mailbox ? "\$HOME/$mailbox" : undef;
if ($gotdef) {
# Update default delivery definition
$gotdef->{'value'} = $maildef;
&procmail::modify_recipe($gotdef);
}
else {
# Create default delivery definition
my $rec = { 'name' => 'DEFAULT',
'value' => $maildef };
if (@recipes) {
&procmail::create_recipe_before($rec, $recipes[0]);
}
else {
&procmail::create_recipe($rec);
}
}
# Find the ORGMAIL variable
@recipes = &procmail::get_procmailrc();
foreach my $r (@recipes) {
if ($r->{'name'} eq 'ORGMAIL') {
$gotorgmail = $r;
}
}
# Same for the ORGMAIL destination, to prevent delivery falling back to
# /var/mail/XXX in an over-quota situation
if ($gotorgmail) {
# Update default delivery rule
$gotorgmail->{'value'} = $maildef;
&procmail::modify_recipe($gotorgmail);
}
else {
# Create default delivery rule
my $rec = { 'name' => 'ORGMAIL',
'value' => $maildef };
if (@recipes) {
&procmail::create_recipe_before($rec, $recipes[0]);
}
else {
&procmail::create_recipe($rec);
}
}
# Re-get the default delivery receipe, and DROPPRIVS
$gotdel = undef;
@recipes = &procmail::get_procmailrc();
foreach my $r (@recipes) {
if ($r->{'action'} eq '$DEFAULT' &&
!@{$r->{'conds'}}) {
$gotdel = $r;
}
elsif ($r->{'name'} eq 'DROPPRIVS') {
$gotdrop = $r;
}
}
# DROPPRIVS needs to be set to yes to force delivery as the correct user. This
# must be done before the rule that delivers to $DEFAULT, or at the end of the
# file.
if (!$gotdrop) {
my $rec = { 'name' => 'DROPPRIVS',
'value' => 'yes' };
if ($gotdel) {
# Add before default rule
&procmail::create_recipe_before($rec, $gotdel);
}
else {
# Add at end
&procmail::create_recipe($rec);
}
}
&release_lock_spam();
}
# enable_procmail_logging()
# Configure Procmail to log to /var/log/procmail.log, and setup logrotate
# for that directory.
sub enable_procmail_logging
{
&require_spam();
&obtain_lock_spam();
local @recipes = &procmail::get_procmailrc();
local ($gotlog, $gottrap);
foreach my $r (@recipes) {
if ($r->{'name'} eq 'LOGFILE') {
$gotlog = 1;
}
if ($r->{'name'} eq 'TRAP') {
$gottrap = 1;
}
}
if (!$gotlog) {
# Add LOGFILE variables
my $rec0 = { 'name' => 'LOGFILE',
'value' => $procmail_log_file };
&procmail::create_recipe_before($rec0, $recipes[0]);
}
if (!$gottrap) {
# Add TRAP, which specifies a command to output logging info about
# the email after delivery
my $rec1 = { 'name' => 'TRAP', 'value' => $procmail_log_cmd };
&procmail::create_recipe_before($rec1, $recipes[0]);
}
# For any domains with spam or virus filtering enabled, add SPAMMODE and
# VIRUSMODE procmail variables so that the logger knows what kind of destination
# email ended up at
foreach my $d (&list_domains()) {
next if (!$d->{'spam'});
&obtain_lock_spam($d);
local $spamrc = "$procmail_spam_dir/$d->{'id'}";
local @recipes = &procmail::parse_procmail_file($spamrc);
local ($spamrec, $spamrecafter, $gotspammode);
local $i = 0;
foreach my $r (@recipes) {
if ($r->{'name'} eq 'SPAMMODE') {
$gotspammode = 1;
}
elsif ($r->{'conds'}->[0]->[1] eq '^X-Spam-Status: Yes') {
# Found place to insert
$spamrec = $r;
$spamrecafter = $recipes[$i+1];
last;
}
$i++;
}
if ($spamrec && !$gotspammode) {
local $varon = { 'name' => 'SPAMMODE', 'value' => 1 };
local $varoff = { 'name' => 'SPAMMODE', 'value' => 0 };
if ($spamrecafter) {
&procmail::create_recipe_before($varoff, $spamrecafter,
$spamrc);
}
else {
&procmail::create_recipe($varoff, $spamrc);
}
&procmail::create_recipe_before($varon, $spamrec, $spamrc);
}
# Do the same for viruses
if ($d->{'virus'}) {
local @recipes = &procmail::parse_procmail_file($spamrc);
local ($clamrec, $clamafter, $gotclammode);
local $i = 0;
foreach my $r (@recipes) {
if ($r->{'name'} eq 'VIRUSMODE') {
$gotclammode = 1;
}
elsif ($r->{'action'} =~ /^\Q$clam_wrapper_cmd\E/) {
# Insert after this one
$clamrec = $recipes[$i+1];
$clamrecafter = $recipes[$i+2];
}
$i++;
}
if ($clamrec && !$gotclammode) {
local $varon = { 'name' => 'VIRUSMODE', 'value' => 1 };
local $varoff = { 'name' => 'VIRUSMODE', 'value' => 0 };
if ($clamrecafter) {
&procmail::create_recipe_before(
$varoff, $clamrecafter, $spamrc);
}
else {
&procmail::create_recipe($varoff, $spamrc);
}
&procmail::create_recipe_before(
$varon, $clamrec, $spamrc);
}
}
&release_lock_spam($d);
}
# Copy the log writer command to /etc/webmin
©_source_dest("$module_root_directory/procmail-logger.pl",
$procmail_log_cmd);
&set_ownership_permissions(undef, undef, 0755, $procmail_log_cmd);
if ($config{'logrotate'} && &foreign_installed("logrotate")) {
# Add logrotate section, if needed
&require_logrotate();
local $log = &get_logrotate_section($procmail_log_file);
if (!$log) {
local $parent = &logrotate::get_config_parent();
local $lconf = { 'file' => &logrotate::get_add_file(),
'name' => [ $procmail_log_file ] };
$lconf->{'members'} = [
{ 'name' => 'rotate',
'value' => $config{'logrotate_num'} || 5 },
{ 'name' => 'daily' },
{ 'name' => 'compress' },
];
&lock_file($lconf->{'file'});
&logrotate::save_directive($parent, undef, $lconf);
&flush_file_lines($lconf->{'file'});
&unlock_file($lconf->{'file'});
}
# Make sure file exists, so logrotate doesn't complain
if (!-r $procmail_log_file) {
open(LOG, ">$procmail_log_file");
close(LOG);
}
}
&release_lock_spam();
}
# procmail_logging_enabled()
# Returns 1 if logging entries exist in /etc/procmailrc
sub procmail_logging_enabled
{
&require_spam();
local @recipes = &procmail::get_procmailrc();
foreach my $r (@recipes) {
if ($r->{'name'} eq 'LOGFILE') {
return 1;
}
}
return 0;
}
# modify_spam(&domain, &olddomain)
# Doesn't have to do anything
sub modify_spam
{
}
# delete_spam(&domain)
# Just remove the domain's procmail config file
sub delete_spam
{
&$first_print($_[0]->{'virus'} ? $text{'delete_spamvirus'}
: $text{'delete_spam'});
&obtain_lock_spam($_[0]);
local $spamrc = "$procmail_spam_dir/$_[0]->{'id'}";
&unlink_logged($spamrc);
local $spamdir = "$spam_config_dir/$_[0]->{'id'}";
&system_logged("rm -rf ".quotemeta($spamdir));
&clear_lookup_domain_cache($_[0]);
&save_domain_spam_autoclear($_[0], undef);
&release_lock_spam($_[0]);
&$second_print($text{'setup_done'});
}
# clone_spam(&domain, &old-domain)
# Copy per-domain procmail rules and spamassassin config files to new domain,
# correcting the domain ID
sub clone_spam
{
local ($d, $oldd) = @_;
&$first_print($text{'clone_spam'});
&obtain_lock_spam($d);
local $pm = "$procmail_spam_dir/$d->{'id'}";
local $opm = "$procmail_spam_dir/$oldd->{'id'}";
©_source_dest($opm, $pm);
local $lref = &read_file_lines($pm);
foreach my $l (@$lref) {
$l =~ s/\Q$oldd->{'id'}\E/$d->{'id'}/;
}
&flush_file_lines($pm);
local $spamdir = "$spam_config_dir/$d->{'id'}";
local $ospamdir = "$spam_config_dir/$oldd->{'id'}";
&system_logged("rm -rf ".quotemeta($spamdir)."/*");
&system_logged("cd ".quotemeta($ospamdir)." && tar cf - . | ".
"(cd $spamdir && tar xpf -)");
# Fix email addresses in per-domain spamassassin config file
local $spamfile = "$spamdir/virtualmin.cf";
&set_ownership_permissions($d->{'uid'}, $d->{'gid'}, undef, $spamfile);
local $lref = &read_file_lines($spamfile);
foreach my $l (@$lref) {
if ($l =~ /^whitelist_from\s+\Q$oldd->{'emailto_addr'}\E/) {
$l = "whitelist_from $d->{'emailto_addr'}";
}
}
&flush_file_lines($spamfile);
# Re-update whitelist
if ($d->{'spam_white'}) {
&update_spam_whitelist($d);
}
# Copy automatic spam clearing
&save_domain_spam_autoclear($d, &get_domain_spam_autoclear($oldd));
&release_lock_spam($d);
&$second_print($text{'setup_done'});
return 1;
}
# check_spam_clash()
# No need to check for clashes ..
sub check_spam_clash
{
return 0;
}
# backup_spam(&domain, file)
# Saves the server's procmail and spamassassin configuration to a file.
# Also saves the auto-spam clearing settings.
sub backup_spam
{
local ($d, $file) = @_;
&$first_print($text{'backup_spamcp'});
local $spamrc = "$procmail_spam_dir/$d->{'id'}";
local $spamdir = "$spam_config_dir/$d->{'id'}";
if (-r $spamrc) {
©_write_as_domain_user($d, $spamrc, $file);
local $temp = &transname();
&execute_command("cd ".quotemeta($spamdir)." && tar cf ".
quotemeta($temp)." . 2>/dev/null ");
©_write_as_domain_user($d, $temp, $file."_cf");
&unlink_file($temp);
# Save spam clearing
local $auto = &get_domain_spam_autoclear($_[0]);
&write_as_domain_user($d,
sub { &write_file($file."_auto", $auto || { }) });
&$second_print($text{'setup_done'});
return 1;
}
else {
&$second_print($text{'backup_nospam'});
return 0;
}
}
# restore_spam(&domain, file)
# Restores the domains procmail and spamassassin configuration files.
# Also restores auto-clearing setting, if in backup.
sub restore_spam
{
&$first_print($text{'restore_spamcp'});
&obtain_lock_spam($_[0]);
local $spamrc = "$procmail_spam_dir/$_[0]->{'id'}";
local $spamdir = "$spam_config_dir/$_[0]->{'id'}";
&execute_command("cp ".quotemeta($_[1])." ".
quotemeta($spamrc));
&execute_command("cd ".quotemeta($spamdir)." && tar xf ".
quotemeta($_[1]."_cf"));
if (-r $_[1]."_auto") {
# Replace auto-clearing setting
&save_domain_spam_autoclear($_[0], undef);
local %auto;
&read_file($_[1]."_auto", \%auto);
if (%auto) {
&save_domain_spam_autoclear($_[0], \%auto);
}
}
# If spamtrap aliases exist, make sure the files and cron job do
local $st = &get_spamtrap_aliases($_[0]);
if ($st > 0) {
&setup_spamtrap_directories($_[0]);
&setup_spamtrap_cron();
}
# Re-create all spam links
&create_spam_config_links($_[0]);
&release_lock_spam($_[0]);
&$second_print($text{'setup_done'});
return 1;
}
# save_global_spam_lockfile(enable)
# Adds or removes a lockfile to all domains' spamassassin calls
sub save_global_spam_lockfile
{
local ($enabled) = @_;
foreach my $d (&get_domain_by("spam", 1)) {
local $spamrc = "$procmail_spam_dir/$d->{'id'}";
local @recipes = &procmail::parse_procmail_file($spamrc);
local @spamrec = &find_spam_recipe(\@recipes);
if ($spamrec[0]) {
# Found it .. modify
if ($enabled) {
$spamrec[0]->{'lockfile'} = $spamassassin_lock_file;
}
else {
delete($spamrec[0]->{'lockfile'});
}
&procmail::modify_recipe($spamrec[0]);
}
}
}
# sysinfo_spam()
# Returns the SpamAssassin version
sub sysinfo_spam
{
&require_spam();
local $vers = &spam::get_spamassassin_version();
return ( [ $text{'sysinfo_spam'}, $vers ] );
}
sub links_spam
{
local ($d) = @_;
local $client = &get_domain_spam_client($d);
if ($client ne "spamc") {
return ( { 'mod' => 'spam',
'desc' => $text{'links_spam'},
'page' => 'index.cgi?file='.&urlize(
"$spam_config_dir/$d->{'id'}/virtualmin.cf").
'&title='.&urlize(&show_domain_name($d)),
'cat' => 'services',
});
}
return ( );
}
# find_spam_recipe(&recipes)
# Returns the five recipes used for spam filtering, some of which may be
# undef if not set. They are :
# 0 - Call to spamassassin or spamc
# 1 - Setting of SPAMMODE=1
# 2 - Delivery for high-score spam
# 3 - Delivery for other spam
# 4 - Setting of SPAMMODE=0
sub find_spam_recipe
{
local ($recs) = @_;
local @rv;
for(my $i=0; $i<@$recs; $i++) {
if ($recs->[$i]->{'action'} =~ /(spamassassin|spamc)($|\s)/) {
# Found spamassassin
$rv[0] = $recs->[$i];
}
elsif ($recs->[$i]->{'name'} eq 'SPAMMODE') {
# Start or end of spam delivery block
if ($recs->[$i]->{'value'} eq '1') {
$rv[1] = $recs->[$i];
}
else {
# End of recipes we care about
$rv[4] = $recs->[$i];
last;
}
}
else {
# Look at conditions
my $r = $recs->[$i];
foreach my $c (@{$r->{'conds'}}) {
if ($c->[1] =~ /X-Spam-Status/i) {
$rv[3] ||= $r;
}
elsif ($c->[1] =~ /X-Spam-Level/i) {
$rv[2] ||= $r;
}
}
}
}
return @rv;
}
# get_domain_spam_delivery(&domain)
# Returns the delivery mode and dest for some domain. The modes can be :
# 0 - Throw away , 1 - File under home , 2 - Forward to email , 3 - Other file,
# 4 - Normal ~/mail/spam file , 5 - Deliver normally , 6 - ~/Maildir/.spam/ ,
# -1 - Broken!
# Also returns the score at which spam is deleted, and the destination (usually
# /dev/null)
sub get_domain_spam_delivery
{
local ($d) = @_;
&require_spam();
local $spamrc = "$procmail_spam_dir/$d->{'id'}";
local @recipes = &procmail::parse_procmail_file($spamrc);
local @spamrec = &find_spam_recipe(\@recipes);
local @rv;
if (!@spamrec) {
@rv = (-1, undef);
}
elsif (!$spamrec[3]) {
@rv = (5, undef);
}
elsif ($spamrec[3]->{'action'} eq '/dev/null') {
@rv = (0, undef);
}
elsif ($spamrec[3]->{'action'} =~ /^\$HOME\/mail\/(spam|Spam|junk|Junk)$/) {
@rv = (4, $1);
}
elsif ($spamrec[3]->{'action'} =~ /^\$HOME\/Maildir\/\.(spam|Spam|junk|Junk)\/$/) {
@rv = (6, $1);
}
elsif ($spamrec[3]->{'action'} =~ /^\$HOME\/(.*)$/) {
@rv = (1, $1);
}
elsif ($spamrec[3]->{'action'} =~ /\@/) {
@rv = (2, $spamrec[3]->{'action'});
}
else {
@rv = (3, $spamrec[3]->{'action'});
}
if ($spamrec[2]) {
# Add deletion recipe info
foreach my $c (@{$spamrec[2]->{'conds'}}) {
if ($c->[1] =~ /X-Spam-Level:\s+((\\\*)+)/i) {
# Found it
push(@rv, length($1)/2, $r->{'action'});
}
}
}
return @rv;
}
# save_domain_spam_delivery(&domain, [mode, dest], [delete-level, delete-dest])
# Updates the delivery method for spam for some domain
sub save_domain_spam_delivery
{
local ($d, $mode, $dest, $level, $ddest) = @_;
&require_spam();
local $spamrc = "$procmail_spam_dir/$d->{'id'}";
local @recipes = &procmail::parse_procmail_file($spamrc);
local @spamrec = &find_spam_recipe(\@recipes);
return 0 if (!@spamrec);
# Preserve existing settings if not set
local ($oldmode, $olddest, $oldlevel, $oldddest) =
&get_domain_spam_delivery($d);
if (!defined($mode)) {
($mode, $dest) = ($oldmode, $olddest);
}
elsif (!defined($dest)) {
$dest = $olddest;
}
if (!defined($level)) {
$level = $oldlevel;
}
if (!defined($ddest)) {
$ddest = $oldddest;
}
$ddest ||= "/dev/null";
# Remove the existing recipes (except the spamassassin call)
local @todel = sort { $b->{'line'} <=> $a->{'line'} }
grep { $_ } @spamrec[1..$#spamrec];
foreach my $r (@todel) {
&procmail::delete_recipe($r);
}
# Make those we now want
local @want;
if ($mode != 5 || $level) {
# Start of delivery section
push(@want, { 'name' => 'SPAMMODE', 'value' => 1 });
}
if ($level) {
# High-level deletion
local $stars = join("", map { "\\*" } (1..$level));
push(@want, { 'conds' => [ [ '', '^X-Spam-Level: '.$stars ] ],
'action' => $ddest });
}
if ($mode != 5) {
# Regular delivery
local $folder;
if ($mode == 4 || $mode == 6) {
if ($dest =~ /^[a-z0-9\.\_\-]+$/i) {
$folder = $dest;
}
else {
$folder = "Junk";
}
}
local $action = $mode == 0 ? "/dev/null" :
$mode == 4 ? "\$HOME/mail/$folder" :
$mode == 6 ? "\$HOME/Maildir/.$folder/" :
$mode == 1 ? "\$HOME/$dest" :
$dest;
local $type = $mode == 2 ? "!" : "";
push(@want, { 'conds' => [ [ '', '^X-Spam-Status: Yes' ] ],
'action' => $action,
'type' => $type });
}
if ($mode != 5 || $level) {
# End of delivery section
push(@want, { 'name' => 'SPAMMODE', 'value' => 0 });
}
# Add them
if (@want) {
@recipes = &procmail::parse_procmail_file($spamrc);
@spamrec = &find_spam_recipe(\@recipes);
local $idx = &indexof($spamrec[0], @recipes);
if ($idx == $#recipes) {
# Add at end
foreach my $r (@want) {
&procmail::create_recipe($r, $spamrc);
}
}
else {
# After spamassassin call
foreach my $r (@want) {
procmail::create_recipe_before($r, $recipes[$idx+1],
$spamrc);
}
}
}
&clear_lookup_domain_cache($_[0]);
return 1;
}
# get_domain_spam_client(&domain)
# Returns the client program (spamassassin or spamc) used by some domain
sub get_domain_spam_client
{
local ($d) = @_;
&require_spam();
local $spamrc = "$procmail_spam_dir/$d->{'id'}";
local @recipes = &procmail::parse_procmail_file($spamrc);
foreach my $r (@recipes) {
if ($r->{'action'} =~ /\/\S+\/(spamassassin|spamc)/) {
return $1;
}
}
return undef; # Cannot happen!
}
# save_domain_spam_client(&domain, spamassassin|spamc)
# Updates the procmail rule which calls spamassassin or spamc
sub save_domain_spam_client
{
local ($d, $client) = @_;
&require_spam();
local $spamrc = "$procmail_spam_dir/$d->{'id'}";
local @recipes = &procmail::parse_procmail_file($spamrc);
foreach my $r (@recipes) {
if ($r->{'action'} =~ /\/\S+\/(spamassassin|spamc)/) {
$r->{'action'} = &spamassassin_client_command($d, $client);
local ($c) = grep { $_->[0] eq '<' } @{$r->{'conds'}};
if ($c && !$config{'spam_size'}) {
# Remove size condition
@{$r->{'conds'}} = grep { $_ ne $c } @{$r->{'conds'}};
}
elsif (!$c && $config{'spam_size'}) {
# Add size condition
push(@{$r->{'conds'}}, [ '<', $config{'spam_size'} ]);
}
elsif ($c && $config{'spam_size'}) {
# Fix size in condition
$c->[1] = $config{'spam_size'};
}
&procmail::modify_recipe($r);
}
}
}
# get_global_spam_client()
# Returns the spam client that is supposed to be used by all domains. If this
# is spamc, also returns the spamd hostname and max message size
sub get_global_spam_client
{
local ($client, $host, $size);
if ($config{'spam_client_global'}) {
# We know the global setting for sure
$client = $config{'spam_client'};
}
else {
# Find the most used one for all domains
local (%cmdcount, $maxcmd);
foreach my $d (grep { $_->{'spam'} } &list_domains()) {
local $cmd = &get_domain_spam_client($d);
if ($cmd) {
$cmdcount{$cmd}++;
if (!$maxcmd || $cmdcount{$cmd} > $cmdcount{$maxcmd}) {
$maxcmd = $cmd;
}
}
}
return $maxcmd || $config{'spam_client'};
}
$host = $config{'spam_host'};
$size = $config{'spam_size'};
return wantarray ? ( $client, $host, $size ) : $client;
}
# save_global_spam_client(client, spamc-host, spamc-size)
# Updates all domains with a new SpamAssassin client program
sub save_global_spam_client
{
local ($client, $host, $size) = @_;
$config{'spam_client'} = $client;
$config{'spam_client_global'} = 1;
$config{'spam_host'} = $host;
$config{'spam_size'} = $size;
&save_module_config();
foreach my $d (grep { $_->{'spam'} } &list_domains()) {
&save_domain_spam_client($d, $client);
}
}
# get_global_quota_exitcode()
# Returns the exit code from lookup-domain when a user is close to or over quota
sub get_global_quota_exitcode
{
&require_spam();
local @recipes = &procmail::get_procmailrc();
foreach my $r (@recipes) {
if ($r->{'action'} =~ /\Q$domain_lookup_cmd\E\s+\-\-exitcode\s+(\d+)/) {
return $1;
}
}
return 73;
}
# save_global_quota_exitcode(code)
# Updates the exit code from lookup-domain when a user is close to or over quota
sub save_global_quota_exitcode
{
local ($code) = @_;
&require_spam();
&lock_file($procmail::procmailrc);
local @recipes = &procmail::get_procmailrc();
local $changed = 0;
foreach my $r (@recipes) {
if ($r->{'action'} =~ /\Q$domain_lookup_cmd\E\s+/) {
# Fix call to lookup-domain to return correct exit code
$r->{'action'} = "VIRTUALMIN=|$domain_lookup_cmd --exitcode $code \$LOGNAME";
&procmail::modify_recipe($r);
$changed++;
}
elsif ($r->{'conds'} && @{$r->{'conds'}} &&
$r->{'conds'}->[0]->[1] =~ /EXITCODE/) {
# Fix rule that uses the exit code
$r->{'conds'}->[0]->[1] =~ s/'(\d+)'/'$code'/;
$r->{'conds'}->[0]->[1] =~ s/"(\d+)"/"$code"/;
&procmail::modify_recipe($r);
$changed++;
}
}
&unlock_file($procmail::procmailrc);
return $changed ? undef : "No receipes found";
}
# update_spam_whitelist(&domain)
# Adds all mailboxes in this domain to the spamassassin whitelist in its
# configuration, and removes any whitelists that don't correspond to users.
sub update_spam_whitelist
{
local ($d) = @_;
return if (!$d->{'spam'} || !$d->{'spam_white'});
&require_spam();
local $spamfile = "$spam_config_dir/$d->{'id'}/virtualmin.cf";
local $conf = &spam::get_config($spamfile);
local @whites = &spam::find_value("whitelist_from", $conf);
local @oldwhites = @whites;
@whites = grep { !/\@$d->{'dom'}$/ } @whites;
foreach my $user (&list_domain_users($d, 0, 1, 1, 1)) {
push(@whites, &remove_userdom($user->{'user'}, $d)."\@".$d->{'dom'});
}
@whites = sort { $a cmp $b } @whites;
if (join(" ", @whites) ne join(" ", @oldwhites)) {
# Need to update spamassassin config
$spam::add_cf = $spamfile;
&spam::save_directives($conf, "whitelist_from", \@whites, 1);
&flush_file_lines($spamfile);
}
}
# show_template_spam(&template)
# Outputs HTML for editing spamassassin related template options
sub show_template_spam
{
local ($tmpl) = @_;
# Default spam clearing mode
local ($cmode, $cnum) = split(/\s+/, $tmpl->{'spamclear'});
local $cdays = $cmode eq 'days' ? $cnum : undef;
local $csize = $cmode eq 'size' ? $cnum : undef;
print &ui_table_row(&hlink($text{'tmpl_spamclear'}, 'template_spamclear'),
&ui_radio("spamclear", $cmode,
[ $tmpl->{'default'} ? ( )
: ( [ "", $text{'default'}."<br>" ] ),
[ "none", $text{'no'}."<br>" ],
[ "days", &text('spam_cleardays',
&ui_textbox("spamclear_days", $cdays, 5))."<br>" ],
[ "size", &text('spam_clearsize',
&ui_bytesbox("spamclear_size", $csize)) ],
]));
# Spamtrap default
print &ui_table_row(&hlink($text{'tmpl_spamtrap'}, 'template_spamtrap'),
&ui_radio("spamtrap", $tmpl->{'spamtrap'},
[ $tmpl->{'default'} ? ( )
: ( [ "", $text{'default'}."<br>" ] ),
[ "yes", $text{'yes'} ],
[ "none", $text{'no'} ] ]));
}
# parse_template_spam(&tmpl)
# Updates spamassassin related template options from %in
sub parse_template_spam
{
local ($tmpl) = @_;
# Parse clearing option
if ($in{'spamclear'} eq '') {
$tmpl->{'spamclear'} = '';
}
elsif ($in{'spamclear'} eq 'none') {
$tmpl->{'spamclear'} = 'none';
}
elsif ($in{'spamclear'} eq 'days') {
$in{'spamclear_days'} =~ /^\d+$/ && $in{'spamclear_days'} > 0 ||
&error($text{'spam_edays'});
$tmpl->{'spamclear'} = 'days '.$in{'spamclear_days'};
}
elsif ($in{'spamclear'} eq 'size') {
$in{'spamclear_size'} =~ /^\d+$/ && $in{'spamclear_size'} > 0 ||
&error($text{'spam_esize'});
$tmpl->{'spamclear'} = 'size '.($in{'spamclear_size'}*
$in{'spamclear_size_units'});
}
# Parse spam trap
$tmpl->{'spamtrap'} = $in{'spamtrap'};
}
# clear_lookup_domain_cache(&domain, [&user])
# Removes entries from the lookup-domain cache for a user all users in a domain
sub clear_lookup_domain_cache
{
local ($d, $user) = @_;
# Open the cache DBM
local $cachefile = "$ENV{'WEBMIN_VAR'}/lookup-domain-cache";
local %cache;
eval "use SDBM_File";
dbmopen(%cache, $cachefile, 0700);
eval "\$cache{'1111111111'} = 1";
if ($@) {
dbmclose(%cache);
eval "use NDBM_File";
dbmopen(%cache, $cachefile, 0700);
}
if ($user) {
# For just one user
delete($cache{$user->{'user'}});
}
else {
# For all users in a domain
foreach my $u (&list_domain_users($d, 0, 1, 1, 1)) {
delete($cache{$u->{'user'}});
}
}
}
# get_domain_spam_autoclear(&domain)
# Returns an object containing spam clearing info for this domain, if defined
sub get_domain_spam_autoclear
{
local ($d) = @_;
local %spamclear;
&read_file_cached($spamclear_file, \%spamclear);
local $ds = $spamclear{$d->{'id'}};
return undef if (!$ds);
local %auto = map { split(/=/, $_, 2) } split(/\s+/, $ds);
return \%auto;
}
# save_domain_spam_autoclear(&domain, &autoclear)
# Saves the automatic spam clearing policy for a domain, and sets up the
# cron job if needed
sub save_domain_spam_autoclear
{
local ($d, $auto) = @_;
# Update config file
local %spamclear;
&read_file_cached($spamclear_file, \%spamclear);
if ($auto) {
$spamclear{$d->{'id'}} = join(" ", map { $_."=".$auto->{$_} }
keys %$auto);
}
else {
delete($spamclear{$d->{'id'}});
}
&write_file($spamclear_file, \%spamclear);
&setup_spamclear_cron_job();
}
# setup_spamclear_cron_job()
# Create or remove the spam-clearing cron job based on whether retention or
# spam clearing is enabled for any domain
sub setup_spamclear_cron_job
{
local $job = &find_cron_script($spamclear_cmd);
local %spamclear;
&read_file_cached($spamclear_file, \%spamclear);
local $want = %spamclear || $config{'retention_policy'};
if ($job && !$want) {
# Disable job, as we don't need it
&delete_cron_script($job);
}
elsif (!$job && $want) {
# Enable the job
$job = { 'user' => 'root',
'command' => $spamclear_cmd,
'active' => 1,
'mins' => int(rand()*60),
'hours' => 0,
'days' => '*',
'months' => '*',
'weekdays' => '*' };
&setup_cron_script($job);
}
}
# create_spam_config_links(&domain)
# Creates links from the global spamasasassin config directory to the domain's
# spam directory.
sub create_spam_config_links
{
local ($d) = @_;
local $defdir;
&require_spam();
if (-d $spam::config{'local_cf'}) {
$defdir = $spam::config{'local_cf'};
}
elsif ($spam::config{'local_cf'} =~ /^(.*)\//) {
$defdir = $1;
}
local $spamdir = "$spam_config_dir/$d->{'id'}";
if ($defdir) {
# Remove any old links
opendir(DIR, $spamdir);
foreach my $f (readdir(DIR)) {
local $p = "$spamdir/$f";
if ($f ne "." && $f ne "..") {
local $lnk = readlink($p);
if ($lnk && !-e $lnk) {
unlink($p);
}
}
}
closedir(DIR);
# Create the new links
opendir(DIR, $defdir);
foreach my $f (readdir(DIR)) {
if ($f ne "." && $f ne "..") {
&symlink_logged("$defdir/$f", "$spamdir/$f");
}
}
closedir(DIR);
}
}
# setup_spam_config_job()
# Create the cron job to link up spamassassin config files, and delete clamav-*
# files in /tmp
sub setup_spam_config_job
{
local $job = &find_cron_script($spamconfig_cron_cmd);
if (!$job) {
# Create, and run for the first time
$job = { 'mins' => int(rand()*60),
'hours' => '*',
'days' => '*',
'months' => '*',
'weekdays' => '*',
'user' => 'root',
'active' => 1,
'command' => $spamconfig_cron_cmd };
&setup_cron_script($job);
}
# And run now, just in case spamassassin was upgraded recently
foreach my $d (grep { $_->{'spam'} } &list_domains()) {
&create_spam_config_links($d);
}
}
# setup_lookup_domain_daemon()
# Create the lookup-domain.pl wrapper script, and setup the lookup-domain-daemon
# background process
sub setup_lookup_domain_daemon
{
&foreign_require("init", "init-lib.pl");
local $pidfile = "$ENV{'WEBMIN_VAR'}/lookup-domain-daemon.pid";
local $helper = &get_api_helper_command();
local $old_init_mode = $init::init_mode;
if (!&init::action_status("lookup-domain")) {
if ($init::init_mode eq 'upstart' || $init::init_mode eq 'systemd') {
# Force use of regular init, to avoid restarting problems
$init::init_mode = 'init';
}
if (!&init::action_status("lookup-domain")) {
&init::enable_at_boot(
"lookup-domain",
"Daemon for quickly looking up Virtualmin servers ".
"from procmail",
"$helper lookup-domain-daemon",
"kill `cat $pidfile`",
undef,
{ 'fork' => 1 });
}
$init::init_mode = $old_init_mode;
}
# Stop and re-start the daemon
my $pid = &check_pid_file($pidfile);
if ($pid) {
kill('KILL', $pid);
sleep(5); # Wait for port to free up
}
&system_logged("$helper lookup-domain-daemon >/dev/null 2>&1 </dev/null &");
}
# delete_lookup_domain_daemon()
# Turn off the background domain-lookup daemon
sub delete_lookup_domain_daemon
{
&foreign_require("init", "init-lib.pl");
&init::disable_at_boot("lookup-domain");
local $pidfile = "$ENV{'WEBMIN_VAR'}/lookup-domain-daemon.pid";
&init::stop_action("lookup-domain");
local $pid = &check_pid_file($pidfile);
if ($pid) {
kill('TERM', $pid);
}
}
# check_lookup_domain_daemon()
# Returns 1 if the domain lookup daemon is running, 0 if not
sub check_lookup_domain_daemon
{
&foreign_require("init", "init-lib.pl");
return &init::action_status("lookup-domain") == 2 ? 1 : 0;
}
# spam_alias_name(&domain)
# Returns the full email address for the spam alias, like spamtrap@foo.com
sub spam_alias_name
{
local ($d) = @_;
return 'spamtrap'.'@'.$d->{'dom'};
}
# ham_alias_name(&domain)
# Returns the full email address for the ham alias, like hamtrap@foo.com
sub ham_alias_name
{
local ($d) = @_;
return 'hamtrap'.'@'.$d->{'dom'};
}
# spam_alias_file(&domain)
# Returns the file in which spam is stored for some domain
sub spam_alias_file
{
local ($d) = @_;
return $spam_alias_dir."/".$d->{'id'};
}
# ham_alias_file(&domain)
# Returns the file in which ham is stored for some domain
sub ham_alias_file
{
local ($d) = @_;
return $ham_alias_dir."/".$d->{'id'};
}
# get_spamtrap_aliases(&domain)
# Returns 1 if spamtrap and hamtrap aliases exist, 0 if not, -1 if cannot be
# created due to clashes. If called in an array context, returns the spam and
# ham aliases too.
sub get_spamtrap_aliases
{
local ($d) = @_;
local (%got, $clash);
foreach my $a (&list_domain_aliases($d)) {
if ($a->{'from'} eq &spam_alias_name($d)) {
# Spam alias .. make sure it goes to the right file
if (@{$a->{'to'}} == 1 &&
$a->{'to'}->[0] &spam_alias_file($d)) {
$got{'spam'} = $a;
}
else {
$clash = 1;
}
}
elsif ($a->{'from'} eq &ham_alias_name($d)) {
# Ham alias
if (@{$a->{'to'}} == 1 &&
$a->{'to'}->[0] &ham_alias_file($d)) {
$got{'ham'} = $a;
}
else {
$clash = 1;
}
}
}
local $rv = $clash ? -1 : $got{'spam'} && $got{'ham'} ? 1 : 0;
return wantarray ? ($rv, $got{'spam'}, $got{'ham'}) : $rv;
}
# setup_spamtrap_aliases(&domain)
# Create aliases in a domain for spamtrap and hamtrap, which deliver to files
# under /var/webmin/spamtrap/$ID. Returns undef on success, or an error message
# on failure.
sub setup_spamtrap_aliases
{
local ($d) = @_;
# Check for aliases already
local ($ok, $spama, $hama) = &get_spamtrap_aliases($d);
if ($ok == 1) {
return $text{'spamtrap_already'};
}
elsif ($ok < 0) {
return &text('spamtrap_clash',
join(", ", $spama ? ( $spama->{'from'} ) : ( ),
$hama ? ( $hama->{'from'} ) : ( )));
}
# Create dirs and empty files
&setup_spamtrap_directories($d);
# Create aliases
local $spamfile = &spam_alias_file($d);
local $hamfile = &ham_alias_file($d);
$spama = { 'from' => &spam_alias_name($d), 'to' => [ $spamfile ] };
$hama = { 'from' => &ham_alias_name($d), 'to' => [ $hamfile ] };
&create_virtuser($spama);
&create_virtuser($hama);
# Setup cron job
&setup_spamtrap_cron();
return undef;
}
# setup_spamtrap_directories(&domain)
# Create the spamtrap directories and mail files for a domain
sub setup_spamtrap_directories
{
local ($d) = @_;
&make_dir($trap_base_dir, 0755) if (!-d $trap_base_dir);
&make_dir($spam_alias_dir, 01777) if (!-d $spam_alias_dir);
&make_dir($ham_alias_dir, 01777) if (!-d $ham_alias_dir);
local $spamfile = &spam_alias_file($d);
local $hamfile = &ham_alias_file($d);
foreach my $f ($spamfile, $hamfile) {
if (!-r $f) {
&open_tempfile(SPAMFILE, ">$f", 0, 1);
&close_tempfile(SPAMFILE);
&set_ownership_permissions(undef, undef, 0666, $f);
}
}
}
# setup_spamtrap_cron()
# Create the cron job that blacklists trapped spam, if needed
sub setup_spamtrap_cron
{
&foreign_require("cron", "cron-lib.pl");
local $job = &find_cron_script($spamtrap_cron_cmd);
if (!$job) {
$job = { 'user' => 'root',
'command' => $spamtrap_cron_cmd,
'active' => 1,
'mins' => int(rand()*60),
'hours' => '*',
'days' => '*',
'weekdays' => '*',
'months' => '*' };
&setup_cron_script($job);
}
}
# delete_spamtrap_aliases(&domain)
# Remove the spamtrap and hamtrap aliases for a domain, and any mail files
sub delete_spamtrap_aliases
{
local ($d) = @_;
# Get the aliases, and remove them
local ($ok, $spama, $hama) = &get_spamtrap_aliases($d);
if ($ok == 1) {
&delete_virtuser($spama);
&delete_virtuser($hama);
}
else {
return $text{'spamtrap_noaliases'};
}
# Delete the mail files
local $spamfile = &spam_alias_file($d);
local $hamfile = &ham_alias_file($d);
&unlink_file($spamfile);
&unlink_file($hamfile);
return undef;
}
# check_spamd_status()
# Checks if spamd is configured and running on this system. Returns 0 if not,
# 1 if yes, or -1 if we can't tell (due to a non-supported OS).
sub check_spamd_status
{
local @pids = grep { $_ != $$ } &find_byname("spamd");
if (@pids) {
# Running already, so we assume everything is cool
return 1;
}
local $spamd = &has_command("spamd") ||
&has_command("/opt/csw/bin/spamd");
if (!$spamd) {
# Not installed
return -1;
}
return 0; # Can be started
}
# enable_spamd()
# Do everything needed to configure and start spamd. May print stuff with the
# standard functions.
sub enable_spamd
{
local $st = &check_spamd_status();
return 1 if ($st == 1 || $st == -1);
# Find init script
&foreign_require("init", "init-lib.pl");
local $init;
foreach my $i ("spamassassin", "spamd", "sa-spamd") {
if (&init::action_status($i)) {
$init = $i;
last;
}
}
$init ||= "virtualmin-spamassassin"; # Fall back to ours
# Create or enable init script
&$first_print(&text('spamd_boot'));
local $spamd = &has_command("spamd") ||
&has_command("/opt/csw/bin/spamd");
&init::enable_at_boot($init, "Start SpamAssassin filter server",
"spamd --pidfile=/var/run/spamd.pid -d",
"kill `cat /var/run/spamd.pid`");
# Update OS-specific config files
local $dfile = "/etc/default/spamassassin";
if (-r $dfile) {
# Init script won't start on Debian without this flag
local %defs;
&lock_file($dfile);
&read_env_file($dfile, \%defs);
if (!$defs{'ENABLED'} || !$defs{'CRON'}) {
$defs{'ENABLED'} = 1;
$defs{'CRON'} = 1;
&write_env_file($dfile, \%defs);
}
&unlock_file($dfile);
}
if ($init::init_mode eq "rc") {
# On FreeBSD, the boot script name differs from the rc.conf entry
&init::enable_rc_script("spamd_enable");
}
&$second_print($text{'setup_done'});
# Start now
&$first_print($text{'spamd_start'});
local ($ok, $out) = &init::start_action($init);
if ($ok) {
&$second_print($text{'setup_done'});
}
else {
&$second_print(&text('spamd_startfailed',
"<tt>".&html_escape($out)."</tt>"));
return 0;
}
return 1;
}
# disable_spamd()
# Turn off spamd now and at boot time
sub disable_spamd
{
local $st = &check_spamd_status();
return 1 if ($st == 0 || $st == -1);
# Find init script
&$first_print(&text('spamd_unboot'));
&foreign_require("init", "init-lib.pl");
local $init;
foreach my $i ("spamassassin", "spamd", "sa-spamd", "virtualmin-spamassassin") {
if (&init::action_status($i)) {
$init = $i;
last;
}
}
if (!$init) {
&$second_print($text{'spamd_unbootact'});
return 0;
}
&init::disable_at_boot($init);
if ($init::init_mode eq "rc") {
# On FreeBSD, the boot script name differs from the rc.conf entry
&init::disable_rc_script("spamd_enable");
}
&$second_print($text{'setup_done'});
# Stop spamd process
&$first_print($text{'spamd_stop'});
local ($ok, $out) = &init::stop_action($init);
if (!$ok) {
&kill_byname_logged('spamd', 'TERM');
}
&$second_print($text{'setup_done'});
return 1;
}
# setup_quota_full_bounce()
# Update /etc/procmailrc with rules after the VIRTUALMIN line to bounce mail
# if quota if full.
sub setup_quota_full_bounce
{
local @recipes = &procmail::get_procmailrc();
# Find existing VIRTUALMIN= and EXITCODE= recipes
my ($virt, $exitcode, $virtafter);
foreach my $r (@recipes) {
if ($r->{'type'} eq '=' && $r->{'action'} =~ /^VIRTUALMIN=/) {
$virt = $r;
}
elsif ($r->{'name'} eq 'EXITCODE') {
$exitcode = $r;
}
elsif ($virt && !$virtafter && $r->{'index'} == $virt->{'index'}+1) {
$virtafter = $r;
}
}
return 0 if (!$virt || $exitcode);
# Create new recipe objects
local $var1 = { 'name' => 'EXITCODE',
'value' => '$?' };
local $testcmd = &has_command("test") || "test";
local $cmd;
if ($gconfig{'os_type'} eq 'solaris') {
$cmd = "sh -c \"$testcmd '\$EXITCODE' = '73'\"";
}
else {
$cmd = "$testcmd \"\$EXITCODE\" = \"73\"";
}
local $var2 = { 'flags' => [ ],
'conds' => [ [ "?", $cmd ] ],
'action' => '/dev/null' };
local $var3 = { 'name' => 'EXITCODE',
'value' => '0' };
# Add after the VIRTUALMIN= line
&procmail::create_recipe_before($var1, $virtafter);
&procmail::create_recipe_before($var2, $virtafter);
&procmail::create_recipe_before($var3, $virtafter);
}
# startstop_spam([&typestatus])
# Returns a hash containing the current status of the spamd server and short
# and long descriptions for the action to switch statuses
sub startstop_spam
{
local ($scanner, $host) = &get_global_spam_client();
if ($scanner ne "spamc" || $host) {
# Spamd isn't being used
return ( );
}
local @pids = grep { $_ != $$ } &find_byname("spamd");
if (@pids) {
return ( { 'status' => 1,
'name' => $text{'index_spamname'},
'desc' => $text{'index_spamstop'},
'restartdesc' => $text{'index_spamrestart'},
'longdesc' => $text{'index_spamstopdesc'} } );
}
else {
return ( { 'status' => 0,
'name' => $text{'index_spamname'},
'desc' => $text{'index_spamstart'},
'longdesc' => $text{'index_spamstartdesc'} } );
}
}
# start_service_spam()
# Attempts to start the spamd server, returning undef on success or any error
# message on failure.
sub start_service_spam
{
&push_all_print();
&set_all_null_print();
local $rv = &enable_spamd();
&pop_all_print();
return $rv ? undef : $text{'spamd_estartmsg'};
}
# stop_service_spam()
# Attempts to stop the spamd server, returning undef on success or any error
# message on failure.
sub stop_service_spam
{
&foreign_require("init", "init-lib.pl");
foreach my $init ("spamassassin", "spamd", "sa-spamd",
"virtualmin-spamassassin") {
if (&init::action_status($init)) {
local ($ok, $out) = &init::stop_action($init);
return $ok ? undef : "<tt>".&html_escape($out)."</tt>";
}
}
local @pids = grep { $_ != $$ } &find_byname("spamd");
if (@pids) {
if (&kill_logged('TERM', @pids)) {
return undef;
}
return &text('spamd_ekillmsg', $!);
}
else {
return $text{'spamd_estopmsg'};
}
}
# obtain_lock_spam(&domain)
# Lock a domain's spamassassin config file and procmail file
sub obtain_lock_spam
{
local ($d) = @_;
return if (!$config{'spam'});
&obtain_lock_anything($d);
if ($d) {
# Lock domain's files
if ($main::got_lock_spam_dom{$d->{'id'}} == 0) {
&require_spam();
&lock_file("$procmail_spam_dir/$d->{'id'}");
&lock_file("$spam_config_dir/$d->{'id'}");
&lock_file("$spam_config_dir/$d->{'id'}/virtualmin.cf");
}
$main::got_lock_spam_dom{$d->{'id'}}++;
}
# Lock master procmail config file
if ($main::get_lock_spam == 0) {
&require_spam();
&lock_file($procmail::procmailrc);
&lock_file($spamclear_file);
}
$main::get_lock_spam++;
}
# release_lock_spam(&domain)
# Un-lock a domain's spamassassin config file and procmail file
sub release_lock_spam
{
local ($d) = @_;
return if (!$config{'spam'});
if ($d) {
# Unlock domain's files
if ($main::got_lock_spam_dom{$d->{'id'}} == 1) {
&require_spam();
&unlock_file("$procmail_spam_dir/$d->{'id'}");
&unlock_file("$spam_config_dir/$d->{'id'}");
&unlock_file("$spam_config_dir/$d->{'id'}/virtualmin.cf");
}
$main::got_lock_spam_dom{$d->{'id'}}--
if ($main::got_lock_spam_dom{$d->{'id'}});
}
# Unlock only master procmail config file
if ($main::get_lock_spam == 1) {
&require_spam();
&unlock_file($procmail::procmailrc);
&unlock_file($spamclear_file);
}
$main::got_lock_spam-- if ($main::got_lock_spam);
&release_lock_anything($d);
}
# obtain_lock_spam_all()
# Lock the spamassassin and procmail config files for all domains
sub obtain_lock_spam_all
{
foreach my $d (grep { $_->{'spam'} } &list_domains()) {
&obtain_lock_spam($d);
}
}
# release_lock_spam_all()
# Un-lock the spamassassin and procmail config files for all domains
sub release_lock_spam_all
{
foreach my $d (grep { $_->{'spam'} } &list_domains()) {
&release_lock_spam($d);
}
}
$done_feature_script{'spam'} = 1;
1;
�
y���~�or�5�J�={��E��e�u磝Qk��ᯘG{�?���+�]������ן�?w�M3X^��歌>{�7پ�K�>�o��n�\����j�y��R���g/�=��f�O��r�o��NV�v��~�Y+��NG�u���Ý����HW��y�w�[eQ�ʨ�S�b��>������>�}G�mx[o[<��{���Ϯ_��qF���vMIEND�B`�