# # MailScanner - SMTP E-Mail Virus Scanner # Copyright (C) 2002 Julian Field # # $Id: MyExample.pm,v 1.1.2.1 2004/03/23 09:23:43 jkf Exp $ # # The author, Julian Field, can be contacted by email at # mailscanner@ecs.soton.ac.uk # or by paper mail at # Julian Field # Electronics & Computer Science # University of Southampton # Southampton # SO17 1BJ # United Kingdom # package MailScanner::CustomConfig; use FileHandle; use strict 'vars'; use strict 'refs'; no strict 'subs'; # Allow bare words for parameter %'s use vars qw($VERSION); ### The package version, both in 1.23 style *and* usable by MakeMaker: $VERSION = substr q$Revision: 1.1.2.1 $, 10; my $Debug = 0; # Set to 1 to enable debug output to STDERR my $tmpfilename = "/tmp/MailScanner.LastSpam.$$.conf"; # Temp MS.conf file my %modtime = (); # Time domain list magic word file was last changed my %filename = (); # Map Config option to magic word file my %magicwords = {}; # Map Config option --> domains --> magic words my %rulesetmodtime = (); # Map option name to last file modification time my %rulesetfilename = (); # Map Config option to ruleset file my %ruleset = {}; # Map Config option --> domains --> 1 my %ReadConfDone = (); # Have we done the ReadConf for this option # Do all the setup for any given configuration option with its filename sub SetupMagicOption { my($option, $filename) = @_; $option = lc($option); # Just in case! print STDERR "Setting everything up for $option from $filename\n" if $Debug; # Set everything up for this configuration option $filename{$option} = $filename; $modtime{$option} = (stat($filename))[9]; $magicwords{$option} = (); MailScanner::Log::WarnLog("Reading magic word list for $option failed") unless CreateMagicWord($option); } # Read and store the magic word list for a given MailScanner.conf option sub CreateMagicWord { my($optionname) = @_; my($domain2word, $filename); $optionname = lc($optionname); # Make sure upper/lower case is consistent # Read in the magic word list for this MailScanner.conf option MailScanner::Log::InfoLog("Initialising $optionname"); $filename = $filename{$optionname}; $domain2word = ReadWordList($filename); print STDERR "domain2word = $domain2word\n" if $Debug; return undef unless $domain2word; $magicwords{$optionname} = $domain2word; $filename{$optionname} = $filename; PrintWordList($optionname) if $Debug; return 1; # Success } # Print out the contents of an option's magic word table sub PrintWordList { my($option) = @_; my($key, $value, $hash); print STDERR "\nThe magic word table for $option is:\n"; $hash = $magicwords{$option}; while(($key, $value) = each %$hash) { print STDERR "$key\t$value\n"; } } # Read the magic word table from a given filename and return a ref to it sub ReadWordList { my($filename) = @_; my($handle, %magic, $domain, $magic, $counter); $handle = new FileHandle; unless ($handle->open("<$filename")) { print STDERR "Could not open $filename for reading magic word list\n" if $Debug; MailScanner::Log::WarnLog("Could not read magic word list $filename"); return undef; } %magic = (); $counter = 0; while(<$handle>) { # Handle comments, leading/trailing space, blank lines and other rubbish chomp; print STDERR "Read \"$_\"\n" if $Debug; s/\#.*$//; s/^\s+//; s/\s+$//; next if $_ eq ""; # Get the interesting bits ($domain, $magic) = split(" ", $_, 2); $domain = lc($domain); $magic = lc($magic); # Check and store it print STDERR "Read magic \"$magic\" for domain \"$domain\"\n" if $Debug; $magic{"$domain"} = $magic; $counter++; } $handle->close; MailScanner::Log::InfoLog("Read $counter magic words from $filename"); return \%magic; } # Check the last mod time of a file and re-read it if it has changed sub UpdateMagicWords { my($option) = @_; my($lastmod); $option = lc($option); ($lastmod) = (stat $filename{$option})[9]; print STDERR "Last mod date of $option is $lastmod\n" if $Debug; if ($lastmod != $modtime{$option}) { MailScanner::Log::InfoLog("Noticed update of magic list for $option"); print STDERR "Update occurred for $option, from $modtime{$option} to $lastmod\n" if $Debug; CreateMagicWord($option); $modtime{$option} = $lastmod; # Update the stored modification time print STDERR "Updated $option\n" if $Debug; } } # Look up the magic words for a given option and email message sub LookupMagic { my($option, $message) = @_; $option = lc($option); print STDERR "Looking up $option for message ".$message->{id}."\n" if $Debug; # If the magic word table has changed, read it and rebuild the tables UpdateMagicWords($option); # If there is no message and there is a default magic word then return it return $magicwords{$option}{'default'} if !$message && exists $magicwords{$option}; # If there is no message and no default value then we failed the lookup return undef unless $message && exists $magicwords{$option}; # Okay, we now know the relevant data structures exist my($todomain, @todomain, $magicword, %magichash, @magiclist); # Beware, there is a list of recipients, subject must end with any # combination of the magic words @todomain = @{$message->{todomain}}; foreach $todomain (@todomain) { print STDERR "Looking up $option for $todomain\n" if $Debug; $magicword = ""; $magicword = $magicwords{$option}{$todomain}; # Use the default if needed, and if the default has been defined $magicword = $magicwords{$option}{'default'} if $magicword eq ""; next if $magicword eq ""; # Store it $magichash{$magicword} = 1; print STDERR "and found magic word \"$magicword\"\n" if $Debug; } @magiclist = keys %magichash; print STDERR "$option for message is " . join(',',@magiclist) . "\n" if $Debug; return @magiclist; } # Does this message end in any of the necessary magic words? # Return 1 if it does, 0 or undef otherwise. sub MatchSubject { my($message, @magicwords) = @_; my($word, @quotedwords); # If there are no magic words, must never find them return 0 unless @magicwords; foreach $word (@magicwords) { push @quotedwords, quotemeta($word); } my $regexp = '(' . join('|',@quotedwords) . ')'; print STDERR "RegExp = $regexp\n" if $Debug; # Must never match an empty regexp return undef if $regexp eq '()'; my $subject = $message->{subject}; return undef if $subject eq ""; print STDERR "Checking \"$subject\" against \"$regexp\"\n" if $Debug; if ($subject =~ /\s$regexp\s*$/i) { print STDERR "Found it!\n" if $Debug; MailScanner::Log::InfoLog("Found magic word in Subject: %s in %s", $subject, $message->{id}); return 1; } else { print STDERR "Did not find it\n" if $Debug; return 0; } } # # Now for all the handling of rulesets as well # We support list of domains which are yes, default yes/no # and we assume 127.0.0.1 is no # sub SetupRecipient { my($option, $ruleset) = @_; $option = lc($option); # Just in case! print STDERR "Setting everything up for ruleset $option from $ruleset\n" if $Debug; # Set everything up for this configuration option $rulesetfilename{$option} = '/dev/null'; $rulesetfilename{$option} = $ruleset if $ruleset; $rulesetmodtime{$option} = (stat($ruleset))[9]; $ruleset{$option} = (); MailScanner::Log::WarnLog("Reading rulset list for $option failed") unless CreateRuleset($option); } sub CreateRuleset { my($optionname) = @_; my($filename, $domain2one); $optionname = lc($optionname); # Make sure upper/lower case is consistent # Read in the magic word list for this MailScanner.conf option MailScanner::Log::InfoLog("Initialising ruleset for $optionname"); $filename = $rulesetfilename{$optionname}; # Read the ruleset for this file and option $domain2one = ReadRuleset($optionname, $filename); print STDERR "domain2one = $domain2one\n" if $Debug; return undef unless $domain2one; $ruleset{$optionname} = $domain2one; $rulesetfilename{$optionname} = $filename; PrintRuleset($optionname) if $Debug; return 1; # Success } sub PrintRuleset { my($option) = @_; my($key, $value, $hash); print STDERR "\nThe ruleset for $option is:\n"; $hash = $ruleset{$option}; while(($key, $value) = each %$hash) { print STDERR "$key\t$value\n"; } } sub ReadRuleset { my($option, $filename) = @_; # Just re-read the ruleset for this option #$rulesetfilename{$option} = $filename; SetupRuleset($option, $rulesetfilename{$option}); # The rest of this is now totally redundant return 0; my($handle, %rules, $to, $domain, $value, $counter); $handle = new FileHandle; unless ($handle->open("<$filename")) { print STDERR "Could not open $filename for reading ruleset list\n" if $Debug; MailScanner::Log::WarnLog("Could not read ruleset list $filename"); return undef; } %rules = (); $counter = 0; while(<$handle>) { # Handle comments, leading/trailing space, blank lines and other rubbish chomp; print STDERR "Read \"$_\"\n" if $Debug; s/\#.*$//; s/^\s+//; s/\s+$//; next if $_ eq ""; # Get the interesting bits ($to, $domain, $value) = split(" ", $_, 3); $domain = lc($domain); $value = lc($value); # Check and store it $domain =~ s/^.*\@//; $value = ($value =~ /y/i)?1:0; print STDERR "Read value \"$value\" for domain \"$domain\"\n" if $Debug; $rules{"$domain"} = $value; $counter++; } $handle->close; MailScanner::Log::InfoLog("Read $counter rules from $filename"); return \%rules; } # Check the last mod time of a file and re-read it if it has changed sub UpdateRulesets { my($option) = @_; my($lastmod); $option = lc($option); ($lastmod) = (stat $rulesetfilename{$option})[9]; print STDERR "Last mod date of ruleset for $option is $lastmod\n" if $Debug; if ($lastmod != $rulesetmodtime{$option}) { MailScanner::Log::InfoLog("Noticed update of ruleset for $option"); print STDERR "Update occurred for ruleset $option, from " . $rulesetmodtime{$option} . " to $lastmod\n" if $Debug; CreateRuleset($option); $rulesetmodtime{$option} = $lastmod; # Update the stored modification time print STDERR "Updated ruleset for $option\n" if $Debug; } } # Lookup the ruleset for a given message and option name sub LookupRuleset { my($option, $message) = @_; $option = lc($option); print STDERR "Looking up ruleset $option for message " . $message->{id} . "\n" if $Debug; # If the ruleset has changed, read it and rebuild the tables UpdateRulesets($option); # All the rulesets have 127.0.0.1 --> 0 return 1 unless $message; return 0 if $message->{clientip} =~ /^127\.0\.0/; # Just evaluate the ruleset my $E2I = MailScanner::Config::GetEtoI(); # Get the old Custom Function name and delete it my $funcname = MailScanner::Config::GetCustomFunction($E2I->{$option}); MailScanner::Config::SetCustomFunction($E2I->{$option}, undef); #print STDERR "About to lookup ruleset for $option (" . $E2I->{$option} . ")\n"; my $rulesetresult = MailScanner::Config::Value($E2I->{$option}, $message); #print STDERR "The ruleset for $option (" . $E2I->{$option} . ") said $rulesetresult\n"; # Restore the old Custom Function name MailScanner::Config::SetCustomFunction($E2I->{$option}, $funcname); return $rulesetresult; ############################### # The rest of this is redundant ############################### # Return the default value is there is no message return $ruleset{$option}{'default'} if !$message && exists $ruleset{$option}; # If there is no message and no default value then we failed the lookup return undef unless $message && exists $ruleset{$option}; # Okay, we now know the relevant data structures exist my($foundit, $result, $todomain, @todomain); # Beware, there is a list of recipients, any domain must say yes @todomain = @{$message->{todomain}}; $result = 0; $foundit = 0; foreach $todomain (@todomain) { print STDERR "Looking up ruleset $option for $todomain\n" if $Debug; if (exists($ruleset{$option}{$todomain})) { $foundit = 1; $result = 1 if $ruleset{$option}{$todomain}; print STDERR "Found a match, result = $result\n" if $Debug; } } # If we found a result, then return it print STDERR "Returning match \"$result\" as we found one\n" if $foundit && $Debug; return $result if $foundit; # We didn't find a result, so return the default $result = undef; $result = $ruleset{$option}{'default'}; $result = 0 unless defined $result; print STDERR "No match found, returning \"$result\"\n" if $Debug; return $result; } ############################################################################ ############################################################################ ############################################################################ # # # You need the following 3 functions for each MailScanner.conf configuration # option which is getting the LastSpam.com treatment. # # The only bit you need to change is the value for "$option" in each function # which should be the external name of the config option. # The external name is the name that appears in MailScanner.conf but with # nothing except a-z and 0-9, all lower-case. # # ############################################################################ # # Virus Scanning = # sub InitLastSpamVirusScanning { my($filename, $ruleset) = @_; my $option = 'virusscanning'; SetupMagicOption($option, $filename); SetupRecipient($option, $ruleset); } sub LastSpamVirusScanning { my($message) = @_; my $option = 'virusscanning'; unless ($ReadConfDone{$option}) { SetupRuleset($option); $ReadConfDone{$option} = 1; } return 0 if MatchSubject($message, LookupMagic($option, $message)); return LookupRuleset($option, $message); } sub EndLastSpamVirusScanning { my $option = 'virusscanning'; MailScanner::Log::InfoLog("Shutting down LastSpam $option"); } ############################################################################ # # Dangerous Content Checks = # sub InitLastSpamDangerousContent { my($filename, $ruleset) = @_; my $option = 'dangerouscontentscanning'; SetupMagicOption($option, $filename); SetupRecipient($option, $ruleset); } sub LastSpamDangerousContent { my($message) = @_; my $option = 'dangerouscontentscanning'; unless ($ReadConfDone{$option}) { SetupRuleset($option); $ReadConfDone{$option} = 1; } return 0 if MatchSubject($message, LookupMagic($option, $message)); return LookupRuleset($option, $message); } sub EndLastSpamDangerousContent { my $option = 'dangerouscontentscanning'; MailScanner::Log::InfoLog("Shutting down LastSpam $option"); } ############################################################################ # # Spam Checks = # sub InitLastSpamSpamChecks { my($filename, $ruleset) = @_; my $option = 'spamchecks'; SetupMagicOption($option, $filename); SetupRecipient($option, $ruleset); } sub LastSpamSpamChecks { my($message) = @_; my $option = 'spamchecks'; unless ($ReadConfDone{$option}) { SetupRuleset($option); $ReadConfDone{$option} = 1; } return 0 if MatchSubject($message, LookupMagic($option, $message)); return LookupRuleset($option, $message); } sub EndLastSpamSpamChecks { my $option = 'spamchecks'; MailScanner::Log::InfoLog("Shutting down LastSpam $option"); } #======================================================================= # # Create and read the MailScanner.conf file for this command-line option # sub SetupRuleset { my($opkeyword) = @_; my $fh = new FileHandle; $fh->open("> $tmpfilename") or die "$!"; my $rf = $rulesetfilename{$opkeyword}; #print STDERR "RF = $rf\n"; #print STDERR $opkeyword . " = $rf\n"; print $fh $opkeyword . " = $rf\n"; $fh->close; MailScanner::Config::SetFileValue($opkeyword, undef); # Must ensure the ruleset for this option is empty before we start reading MailScanner::Config::ReadData($tmpfilename); unlink $tmpfilename; } # This file must end with the following line no strict; 1;