#
# 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;
syntax highlighted by Code2HTML, v. 0.9.1