#!/usr/bin/perl ############################################################################# # oSpam - Provides a server wide anti-spam solution for qmail + vmailmgr # # based on ifspam, perl and SpamAssassin - # # # # Copyright (C) 2003 Olivier Müller # # # # $Id: $ # # $Source: $ # # # # This program is free software; you can redistribute it and/or # # modify it under the terms of the GNU General Public License # # as published by the Free Software Foundation; either version 2 # # of the License, or (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program; if not, write to the Free Software Foundation, # # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################# use DBI; use strict; ################################################# # SETUP: my $debug = 1; my $logfile = "/usr/local/nospam/ospam.log"; my $debugfile = "/usr/local/nospam/debug.log"; # emails: my $debug_bcc_mail = 'ospam-test@8304.ch'; # keep empty after testing my $admin_mail = 'ospam-admin@8304.ch'; my $mail_sender = 'oSpam system '; # database: my $db_username = "nospam"; my $db_password = "********"; my $db_hostname = "localhost"; my $db_database = "nospam"; my $tb_userpref = "userpref"; # SA sql table my $tb_dotqmail = "dotqmail"; # ospam data # file & cmd path: my $cmd_preline = "/var/qmail/bin/preline"; my $cmd_spamc = "/usr/bin/spamc"; my $cmd_vdeliver = "/usr/local/bin/vdeliver"; my $cmd_filepipe = "/usr/local/nospam/filepipe"; my $cmd_ifspamh = "/usr/local/nospam/ifspamh"; my $spamc_options = "-f"; my $cmd_md5sum = "/usr/bin/md5sum"; my $cmd_chown = "/bin/chown"; my $cmd_chmod = "/bin/chmod"; my $cmd_sendmail = "/usr/sbin/sendmail"; my $sendmail_opt = "-oem -oi -t"; # qmail setup: my $cfg_virtualdomains = "/var/qmail/control/virtualdomains"; my $cfg_rcpthosts = "/var/qmail/control/rcpthosts"; my $dot_qmail_prefix = ".qmail-"; # internal values my $version = 1; # integer my $internal_version = 1000; # incrementing this number will force re-generation of all .qmail-files ################################################# if (!-f $cmd_preline) { die "preline missing"; } if (!-f $cmd_spamc) { die "spamc missing"; } if (!-f $cmd_vdeliver) { die "vdeliver missing"; } if (!-f $cmd_filepipe) { die "filepipe missing"; } if (!-f $cmd_ifspamh) { die "ifspamh missing"; } if (!-f $cmd_md5sum) { die "md5sum missing"; } if (!-f $cmd_chown) { die "chown missing"; } if (!-f $cmd_chmod) { die "chmod missing"; } if (!-f $cmd_sendmail) { die "sendmail missing"; } if (!-f $cfg_virtualdomains) { die "virtualdomains missing"; } &log("--------------------- oSpam $version.$internal_version started -------------------------"); ################################################# # dotqmail file templates: my $t_scan_only = <connect("DBI:mysql:$db_database:$db_hostname","$db_username","$db_password") or die "mysql connection failed"; # get list of new/changed/deleted spam accounts $query = $db->prepare("select $tb_userpref.username as username from $tb_userpref left join $tb_dotqmail on $tb_userpref.username=$tb_dotqmail.username where $tb_dotqmail.username IS NULL AND preference LIKE 'spam_enabled' order by username"); $query->execute or die "sql query error"; $i = 0; $msg = ""; while($data = $query->fetchrow_hashref) { $added[$i++] = $$data{"username"}; $msg .= $$data{"username"} . " "; } &debug("ADDED: $msg"); $query = $db->prepare("select $tb_dotqmail.username as username,md5sum,filename,active,error,lastprefchange from $tb_dotqmail left join $tb_userpref on $tb_userpref.username=$tb_dotqmail.username where $tb_userpref.username IS NULL AND $tb_dotqmail.error NOT LIKE '1' order by username"); $query->execute or die "sql query error"; $i = 0; $msg = ""; while($data = $query->fetchrow_hashref) { $removed[$i++] = $$data{"username"}; $msg .= $$data{"username"} . " "; $removed_data{($$data{"username"})} = { 'md5sum' => $$data{"md5sum"}, 'filename' => $$data{"filename"}, 'lastprefchange' => $$data{"lastprefchange"}, 'active' => $$data{"active"}, 'error' => $$data{"error"} }; } &debug("REMOVED: $msg"); $query = $db->prepare("select $tb_userpref.username as username,$tb_dotqmail.md5sum as md5sum,$tb_dotqmail.filename as filename,$tb_dotqmail.lastprefchange as lastprefchange,$tb_dotqmail.active as active,$tb_dotqmail.error as error from $tb_userpref left join $tb_dotqmail on $tb_userpref.username=$tb_dotqmail.username WHERE preference LIKE 'spam_enabled' AND $tb_userpref.ts NOT LIKE $tb_dotqmail.lastprefchange AND $tb_dotqmail.error NOT LIKE '1' ORDER BY username"); $query->execute or die "sql query error"; $i = 0; $msg = ""; while($data = $query->fetchrow_hashref) { $changed[$i++] = $$data{"username"}; $msg .= $$data{"username"} . " "; $changed_data{($$data{"username"})} = { 'md5sum' => $$data{"md5sum"}, 'filename' => $$data{"filename"}, 'lastprefchange' => $$data{"lastprefchange"}, 'active' => $$data{"active"}, 'error' => $$data{"error"} }; } &debug("CHANGED: $msg"); $query = $db->prepare("select $tb_userpref.username as username,$tb_dotqmail.md5sum as md5sum,$tb_dotqmail.filename as filename,$tb_dotqmail.lastprefchange as lastprefchange,$tb_dotqmail.active as active,$tb_dotqmail.error as error from $tb_userpref left join $tb_dotqmail on $tb_userpref.username=$tb_dotqmail.username where preference LIKE 'spam_enabled' AND $tb_userpref.ts LIKE $tb_dotqmail.lastprefchange AND $tb_dotqmail.error NOT LIKE '1' ORDER BY username"); $query->execute or die "sql query error"; $i = 0; $msg = ""; while($data = $query->fetchrow_hashref) { $notchanged[$i++] = $$data{"username"}; $msg .= $$data{"username"} . " "; $notchanged_data{($$data{"username"})} = { 'md5sum' => $$data{"md5sum"}, 'filename' => $$data{"filename"}, 'lastprefchange' => $$data{"lastprefchange"}, 'active' => $$data{"active"}, 'error' => $$data{"error"} }; } &debug("NOTCHANGED: $msg"); ################################################# my %virtualdomains_uid = (); my %virtualdomains_homedir = (); my $domain = ""; my $uid = ""; # load domains list -> to fetch home directory based on mail domain open(VDOM, $cfg_virtualdomains) || die("Error: cannot open $cfg_virtualdomains"); while (my $line = ) { chomp $line; ($domain,$uid) = split(/:/, $line); $virtualdomains_uid{$domain} = $uid; $virtualdomains_homedir{$domain} = ((getpwnam($uid))[7]) ; } close(VDOM); ############## # # handle new accounts # &debug("ADDED - handle new accounts"); foreach $email (@added) { $username = ""; $domain = ""; $homedir = ""; $dotfile = ""; $md5sum = ""; if ($email =~ /^(.*)@(.*)$/) { $username = $1; $username_encoded = $1; $domain = $2; $username_encoded =~ s/\./\:/g; } else { next; &log("ERROR: '$email' not a mail address!"); } $uid = $virtualdomains_uid{$domain}; $homedir = $virtualdomains_homedir{$domain}; $file = $homedir . "/" . $dot_qmail_prefix . $username_encoded; if (!$homedir || !$uid) { &log("ERROR: no homedir '$homedir' or uid '$uid' for email = '$email' - domain = $domain - username = $username - encoded = $username_encoded "); next; } &debug("$email -> $file in $homedir [$uid]"); # retrieve SA infos ($spam_enabled, $spam_ts, $spam_trash, $spam_fwd) = &spam_getinfo($email); &debug("spam_enabled: $spam_enabled - spam_trash = $spam_trash - spam_fwd = $spam_fwd"); # create the dot qmail file $error = 0; if ($spam_enabled && !(-f $file)) { # only create a file if user really turned the system on $template = &gen_template($email, $uid, $spam_trash, $spam_fwd, $file); &debug("---[dotqmail]----\n$template---[/dotqmail]----\n"); if (open("DOTQMAIL", ">$file")) { print DOTQMAIL $template; close (DOTQMAIL); system($cmd_chown, $uid, $file); system($cmd_chmod, "0600", $file); $md5sum = substr(`$cmd_md5sum $file`,0,32); &debug("file created: $file - md5sum = $md5sum"); # update db $history = "Created and activated on " . localtime(); $query = "INSERT INTO $tb_dotqmail (username,filename,lastprefchange,md5sum,active,history) "; $query .= "VALUES ('$email','$file','$spam_ts','$md5sum','1','$history')"; $result = $db->do($query) or do { &log("ERR: $DBI::errstr"); }; # send mail confirmation &send_mail_confirmation($email, "Spam system activated!", "", $spam_enabled, $spam_trash, $spam_fwd); &log("ospam activated for account $email"); } else { $error = 1; $history = "Error on creation " . localtime(); $msg = "Couldn't create the $file file: permission problems?\n"; } } elsif ($spam_enabled && (-f $file)) { $error = 1; $history = "Error on creation: .qmail- file already exists " . localtime(); } else { # spam_enabled = 0, but we still update our DB for consistancy. $history = "Creating inactive account on " . localtime(); $query = "INSERT INTO $tb_dotqmail (username,filename,lastprefchange,md5sum,active,error,history) "; $query .= "VALUES ('$email','$file','$spam_ts','','0','0','$history')"; $result = $db->do($query) or do { &log("ERR: $DBI::errstr"); }; } if ($error) { &log("ERROR for added account $email, activation failed: $msg"); $query = "INSERT INTO $tb_dotqmail (username,filename,lastprefchange,md5sum,active,error,history) "; $query .= "VALUES ('$email','$file','$spam_ts','','0','1','$history')"; $result = $db->do($query) or do { &log("ERR: $DBI::errstr"); }; &send_mail_confirmation($email, "ERROR: System activation failed!", "$msg\n\nPlease contact your mailserver administrator.", $spam_enabled, $spam_trash, $spam_fwd); } } ############## # # handle deleted accounts (= accounts who are not in userpref table anymore: occurs on mail address deletion) # &debug("REMOVED - handle deleted accounts"); foreach $email (@removed) { $file = $removed_data{$email}->{'filename'}; $md5sum = $removed_data{$email}->{'md5sum'}; if ($removed_data{$email}->{'active'}) { &debug("$email -> $file: "); $error = 0; if (-f $file) { # check if md5sum maches $md5sum_check = substr(`$cmd_md5sum $file`,0,32); if ($md5sum eq $md5sum_check) { &debug("md5sum matches! ($md5sum)"); } else { &debug("md5sum differnce: DB $md5sum =! FILE $md5sum_check)"); $msg = "active account md5 mismatch: differnce: DB $md5sum =! FILE $md5sum_check for $email\n"; &log("ERROR: $msg"); $error = 1; } } else { $msg = "active account: file $file deleted?! $email\n"; &log($msg); } if ($error) { $query = $db->prepare("SELECT history FROM $tb_dotqmail WHERE username LIKE '$email'"); $query->execute or die "sql query error"; if (@datarow = $query->fetchrow_array) { $history = $datarow[0]; } $history .= "\nError: removed, " . $msg; $query = "UPDATE $tb_dotqmail SET history='$history',error='1',active='0' WHERE username LIKE '$email'"; $result = $db->do($query) or do { &log("ERR: $DBI::errstr"); }; } else { unlink($file); $query = "DELETE FROM $tb_dotqmail WHERE username LIKE '$email'"; $result = $db->do($query) or do { &log("ERR: $DBI::errstr"); }; &log("removed account for $email"); } } } ############## # # handle notchanged accounts # # -> check if file still there, md5, timestamp &debug("NOT CHANGED - handle notchanged accounts"); foreach $email (@notchanged) { $file = $notchanged_data{$email}->{'filename'}; $md5sum = $notchanged_data{$email}->{'md5sum'}; if ($notchanged_data{$email}->{'active'}) { &debug("$email -> $file: "); $error = 0; if (-f $file) { # check if md5sum maches $md5sum_check = substr(`$cmd_md5sum $file`,0,32); if ($md5sum eq $md5sum_check) { &debug("md5sum matches! ($md5sum)"); } else { &debug("md5sum differnce: DB $md5sum =! FILE $md5sum_check)"); $msg = "active account md5 mismatch: differnce: DB $md5sum =! FILE $md5sum_check for $email\n"; &log("ERROR: $msg"); $error = 1; } } else { $error = 1; $msg = "active account: file $file deleted?! $email\n"; &log("ERROR: $msg"); } if ($error) { $query = $db->prepare("SELECT history FROM $tb_dotqmail WHERE username LIKE '$email'"); $query->execute or die "sql query error"; if (@datarow = $query->fetchrow_array) { $history = $datarow[0]; } $history .= "\nError: not_changed, " . $msg; $query = "UPDATE $tb_dotqmail SET history='$history',error='1',active='0' WHERE username LIKE '$email'"; $result = $db->do($query) or do { &log("ERR: $DBI::errstr"); }; } } } ############## # # handle changed accounts # # -> check if file still there, md5, timestamp &debug("CHANGED - handle changed accounts"); foreach $email (@changed) { $file = $changed_data{$email}->{'filename'}; $md5sum = $changed_data{$email}->{'md5sum'}; $active = $changed_data{$email}->{'active'}; &debug("$email -> $file: "); if ($active) { $error = 0; if (-f $file) { # check if md5sum maches $md5sum_check = substr(`$cmd_md5sum $file`,0,32); if ($md5sum eq $md5sum_check) { &debug("md5sum matches! ($md5sum)"); } else { &debug("md5sum differnce: DB $md5sum =! FILE $md5sum_check)"); $msg = "active account md5 mismatch: differnce: DB $md5sum =! FILE $md5sum_check for $email\n"; &log("ERROR: $msg"); $error = 1; } } else { $error = 1; $msg = "active account: file $file deleted?! $email\n"; &log("ERROR: $msg"); } if ($error) { $query = $db->prepare("SELECT history FROM $tb_dotqmail WHERE username LIKE '$email'"); $query->execute or die "sql query error"; if (@datarow = $query->fetchrow_array) { $history = $datarow[0]; } $history .= "\nError: changed, " . $msg; $query = "UPDATE $tb_dotqmail SET history='$history',error='1',active='0' WHERE username LIKE '$email'"; $result = $db->do($query) or do { &log("ERR: $DBI::errstr"); }; } } if (!$error) { # no error, let's update the file $md5sum = ""; if ($email =~ /^(.*)@(.*)$/) { $username = $1; $username_encoded = $1; $domain = $2; $username_encoded =~ s/\./\:/g; } else { next; } $uid = $virtualdomains_uid{$domain}; $homedir = $virtualdomains_homedir{$domain}; $file = $homedir . "/" . $dot_qmail_prefix . $username_encoded; &debug("$email -> $file in $homedir [$uid]"); # retrieve SA infos ($spam_enabled, $spam_ts, $spam_trash, $spam_fwd) = &spam_getinfo($email); &debug("spam_enabled: $spam_enabled - spam_trash = $spam_trash - spam_fwd = $spam_fwd"); # re-create the dot qmail file if ($spam_enabled) { # only create a file if user really turned the system on $template = &gen_template($email, $uid, $spam_trash, $spam_fwd, $file); &debug("---[dotqmail]----\n$template---[/dotqmail]----\n"); if (open("DOTQMAIL", ">$file")) { print DOTQMAIL $template; close (DOTQMAIL); system($cmd_chown, $uid, $file); system($cmd_chmod, "0600", $file); $md5sum = substr(`$cmd_md5sum $file`,0,32); &debug("file updated: $file - md5sum = $md5sum"); # update db $query = $db->prepare("SELECT history FROM $tb_dotqmail WHERE username LIKE '$email'"); $query->execute or die "sql query error"; if (@datarow = $query->fetchrow_array) { $history = $datarow[0]; } $history .= "\nSettings updated on " . localtime(); $query = "UPDATE $tb_dotqmail SET md5sum='$md5sum',history='$history',filename='$file',lastprefchange='$spam_ts',error='0',active='1' WHERE username LIKE '$email'"; $result = $db->do($query) or do { &log("ERR: $DBI::errstr"); }; &log("updated settings for account $email"); # send mail confirmation &send_mail_confirmation($email, "Settings updated!", "", $spam_enabled, $spam_trash, $spam_fwd); } else { $query = $db->prepare("SELECT history FROM $tb_dotqmail WHERE username LIKE '$email'"); $query->execute or die "sql query error"; if (@datarow = $query->fetchrow_array) { $history = $datarow[0]; } $history .= "\nsettings update FAILED on " . localtime(); $query = "UPDATE $tb_dotqmail SET history='$history',error='1',active='0' WHERE username LIKE '$email'"; $result = $db->do($query) or do { &log("ERR: $DBI::errstr"); }; &log("ERROR on change: couldn't create $file"); &send_mail_confirmation($email, "ERROR: System activation failed!", "-> couldn't create the $file file!\n(maybe a file or directory permissions problem ?)\nPlease contact your mailserver administrator.", $spam_enabled, $spam_trash, $spam_fwd); } } else { # not enables: remove the file! unlink($file); &log("removed $file for $email - turned off SA"); # update db $query = $db->prepare("SELECT history FROM $tb_dotqmail WHERE username LIKE '$email'"); $query->execute or die "sql query error"; if (@datarow = $query->fetchrow_array) { $history = $datarow[0]; } $history .= "\noSpam stopped (removed .qmail file) on " . localtime(); $query = "UPDATE $tb_dotqmail SET md5sum='0',history='$history',filename='$file',lastprefchange='$spam_ts',error='0',active='0' WHERE username LIKE '$email'"; $result = $db->do($query) or do { &log("ERR: $DBI::errstr"); }; # send mail confirmation &send_mail_confirmation($email, "Spam system stopped!", "", 0,0,0); } } } &log("end."); exit(); ########################################################################## # Subroutines ########################################################################## sub log { my $timestamp = localtime(); my $logaction = shift; unless ( ($logfile eq 'no') || ( -l "$logfile" ) ) { if (open (LOGFILE,">>$logfile")) { print LOGFILE "$timestamp\t$logaction\n"; close (LOGFILE); } } &debug($logaction); } ############## sub debug { my $timestamp = localtime(); my $logaction = shift; unless ( ($debugfile eq 'no') || ( -l "$debugfile" ) ) { if (open (LOGFILE,">>$debugfile")) { print LOGFILE "$timestamp\t$logaction\n"; close (LOGFILE); } } } ############## sub spam_getinfo { my $email = shift; my $spam_trash = 0; my $spam_fwd = ""; my $spam_enabled = 0; my $spam_ts = 0; $query = $db->prepare("SELECT value,ts FROM $tb_userpref WHERE preference LIKE 'spam_enabled' AND username LIKE '$email'"); $query->execute or die "sql query error"; if (@datarow = $query->fetchrow_array) { $spam_enabled = $datarow[0]; $spam_ts = $datarow[1]; } $query = $db->prepare("SELECT value FROM $tb_userpref WHERE preference LIKE 'spam_trash' AND username LIKE '$email'"); $query->execute or die "sql query error"; if (@datarow = $query->fetchrow_array) { $spam_trash = $datarow[0]; } $query = $db->prepare("SELECT value FROM $tb_userpref WHERE preference LIKE 'spam_fwd' AND username LIKE '$email'"); $query->execute or die "sql query error"; if (@datarow = $query->fetchrow_array) { $spam_fwd = $datarow[0]; } return ($spam_enabled, $spam_ts, $spam_trash, $spam_fwd); } ######### sub gen_template() { my $email = shift; my $uid = shift; my $spam_trash = shift; my $spam_fwd = shift; my $file = shift; $template = "# =================================================================================\n"; $template .= "# $file generated on " . localtime() . "\n"; $template .= "# by oSpam version $version.$internal_version for <$email> [$uid]\n"; $template .= "# ___ DO NOT EDIT ___ -== http://ospam.omnis.ch/ ==- \n"; $template .= "# \n"; if ($spam_trash && $spam_fwd eq "") { $template .= $t_scan_and_trash; } elsif ($spam_trash && $spam_fwd ne "") { $template .= $t_scan_and_fwdonly; } elsif (!$spam_trash && $spam_fwd eq "") { $template .= $t_scan_only; } elsif (!$spam_trash && $spam_fwd ne "") { $template .= $t_scan_and_fwd_and_deliver; } else { $template .= $t_scan_only; } # shouldn't happen, but we all know Murphy :) $template .= "# \n"; $template .= "# =================================================================================\n"; # parse template $template =~ s/___USERADDR___/$email/sg; $template =~ s/___FWDADDR___/$spam_fwd/sg; return $template; } ######## sub send_mail_confirmation { my $email = shift; my $subject = shift; my $body = shift; my $spam_enabled = shift; my $spam_trash = shift; my $spam_fwd = shift; &debug("sending mail to '$email' with subject '$subject'"); if (open(SENDMAIL, "| $cmd_sendmail $sendmail_opt 1>&2")) { print SENDMAIL "To: $email\n"; print SENDMAIL "From: $mail_sender\n"; print SENDMAIL "Bcc: $debug_bcc_mail\n" if $debug_bcc_mail; print SENDMAIL "Subject: [oSpam] $subject\n"; print SENDMAIL "\n"; print SENDMAIL "$body\n\n" if $body; if ($spam_enabled == 1) { print SENDMAIL "Selected anti-spam settings:\n"; print SENDMAIL "- mails are scanned against spam\n" if $spam_enabled; print SENDMAIL "- spams will not be delivered to your account\n" if $spam_trash; print SENDMAIL "- spams will be delivered 'tagged' to your account\n" if !$spam_trash; print SENDMAIL "- all spams are forwarded to an external account: $spam_fwd\n" if $spam_fwd; } elsif ($spam_enabled == 0) { print SENDMAIL "From now all mails are going to be delivered without\n"; print SENDMAIL "going through the anti-spam system. \n"; } print SENDMAIL "\n"; print SENDMAIL "Thanks for trying oSpam :) \n"; print SENDMAIL "--sysadmin\n"; print SENDMAIL "\n"; close(SENDMAIL); } else { &log("ERROR: couldn't send mail to $email"); } return; } ##########################################################################