Server IP : 195.201.23.43 / Your IP : 13.59.203.127 Web Server : Apache System : Linux webserver2.vercom.be 5.4.0-192-generic #212-Ubuntu SMP Fri Jul 5 09:47:39 UTC 2024 x86_64 User : kdecoratie ( 1041) PHP Version : 7.1.33-63+ubuntu20.04.1+deb.sury.org+1 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals, MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /usr/share/webmin/virtual-server/ |
Upload File : |
# 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"); &foreign_require("spam"); } 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 { my ($d) = @_; &$first_print($text{'setup_spam'}); &require_spam(); &foreign_require("cron"); # 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/$d->{'id'}"; &make_dir($spamdir, 0750); &set_ownership_permissions($d->{'uid'}, $d->{'gid'}, 0750, $spamdir); &obtain_lock_spam($d); &obtain_lock_cron($d); # 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); } } # 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($d); # Create recipes to call spamassassin local $spamrc = "$procmail_spam_dir/$d->{'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($d); # 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($d->{'uid'}, $d->{'gid'}, 0755, "$spamdir/virtualmin.cf"); # Whitelist all domain mailboxes if ($config{'spam_white'}) { $d->{'spam_white'} = 1; &update_spam_whitelist($d); } # Setup automatic spam clearing my $opts = { }; my ($cmode, $cnum) = split(/\s+/, $tmpl->{'spamclear'}); if ($cmode eq 'days' || $cmode eq 'size') { $opts->{$cmode} = $cnum; } my ($tmode, $tnum) = split(/\s+/, $tmpl->{'trashclear'}); if ($tmode eq 'days' || $tmode eq 'size') { $opts->{'trash'.$tmode} = $tnum; } if (keys %$opts) { &save_domain_spam_autoclear($d, $opts); } # Setup spamtrap aliases, if requested if ($tmpl->{'spamtrap'} eq 'yes') { &obtain_lock_mail($d); &setup_spamtrap_aliases($d); &release_lock_mail($d); } &release_lock_cron($d); &release_lock_spam($d); &$second_print($text{'setup_done'}); return 1; } # spamassassin_client_command(&domain, [client]) # Returns the command for calling spamassassin in some domain, plus args sub spamassassin_client_command { my ($d, $client) = @_; my $spamid = $d->{'parent'} || $d->{'id'}; $client ||= $config{'spam_client'}; my $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 { my ($d) = @_; my $spamrc = "$procmail_spam_dir/$d->{'id'}"; return &text('validate_espamprocmail', "<tt>$spamrc</tt>") if (!-r $spamrc); my $spamdir = "$spam_config_dir/$d->{'id'}"; return &text('validate_espamconfig', "<tt>$spamdir</tt>") if (!-d $spamdir); &require_spam(); my @recs = &procmail::parse_procmail_file($spamrc); my $cmd = $spam::config{'spamassassin'}; my $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" : "/var/mail/\$LOGNAME"; 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 { my ($d, $oldd) = @_; return 1; } # delete_spam(&domain) # Just remove the domain's procmail config file sub delete_spam { my ($d) = @_; &$first_print($d->{'virus'} ? $text{'delete_spamvirus'} : $text{'delete_spam'}); &obtain_lock_spam($d); local $spamrc = "$procmail_spam_dir/$d->{'id'}"; &unlink_logged($spamrc); local $spamdir = "$spam_config_dir/$d->{'id'}"; &system_logged("rm -rf ".quotemeta($spamdir)); &clear_lookup_domain_cache($d); &save_domain_spam_autoclear($d, undef); &release_lock_spam($d); &$second_print($text{'setup_done'}); return 1; } # 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, $opts, $homefmt, $increment, $asd, $allopts, $key) = @_; local $compression = $allopts->{'dir'}->{'compression'}; &$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(&make_archive_command( $compression, $spamdir, $temp, ".")." 2>&1"); ©_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 { local ($d, $file) = @_; &$first_print($text{'restore_spamcp'}); &obtain_lock_spam($d); local $spamrc = "$procmail_spam_dir/$d->{'id'}"; local $spamdir = "$spam_config_dir/$d->{'id'}"; ©_source_dest($file, $spamrc); &execute_command(&make_unarchive_command($spamdir, $file."_cf")." 2>&1"); # Fix any incorrect /etc/webmin references my $lref = &read_file_lines($spamrc); foreach my $l (@$lref) { if ($l =~ /^(.*\s+)(\S+\/clam-wrapper.pl)(\s+.*)$/) { $l = $1.$clam_wrapper_cmd.$3; } if ($l =~ /^(.*\s+--siteconfigpath\s+)(\S+\/virtual-server\/spam\/\S+)$/) { $l = $1.$spamdir; } } &flush_file_lines($spamrc); if (-r $file."_auto") { # Replace auto-clearing setting &save_domain_spam_autoclear($d, undef); local %auto; &read_file($file."_auto", \%auto); if (%auto) { &save_domain_spam_autoclear($d, \%auto); } } # If spamtrap aliases exist, make sure the files and cron job do local $st = &get_spamtrap_aliases($d); if ($st > 0) { &setup_spamtrap_directories($d); &setup_spamtrap_cron(); } # Re-create all spam links &create_spam_config_links($d); &release_lock_spam($d); &$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' => 'mail', }); } 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+\$LOGNAME/) { # Fix call to lookup-domain to return correct exit code my $flags = $1; $flags =~ s/\s+--exitcode\s+\d+//; $flags .= " --exitcode $code"; $r->{'action'} = "VIRTUALMIN=|$domain_lookup_cmd".$flags." \$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"; } # save_lookup_domain_port(port) # Update the procmail config and the lookup domain server port sub save_lookup_domain_port { my ($port) = @_; # Update the procmail config &require_spam(); &lock_file($procmail::procmailrc); my @recipes = &procmail::get_procmailrc(); foreach my $r (@recipes) { if ($r->{'action'} =~ /\Q$domain_lookup_cmd\E(.*?)\s+\$LOGNAME/) { my $flags = $1; $flags =~ s/\s+--port\s+\d+//; if ($port) { $flags .= " --port $port"; } $r->{'action'} = "VIRTUALMIN=|$domain_lookup_cmd".$flags." \$LOGNAME"; &procmail::modify_recipe($r); } } &unlock_file($procmail::procmailrc); # Update the server &lock_file($module_config_file); $config{'lookup_domain_port'} = $port; &save_module_config(); &unlock_file($module_config_file); &foreign_require("init"); &init::restart_action("lookup-domain"); } # 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)) ], ])); # Default trash clearing mode local ($cmode, $cnum) = split(/\s+/, $tmpl->{'trashclear'}); local $cdays = $cmode eq 'days' ? $cnum : undef; local $csize = $cmode eq 'size' ? $cnum : undef; print &ui_table_row(&hlink($text{'tmpl_trashclear'}, 'template_trashclear'), &ui_radio("trashclear", $cmode, [ $tmpl->{'default'} ? ( ) : ( [ "", $text{'default'}."<br>" ] ), [ "none", $text{'no'}."<br>" ], [ "days", &text('spam_cleardays', &ui_textbox("trashclear_days", $cdays, 5))."<br>"], [ "size", &text('spam_clearsize', &ui_bytesbox("trashclear_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 spam 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 clearing option if ($in{'trashclear'} eq '') { $tmpl->{'trashclear'} = ''; } elsif ($in{'trashclear'} eq 'none') { $tmpl->{'trashclear'} = 'none'; } elsif ($in{'trashclear'} eq 'days') { $in{'trashclear_days'} =~ /^\d+$/ && $in{'trashclear_days'} > 0 || &error($text{'spam_edays'}); $tmpl->{'trashclear'} = 'days '.$in{'trashclear_days'}; } elsif ($in{'trashclear'} eq 'size') { $in{'trashclear_size'} =~ /^\d+$/ && $in{'trashclear_size'} > 0 || &error($text{'spam_esize'}); $tmpl->{'trashclear'} = 'size '.($in{'trashclear_size'}* $in{'trashclear_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"); local $pidfile = "$ENV{'WEBMIN_VAR'}/lookup-domain-daemon.pid"; local $helper = &get_api_helper_command(); if (!&init::action_status("lookup-domain")) { # Need to create and start the action &init::enable_at_boot( "lookup-domain", "Daemon for quickly looking up Virtualmin servers from procmail", "$helper lookup-domain-daemon", "@{[&has_command('kill')]} `cat $pidfile`", undef, { 'fork' => 1, 'pidfie' => $pidfile }); &init::start_action("lookup-domain"); } else { # Stop and re-start the daemon to pick up new version my ($ok) = &init::stop_action("lookup-domain"); if ($ok) { sleep(5); # Wait for port to free up &init::start_action("lookup-domain"); } } } # delete_lookup_domain_daemon() # Turn off the background domain-lookup daemon sub delete_lookup_domain_daemon { &foreign_require("init"); &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"); 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"); 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"); 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"); } if (&init::action_status("spamassassin-maintenance.timer")) { &init::enable_at_boot("spamassassin-maintenance.timer"); } &$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"); 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"); } if (&init::action_status("spamassassin-maintenance.timer")) { &init::disable_at_boot("spamassassin-maintenance.timer"); } &$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"); 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;Private