#
#   MailScanner - SMTP E-Mail Virus Scanner
#   Copyright (C) 2002  Julian Field
#
#   $Id: CustomConfig.pm,v 1.3.2.36 2006/01/14 21:56:52 jkf Exp $
#
#   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
#
#   The author, Julian Field, can be contacted by email at
#      Jules@JulianField.net
#   or by paper mail at
#      Julian Field
#      Dept of Electronics & Computer Science
#      University of Southampton
#      Southampton
#      SO17 1BJ
#      United Kingdom
#

package MailScanner::CustomConfig;

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.3.2.36 $, 10;

#
# These are the custom functions that you can write to produce a value
# for any configuration keyword that you want to do clever things such
# as retrieve values from a database.
#
# Your function may be passed a "message" object, and must return
# a legal value for the configuration parameter. No checking will be
# done on the result, for extra speed. If you want to find out what
# there is in a "message" object, look at Message.pm as they are all
# listed there.
#
# You must handle the case when no "message" object is passed to your
# function. In this case it should return a sensible default value.
#
# Return value: You must return the internal form of the result values.
#               For example, if you are producing a yes or no value,
#               you return 1 or 0. To find all the internal values
#               look in ConfigDefs.pl.
#
# For each function "FooValue" that you write, there needs to be a
# function "InitFooValue" which will be called when the configuration
# file is read. In the InitFooValue function, you will need to set up
# any global state such as create database connections, read more
# configuration files and so on.
#

##
## This is a trivial example function to get you started.
## You could use it in the main MailScanner configuration file like
## this:
##      VirusScanning = &ScanningValue
##
#sub InitScanningValue {
#  # No initialisation needs doing here at all.
#  MailScanner::Log::InfoLog("Initialising ScanningValue");
#}
#
#sub EndScanningValue {
#  # No shutdown code needed here at all.
#  # This function could log total stats, close databases, etc.
#  MailScanner::Log::InfoLog("Ending ScanningValue");
#}
#
## This will return 1 for all messages except those generated by this
## computer.
#sub ScanningValue {
#  my($message) = @_;
#
#  return 1 unless $message; # Default if no message passed in
#
#  return 0 if $message->{subject} =~ /jules/i;
#  return 1;
#
#  #my($IPAddress);
#  #$IPAddress = $message->{clientip};
#  #return 0 if $IPAddress eq '127.0.0.1';
#  #return 1;
#}

#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************

#
# This set of functions provides per-domain simple spam whitelists and
# blacklists. Each of the 2 directories set below contains 1 file for
# each domain, with the domain name being the filename. The file contains
# a list of entries, 1 per line, each one either being a full address:
#    user@domain.com
# or an entire domain:
#    domain.com
# The addresses contained in the file for a domain make up the entire
# spam whitelist or blacklist for that domain.
#
# For example, say you had /etc/MailScanner/spam.bydomain/whitelist/jules.fm
# which included the lines
#	soton.ac.uk
#	ecs.soton.ac.uk
#	jules@julianfield.net
#	123.234.45.56
# Then all mail from anything@soton.ac.uk, anything@ecs.soton.ac.uk or
# jules@julianfield.net would be whitelisted if it was heading to any
# address @jules.fm. Also all mail from IP address 123.234.45.56 would be
# whitelisted if it was heading to any address @jules.fm.
# The same thing works for the blacklist directory.
# For per user per domain use the file name username@domainname.com.
#
# Overall white and blacklists should be put in a file in each directory
# called 'default'.
#
# To enable these functions, set the following in your MailScanner.conf file:
#   Is Definitely Not Spam = &ByDomainSpamWhitelist
#   Is Definitely Spam     = &ByDomainSpamBlacklist
#

# Set these to be the location of your whitelist files and blacklist files
my $WhitelistDir = '/etc/MailScanner/spam.bydomain/whitelist';
my $BlacklistDir = '/etc/MailScanner/spam.bydomain/blacklist';


use DirHandle;
use FileHandle;

my(%Whitelist, %Blacklist);

#
# Initialise by-domain spam whitelist and blacklist
#
sub InitByDomainSpamWhitelist {
  MailScanner::Log::InfoLog("Starting up by-domain spam whitelist, " .
                            "reading from %s", $WhitelistDir);
  my $domains = CreateByDomainList($WhitelistDir, \%Whitelist);
  MailScanner::Log::InfoLog("Read whitelist for %d domains", $domains);
}

sub InitByDomainSpamBlacklist {
  MailScanner::Log::InfoLog("Starting up by-domain spam blacklist, " .
                            "reading from %s", $BlacklistDir);
  my $domains = CreateByDomainList($BlacklistDir, \%Blacklist);
  MailScanner::Log::InfoLog("Read blacklist for %d domains", $domains);
}


#
# Lookup a message in the by-domain whitelist and blacklist
#
sub ByDomainSpamWhitelist {
  my($message) = @_;

  return LookupByDomainList($message, \%Whitelist);
}

sub ByDomainSpamBlacklist {
  my($message) = @_;

  return LookupByDomainList($message, \%Blacklist);
}


#
# Close down the by-domain whitelist and blacklist
#
sub EndByDomainSpamWhitelist {
  MailScanner::Log::InfoLog("Closing down by-domain spam whitelist");
}

sub EndByDomainSpamBlacklist {
  MailScanner::Log::InfoLog("Closing down by-domain spam blacklist");
}


#
# Setup the per-domain spam white or black list.
# Note this doesn't do anything much in the way of syntax-checking the
# files, so they better be right! If there are duff lines in the files,
# they just won't produce any matches, they can't actually cause any harm.
#
sub CreateByDomainList {
  my($dirname, $BlackWhite) = @_;

  my($dir, $filename, $fh, $domains);

  $dir = new DirHandle;
  $dir->open($dirname) or return 0;
  $domains = 0; # Count the number of domains we have read
  while ($filename = $dir->read()) {
    next if $filename =~ /^\./;
    next unless -f "$dirname/$filename";

    $fh = new FileHandle;
    $fh->open("$dirname/$filename") or next;
    $filename = lc($filename); # Going to store the name in lower case
    while(<$fh>) {
      chomp;
      #print STDERR "Line is \"$_\"\n";
      s/#.*$//; # Strip comments
      s/\S*:\S*//g; # Strip any words with ":" in them
      s/^\s+//g; # Strip leading whitespace
      s/^(\S+)\s.*$/$1/; # Use only the 1st word
      s/^\*\@//; # Strip any leading "*@" they might have put in
      #print STDERR "Line is \"$_\"\n";
      next if /^$/; # Strip blank lines
      $BlackWhite->{$filename}{lc($_)} = 1; # Store the whitelist entry
    }
    $fh->close();
    $domains++;
  }
  $dir->close();

  return $domains;
}


#
# Based on the address it is going to, choose the right spam white/blacklist.
# Return 1 if the "from" address is white/blacklisted, 0 if not.
#
sub LookupByDomainList {
  my($message, $BlackWhite) = @_;

  return 0 unless $message; # Sanity check the input

  # Find the "from" address and the first "to" address
  my($from, $fromdomain, @todomain, $todomain, @to, $to, $ip);
  $from       = $message->{from};
  $fromdomain = $message->{fromdomain};
  @todomain   = @{$message->{todomain}};
  $todomain   = $todomain[0];
  @to         = @{$message->{to}};
  $to         = $to[0];
  $ip         = $message->{clientip};

  # It is in the list if either the exact address is listed,
  # or the domain is listed
  return 1 if $BlackWhite->{$to}{$from};
  return 1 if $BlackWhite->{$to}{$fromdomain};
  return 1 if $BlackWhite->{$to}{$ip};
  return 1 if $BlackWhite->{$todomain}{$from};
  return 1 if $BlackWhite->{$todomain}{$fromdomain};
  return 1 if $BlackWhite->{$todomain}{$ip};
  return 1 if $BlackWhite->{'default'}{$from};
  return 1 if $BlackWhite->{'default'}{$fromdomain};
  return 1 if $BlackWhite->{'default'}{$ip};

  # It is not in the list
  return 0;
}



#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************

###########################################################################
#
# Handy little feature to let you use the same MailScanner.conf file on
# lots of different hosts, where the only difference is the hostname.
# Just uncomment the "use Sys::Hostname" line and then set
#   Hostname = &Hostname
# in your MailScanner.conf to use this.
#
# Many thanks to Tony Finch for this.
#
###########################################################################

# Uncomment this line: use Sys::Hostname;

my $hostname2;

sub InitHostname {
  $hostname2 = hostname;
}

sub Hostname {
  return $hostname2;
}

sub EndHostname {
  # nothing to do
}


#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************

###########################################################################
#
# This is a setup to do logging to an SQL database.
# For speed, the per-message logs are written to a tab-separated file
# during execution.
# When the child process dies of old age (or is politely killed), the
# log file is read and inserted into a database table.
#
# If you want to use this code, you must:
# 1. uncomment the "use DBI;" line just below this comment.
# 2. Read the README.sql-logging file in the docs directory
#    of the distribution.
#
###########################################################################

use IO::File;
# UNCOMMENT THIS LINE: use DBI;

my($logfile1, $logfile2, $logfile3);

# Initialise. All we need to do is create the temporary log files. These
# are created using tmpfile() to avoid security problems caused by any
# other process on the system being able to read (or even write!) to
# the log files. The files created are not accessible to any other processes
# at all, as they don't have an entry in a directory.

sub InitSQLLogging {
  MailScanner::Log::InfoLog("Initialising SQL Logging temp files");
  $logfile1 = IO::File->new_tmpfile or die "IO::File->new_tmpfile: $!";
  $logfile2 = IO::File->new_tmpfile or die "IO::File->new_tmpfile: $!";
  $logfile3 = IO::File->new_tmpfile or die "IO::File->new_tmpfile: $!";
  #$logfile->autoflush(1);
}

# Shutdown. Write all the log entries to the SQL database, then close
# the temporary log files. Closing them will also delete them as they were
# created with tmpfile().
sub EndSQLLogging {
  my(@fields);

  MailScanner::Log::InfoLog("Ending SQL Logging temp output " .
                            "and flushing to database");

  # Create database connection
  my($dbh);
  $dbh = DBI->connect("DBI:mysql:mailscanner:192.168.0.51",
                      "mailscanner", "",
                      {'PrintError' => 0})
   or MailScanner::Log::DieLog("Cannot connect to the database: %s",
                                $DBI::errstr);

  # Rewind to start of logfile1
  $logfile1->flush();
  seek($logfile1, 0, 0)
    or MailScanner::Log::DieLog("EndSQLLogging seek: %s", $!);

  while(<$logfile1>) {
    chomp;
    @fields = split(/\t/);
    # Work through each field protecting any special characters such as '
    # The line below replaces ' with \'
    map { s/\'/\\'/g } @fields;

    next unless $fields[1]; # The primary key must not be blank!

    # Set any empty strings to NULL so the SQL insert works correctly
    @fields = map { ($_ eq '')?'NULL':"$_" } @fields;

    # Insert @fields into a database table
    my($sth) = $dbh->prepare("INSERT INTO maillog_mail (time, msg_id, size, from_user, from_domain, subject, clientip, archives, isspam, ishighspam, sascore, spamreport) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)");
    $sth->execute($fields[0],$fields[1],$fields[2],$fields[3],$fields[4],$fields[5],$fields[6],$fields[7],$fields[8],$fields[9],$fields[10],$fields[11]) or MailScanner::Log::DieLog("Cannot insert row: %s", $DBI::errstr);
  }

  # Rewind to start of logfile2
  $logfile2->flush();
  seek($logfile2, 0, 0)
    or MailScanner::Log::DieLog("EndSQLLogging seek: %s", $!);

  while(<$logfile2>) {
    chomp;
    @fields = split(/\t/);
    # Work through each field protecting any special characters such as '
    # The line below replaces ' with \'
    map { s/\'/\\'/g } @fields;

    # Insert @fields into a database table
    my($sth) = $dbh->prepare("INSERT INTO maillog_report (msg_id, filename, filereport) VALUES (?,?,?)");
    $sth->execute($fields[0],$fields[1],$fields[2]) or MailScanner::Log::DieLog("Cannot insert row: %s", $DBI::errstr);
  }

  # Rewind to start of logfile3
  $logfile3->flush();
  seek($logfile3, 0, 0)
    or MailScanner::Log::DieLog("EndSQLLogging seek: %s", $!);

  while(<$logfile3>) {
    chomp;
    @fields = split(/\t/);
    # Work through each field protecting any special characters such as '
    # The line below replaces ' with \'
    map { s/\'/\\'/g } @fields;

    # Insert @fields into a database table
    my($sth) = $dbh->prepare("INSERT INTO maillog_recipient (msg_id, to_user, to_domain) VALUES (?,?,?)");
    $sth->execute($fields[0],$fields[1],$fields[2]) or MailScanner::Log::DieLog("Cannot insert row: %s", $DBI::errstr);
  }


  # Close database connection
  $dbh->disconnect();

  # Close and delete the temporary files (deletion is done automatically)
  $logfile1->close();
  $logfile2->close();
  $logfile3->close();
  MailScanner::Log::InfoLog("Database flush completed");
}

# Write all the log information for 1 message to the temporary file.
# For messages with reports, write 1 line for each report.
sub SQLLogging {
  my($message) = @_;

  my $id = $message->{id};
  my $size = $message->{size};
  my $from = $message->{from};
  my ($from_user, $from_domain);

  # split the from address into user and domain bits.
  # This may be unnecessary for you; we use it to more easily determine
  # inbound vs outbound email in a multi-domain environment.
  # HINT: refine queries using SQL 'join' with a table containing local 
  # domains.

  ($from_user, $from_domain) = split /\@/, $from;

  my @to   = @{$message->{to}};
  my $subject = $message->{subject};
  my $clientip = $message->{clientip};
  my $archives = join(',', @{$message->{archiveplaces}});
  my $isspam = $message->{isspam};
  my $ishighspam = $message->{ishigh};
  my $sascore = $message->{sascore};
  my $spamreport = $message->{spamreport};

  # Get rid of control chars and tidy-up SpamAssassin report
  $spamreport =~ s/\n/ /g;
  $spamreport =~ s/\t//g;

  # Get timestamp, and format it so it is suitable to use with MySQL
  my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();
  my($timestamp) = sprintf("%d-%02d-%02d %02d:%02d:%02d",
			   $year+1900,$mon+1,$mday,$hour,$min,$sec);

  # Print 1 line for each message.

  $subject =~ s/\t//g; # Remove stray tab characters
  print $logfile1 join("\t", $timestamp, $id, $size, $from_user, $from_domain,
			 $subject, $clientip, $archives, $isspam, $ishighspam,
			 $sascore, $spamreport) . "\n";

  # Generate one line in logfile2 for each report. This logfile includes
  # the message ID, attachment filename and report.

  my($file, $text);
  while(($file, $text) = each %{$message->{allreports}}) {
    $file = "the entire message" if $file eq "";
    # Use the sanitised filename to avoid problems caused by people forcing
    # logging of attachment filenames which contain nasty SQL instructions.
    $file = $message->{file2safefile}{$file} || $file;
    $text =~ s/\n/ /;  # Make sure text report only contains 1 line
    $text =~ s/\t/ /; # and no tab characters
    print $logfile2 join("\t", $id, $file, $text) . "\n";
  }
 
  # Now print the recipients in logfile3.
 
  for (@to) {

    # again, split the recipient's email into user and domain halves first.
    # see comment above about splitting the email like this.

    my ($to_user, $to_domain);
    ($to_user, $to_domain) = split /\@/, $_;
    print $logfile3 join ("\t", $id, $to_user, $to_domain) . "\n";
  }
}


#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************


#
# This Custom Function provides a facility whereby some internal-only
# accounts can only send mail to other "internal" domain names, and cannot
# send mail to any other addresses apart from those domains.
#
# To use it, specify
#    Non Spam Actions          = &InternalActions
#    Spam Actions              = &InternalActions
#    High Scoring Spam Actions = &InternalActions
# in your MailScanner.conf file, having added this code to
#    /usr/lib/MailScanner/MailScanner/CustomConfig.pm
#
# It uses a configuration file whose path is
my($InternalAccountList) = '/etc/MailScanner/internal.accounts.conf';
# to read lines that look like one of these
#   domain         yourdomain.com
#   account        local-only1
# These 2 lines in the file would define that a local email account
# "local-only1" could not send mail to any address except addresses
# @yourdomain.com.
# There can be many domains and many accounts specified, one per line.
#
# Mail from the internal-only accounts to external domains will have the
my($InternalFailAction) = 'delete';
# action applied to it. This can be any of the legal "spam actions" as
# defined in the MailScanner.conf file.
#

use FileHandle;

my(%InternalDomains, %InternalAccounts);

sub InitInternalActions {
  MailScanner::Log::InfoLog("Initialising Internal account list");
  my $listfile = new FileHandle;

  unless($listfile->open("<$InternalAccountList")) {
    MailScanner::Log::WarnLog("Could not read list of internal accounts " .
                              "from %s", $InternalAccountList);
    return;
  }

  my($keyword, $value);
  my $line = 0;
  my $domains = 0;
  my $accounts = 0;
  while(<$listfile>) {
    $line++;
    chomp;
    s/^#.*$//;
    s/^\s*//g;
    s/\s*$//g;
    next if /^$/;

    $keyword = undef;
    $value   = undef;
    /^([^\s]+)\s*([^\s]+)$/;
    ($keyword, $value) = (lc($1), lc($2));
    $value =~ s/\@.*$//; # Delete the @ and everything after it
    if ($keyword =~ /domain/i) {
      #print STDERR "Storing domain $value\n";
      $InternalDomains{$value} = 1;
      $domains++;
    } elsif ($keyword =~ /account|user/i) {
      #print STDERR "Storing account $value\n";
      $InternalAccounts{$value} = 1;
      $accounts++;
    } else {
      MailScanner::Log::WarnLog("Syntax error in %s at line %d",
                                $InternalAccountList, $line);
    }
  }
  $listfile->close();
  MailScanner::Log::InfoLog("Internal Account List read %d domains and %d " .
                            "accounts", $domains, $accounts);
}

sub EndInternalActions {
  # No shutdown code needed here at all.
  MailScanner::Log::InfoLog("Shutting down internal accounts list");
}

# This will return 1 for all messages except those generated by this
# computer.
# This will return "deliver" for all internal mail as requested,
# and $InternalFailAction for everything else.
sub InternalActions {
  my($message) = @_;

  return 'deliver' unless $message; # Default if no message passed in
  return 'deliver' unless $message->{from}; # Default if duff message

  my($fromac, $fromdomain, $todomain);
  $fromac = lc($message->{from});
  $fromdomain = $fromac;
  $fromac =~ s/\@.*$//;   # Leave everything before @
  $fromdomain =~ s/^.*\@//; # Leave everything after  @

  # Is it coming from inside?
  #print STDERR "Testing $fromdomain\n";
  #print STDERR "Answer is " . $InternalDomains{$fromdomain} . "\n";
  return 'deliver' unless $InternalDomains{$fromdomain};
  #print STDERR "$fromdomain passed internaldomains test\n";
  # and is it coming from an internal-only address?
  return 'deliver' unless $InternalAccounts{$fromac};
  #print STDERR "$fromac passed internalaccounts test\n";

  # Fail if it is being delivered to *any* external addresses
  foreach $todomain (@{$message->{todomain}}) {
    $todomain = lc($todomain);
    #print STDERR "Testing $todomain\n";
    unless ($InternalDomains{$todomain}) {
      MailScanner::Log::WarnLog("Internal-only account %s attempted to " .
                   "send mail to external address \@%s", $fromac, $todomain);
      return $InternalFailAction;
    }
  }

  # Passed that, so it must be only going to internal addresses
  return 'deliver';
}



#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************

#
# This Custom Function provides a facility whereby you have multiple
# outgoing mail queues, each handled by a separate sendmail queue-runner.
# In addition to enabling this code, you will need to run a queue-runner
# process for each new queue you add, so for this code as written here,
# you will need
#    sendmail -q30m -OQueueDirectory=/var/spool/mqueue.spam
#    sendmail -q60m -OQueueDirectory=/var/spool/mqueue.highspam
# You will of course also need to create the directories
#    mkdir /var/spool/mqueue.spam
#    mkdir /var/spool/mqueue.highspam
#
# To use this code from MailScanner.conf, set this in MailScanner.conf:
#    Outgoing Queue Dir = &MultipleQueueDir
#

sub InitMultipleQueueDir {
  ;
}

sub EndMultipleQueueDir {
  ;
}

sub MultipleQueueDir {
  my($message) = @_;

  return '/var/spool/mqueue' unless $message; # catch-all if message is duff
  return '/var/spool/mqueue.highspam' if $message->{ishigh};
  return '/var/spool/mqueue.spam'     if $message->{isspam};
  return '/var/spool/mqueue';
}



#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************


#
# This Custom Function provides a facility whereby IP addresses that send
# us more than a certain number of messages per hour are blocked for the
# rest of that hour. The number of messages per hour for each IP addresses
# is configurable in a config file. A default value is also supplied in
# the config file.
# This does not take any account of whether the messages are plain, spam,
# viruses or anything else. It simply counts the number of messages in
# the current hour.
# It is currently only designed to work with sendmail.
#
# The config file is stored in /etc/MailScanner/IPBlock.conf.  You
# should set high default numbers for 127.0.0.1 and the IP number
# of your mail server(s).  First match in the list defines the number
# for a particular IP.  If you have the rules:
#
# 152.123.34.0/24 1000
# 152.123.34.36   100
#
# then machine 152.123.34.36 will have a setting of 1000 messages per
# hour.  If you want it to have a smaller limit than the rest of the
# class-C netblock, then code the rules like this:
#
# 152.123.34.36   100
# 152.123.34.0/24 1000
#
# Other examples:
# 10.11.12.13	100000	# Known good site
# 152.123.34.45	100	# Known spammer, throttle to 100 messages per hour
# 152.123.      100
# 152.123       100
# 152.123/255.255.0.0 100
# 152.123.34/24 100
# 152.123.34.0-152.123.37.255 100
# default	1000	# Default limit is 1000 messages per hour
#
# To use this, configure the variables defined immediately below this
# comment and set
# Always Looked Up Last = &IPBlock
# in MailScanner.conf.
# You will also need to look at the end of this file for the contents
# of an hourly cron job to clear out old entries from the database.
#
# NOTE: Postfix Users
# ===================
# To be able to use IPBlock make sure that you have the
# smtpd_client_restrictions entry with /etc/postfix/access.
#
# Your entry should look some thing like this
# smtpd_client_restrictions = check_client_access hash:/etc/postfix/access
#			      ......... other checks for client restrictions			      


# Import the flock names
use Fcntl qw(:DEFAULT :flock);
use FileHandle;
use Net::CIDR;
use Socket;
use POSIX qw(:signal_h); # For Solaris 9 SIG bug workaround
use IO;
BEGIN { @AnyDBM_File::ISA = qw(DB_File GDBM_File NDBM_File SDBM_File) }
use AnyDBM_File;

# You may need to configure these for your site:
my $WhitelistFile= '/etc/MailScanner/IPBlock.conf';
my $LockFile     = '/var/spool/MailScanner/IPBlock.lock';
#my $LogFile     = '/var/spool/MailScanner/IPBlock.log';
my $BlockDB      = '/var/spool/MailScanner/IPBlock.db';
my $DefaultMaxMessagesPerHour = 1000; # 999999999
my $FailCode     = '550';

my(%IP2Limit, %IPBWhiteCIDR);
my(%CIDRtoLimit); # Map CIDR onto limit
my(@CIDRlist);    # Ordered list of rules, all in CIDR form
my($hostname);

my $ConfFile = $ARGV[0];
$ConfFile = $ARGV[1] if $ConfFile =~ /^-+v/i;

my ($AccessDB, $Refusal, $my_mta); # Generalized here for Multiple MTA support
$my_mta = lc(MailScanner::Config::QuickPeek($ConfFile, 'mta')) if $ConfFile &&
                                                               -f $ConfFile;
if ($my_mta eq "postfix") { 
  $AccessDB = "/etc/postfix/access.db";
  $Refusal = "$FailCode Site blocked by MailScanner due to excessive email";

} elsif ($my_mta eq "sendmail") {
  $AccessDB = "/etc/mail/access.db";
  $Refusal = "\"$FailCode Site blocked by MailScanner due to excessive email\"";
} else {
  $AccessDB = "";
  $Refusal = "";
}


sub InitIPBlock {
  my($LimitsH, $cidr, $limit, $counter);

  # Skip IP block initialization if not postfix or sendmail
  if ($my_mta !~ /postfix|sendmail/) {
    MailScanner::Log::ErrorLog("IPBlock Currently not supported " .
                               "for your MTA %s", $my_mta);
    return 1;
  }

  MailScanner::Log::InfoLog("Initialising IP blocking");
  my $LimitsH = new FileHandle;
  unless ($LimitsH->open($WhitelistFile)) {
    MailScanner::Log::WarnLog("Could not read IPBlock configuration from %s",
                              $WhitelistFile);
    return;
  }
  $counter = 0;
  while(<$LimitsH>) {
    chomp;
    s/#.*$//;
    s/^\s*//g;
    s/\s*$//g;
    next if /^$/;
    ($cidr, $limit) = split;
    $cidr =~ s/\s//g;
    $limit = 0 unless defined $limit;

    my @cidrlist = undef;
    if ($cidr =~ /-/) {
      # It looks like 152.78.67.0-152.78.69.255
      @cidrlist = Net::CIDR::range2cidr($cidr);
    } elsif ($cidr =~ /\//) {
      # It looks like 152.78.0.0/16 or 152.78/16 or 152.78/255.255.0.0
      my($network, $bits, $count);
      ($network, $bits) = split(/\//, $cidr);
      $network =~ s/\.$//; # Delete any trailing dot
      $count = split(/\./, $network);
      $network .= '.0' x (4-$count); # Fill out the CIDR for Net::CIDR
      # 152.78 now looks like 152.78.0.0
      if ($bits =~ /\./) {
        # It's like 152.78.0.0/255.255.0.0
        push @cidrlist, Net::CIDR::addrandmask2cidr($network, $bits);
      } else {
        # It's like 152.78.0.0/16
        push @cidrlist, "$network/$bits";
      }
    } elsif ($cidr =~ /default/i) {
      # It is the default value used when nothing else matches
      $DefaultMaxMessagesPerHour = $limit;
    } else {
      # Must just be an IP address or look like 152.78 or 152.78.
      $cidr =~ s/\.$//; # Delete any trailing dot
      my $count = split(/\./, $cidr);
      $cidr .= '.0' x (4-$count);
      push @cidrlist, "$cidr/" . ($count*8);
    }

    # Build the map from CIDR to message limit
    foreach (@cidrlist) {
      next unless $_;
      #print STDERR "IPBlock: adding $_\n";
      $CIDRtoLimit{$_} = $limit;
      push @CIDRlist, $_;
    }
    $counter++;
  }
  close($LimitsH);
  MailScanner::Log::InfoLog("Read %d IP blocking entries from %s",
                            $counter, $WhitelistFile);
}

sub EndIPBlock {
  MailScanner::Log::InfoLog("Closing down IP blocking");
}

sub IPBlock {
  my($message) = @_;

  # Skip IPblock if not sendmail or postfix

  if ($my_mta !~ /postfix|sendmail/) {
    MailScanner::Log::ErrorLog("IPBlock Currently not supported for " .
                               "your MTA %s", $my_mta);
    return 1;
  }

  return 1 unless $message; # Default if no message passed in

  my $ip = $message->{clientip};
  return 1 unless $ip;

  # Work out which (if any) CIDR this IP address is in.
  my($cidrkey, $foundcidr, $foundit, $limit);
  $foundit = 0;
  foreach $cidrkey (@CIDRlist) {
    #print STDERR "Looking for $ip in $cidrkey\n";
    if (Net::CIDR::cidrlookup($ip, $cidrkey)) {
      #print STDERR "Found it\n";
      $foundit = 1;
      $foundcidr = $cidrkey;
      last;
    }
  }
  # If we didn't find it, use the default value
  $limit = $foundit ? $CIDRtoLimit{$foundcidr} : $DefaultMaxMessagesPerHour;

  #print STDERR "Limit of $foundcidr is $limit\n";

  # If there is already a counter for this IP address and it's zero
  # then return, as this IP is unlimited.
  return 1 if $limit == 0;

  #
  # Lock $LockFile exclusively to block out all other processes
  #
  my $LockFileH = new FileHandle;
  IPBopenlock($LockFileH, ">$LockFile");

  ## Add this IP address to the log file.
  ## It is faster to always write it than to check if it needs to be written.
  #my $LogH = new FileHandle;
  #unless ($LogH->open(">+$LogFile")) {
  #  MailScanner::Log::WarnLog("IPBlock: Cannot open %s for writing", $LogFile);
  #  return 1;
  #}
  #print $LogH $ip . "\n";
  #$LogH->close;

  # Update the counter of the number of messages this IP
  # has sent in the last hour
  #Bind to BlockDB
  my %BlockDB;
  unless (tie %BlockDB, "AnyDBM_File", $BlockDB, O_RDWR|O_CREAT, 0644) {
    MailScanner::Log::WarnLog("IPBlock: Could not open/create %s", $BlockDB);
    IPBunlockclose($LockFileH);
    return 0;
  }
  #Look up $ip and increment counter
  my($record, $hostname, $counter, $time, $donealready);
  $record = $BlockDB{$ip};
  if ($record) {
    ($hostname, $counter, $time, $donealready) = split (/,/, $record);
    $counter++;
    #print STDERR "Found IP in BlockDB, counter=$counter time=$time done=$donealready\n";
  } else {
    $counter = 1;
    $time = time();
    $donealready = 0;
    $hostname = IPBlockIP2Hostname($ip);
    #print STDERR "Not found IP in BlockDB, creating new record\n";
  }

  # If there is already a limit for this IP then use it, else create it
  my($thislimit, $MaxMessagesPerHour);
  $thislimit = $IP2Limit{$ip};
  if (defined $thislimit) {
    $MaxMessagesPerHour = $thislimit;
  } else {
    $MaxMessagesPerHour = $limit;
    $IP2Limit{$ip} = $limit;
  }
  #print STDERR "Messages per hour limit for $ip is $limit\n";

  #
  # This IP address has gone over its limit,
  # so add it to the access DB for sendmail.
  #
  if ($counter > $MaxMessagesPerHour && !$donealready) {
    #print STDERR "Adding record for $ip to accessdb\n";
    my %AccessDB;
    unless (tie %AccessDB, "AnyDBM_File", $AccessDB, O_RDWR, 0644) {
      MailScanner::Log::WarnLog("IPBlock: Could not open access database %s",
                                $AccessDB);
      #Close files and unlock $LockFile
      untie %BlockDB;
      IPBunlockclose($LockFileH);
      return 0;
    }
    MailScanner::Log::InfoLog("IPBlock: Adding block for %s", $ip);
    $AccessDB{$ip} = $Refusal;
    $AccessDB{$hostname} = $Refusal;
    $donealready = 1; # Mark it so we don't try to waste CPU setting it twice
    untie %AccessDB;
  }

  #Write back counter + timestamp
  $BlockDB{$ip} = "$hostname,$counter,$time,$donealready";
  #Unbind BlockDB
  untie %BlockDB;

  #Unlock $LockFile
  IPBunlockclose($LockFileH);
  
  return 1;
}

sub IPBopenlock {
  my($fh, $fn) = @_;

  if (open($fh, $fn)) {
    flock($fh, LOCK_EX);
  } else {
    MailScanner::Log::InfoLog("Could not open file $fn: %s", $!);
  }
}

sub IPBunlockclose {
  my($fh) = @_;

  flock($fh, LOCK_UN);
  close ($fh);
}

# Convert an IP address to a hostname, with timeout protection.
# Return "" if it times out or I can't get a name at all.
sub IPBlockIP2Hostname {
  my($IPstring) = @_;

  return "" unless $IPstring =~ /[0-9.]+/;

  my($pipe);

  unless ($pipe = new IO::Pipe) {
    MailScanner::Log::WarnLog('IPBlock: Could not create pipe, %s', $!);
    return "";
  }

  my $PipeReturn = 0;
  my $GotAHit = 0;

  my $pid = fork();
  unless (defined($pid)) {
    MailScanner::Log::WarnLog('IPBlock: Could not fork process, %s', $!);
    return "";
  }

  if ($pid == 0) {
    # In the child
    $pipe->writer();
    #POSIX::setsid();

    # Switch to line buffering
    $pipe->autoflush();

    # Work out the hostname and print it to the parent
    my $ipaddr = inet_aton($IPstring);
    my $claimed_name = gethostbyaddr($ipaddr, AF_INET);
    print $pipe "$claimed_name\n";

    $pipe->close();
    exit 0;
  }

  # Now for the parent code
  eval {
    $pipe->reader();
    local $SIG{ALRM} = sub { die "Command Timed Out" };
    alarm MailScanner::Config::Value('spamlisttimeout');
    $hostname = <$pipe>;
    chomp $hostname;
    $pipe->close();
    waitpid $pid, 0;
    $PipeReturn = $?;
    alarm 0;
    $pid = 0;
  };
  alarm 0;
  # Workaround for bug in perl shipped with Solaris 9,
  # it doesn't unblock the SIGALRM after handling it.
  eval {
    my $unblockset = POSIX::SigSet->new(SIGALRM);
    sigprocmask(SIG_UNBLOCK, $unblockset)
      or die "Could not unblock alarm: $!\n";
  };

  # Note to self: I only close the KID in the parent, not in the child.

  # Catch failures other than the alarm
  MailScanner::Log::WarnLog("IPBlock: Hostname lookup for %s failed with real error: %s", $IPstring, $@)
    if $@ and $@ !~ /Command Timed Out/;

  # In which case any failures must be the alarm
  #($@ or $pid>0)
  if ($pid>0) {
    MailScanner::Log::WarnLog("IPBlock: Hostname lookup for %s timed out and was killed", $IPstring);
    # Kill the running child process
    my($i);
    kill 15, $pid; # Was -15
    for ($i=0; $i<5; $i++) {
      sleep 1;
      waitpid($pid, &POSIX::WNOHANG);
      ($pid=0),last unless kill(0, $pid);
      kill 15, $pid; # Was -15
    }
    # And if it didn't respond to 11 nice kills, we kill -9 it
    if ($pid) {
      kill 9, $pid; # Was -9
      waitpid $pid, 0; # 2.53
    }
  }

  return $hostname;
}

#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************
#**************************************************************************

#
# This set of functions provide the ability for a great speed improvement
# in the processing of rules files for the "Spam List" and "Spam Domain
# List" settings. It assumes that the addresses to match against in the
# rules file are all of the kind
# To: user@domain rbl1 rbl2 rbl3....
# To: domain rbl1 rbl2 rbl3....
#
# To use this, set
# Spam List = &FastSpamList
# Spam Domain List = &FastSpamDomainList
# in /etc/MailScanner/MailScanner.conf
# and put the rules in these 2 files:
my $Spamlistfile   = '/etc/MailScanner/rules/spam.list.rules';
my $Spamdomainlistfile = '/etc/MailScanner/rules/spam.domain.list.rules';

my $Codebasefile   = '/etc/MailScanner/rules/codebase.tags.rules';
my $Externalfile   = '/etc/MailScanner/rules/external.message.bodies.rules';
my $Filenamefile   = '/etc/MailScanner/rules/filename.rules';
my $Htmltotextfile = '/etc/MailScanner/rules/html.to.text.rules';
my $Iframefile     = '/etc/MailScanner/rules/iframe.tags.rules';
my $Partialfile    = '/etc/MailScanner/rules/partial.message.rules';


use FileHandle;
my(%Fastspamlist, %Fastspamdomainlist, %Fastcodebase, %Fastexternal,
   %Fastfilename, %Fasthtmltotext, %Fastiframe, %Fastpartial);

#-----------------------
# All the Init functions
#-----------------------

sub InitFastSpamList {
  my($dir, $address, $result);

  my $fh = new FileHandle;
  unless ($fh->open($Spamlistfile)) {
    MailScanner::Log::WarnLog("Could not read fast spam list rules from %s",
                              $Spamlistfile);
    return;
  }

  my $counter = 0;
  while(<$fh>) {
    chomp;
    s/#.*$//;     # Remove comments
    s/\s+$//;     # and trailing whitespace
    s/^\s+//;     # and leading whitespace
    next if /^$/; # and blank lines
    ($dir, $address, $result) = split(" ", $_, 3);
    $Fastspamlist{$address} = $result;
    $counter++;
  }

  MailScanner::Log::InfoLog("Read %d fast spam list rules", $counter);
  #print STDERR "Read $counter rules\n";
}


sub InitFastSpamDomainList {
  my($dir, $address, $result);

  my $fh = new FileHandle;
  unless ($fh->open($Spamdomainlistfile)) {
    MailScanner::Log::WarnLog("Could not read fast spam domain list rules " .
                              "from %s", $Spamdomainlistfile);
    return;
  }

  my $counter = 0;
  while(<$fh>) {
    chomp;
    s/#.*$//;     # Remove comments
    s/\s+$//;     # and trailing whitespace
    s/^\s+//;     # and leading whitespace
    next if /^$/; # and blank lines
    ($dir, $address, $result) = split(" ", $_, 3);
    $Fastspamdomainlist{$address} = $result;
    $counter++;
  }

  MailScanner::Log::InfoLog("Read %d fast spam domain list rules", $counter);
  #print STDERR "Read $counter rules\n";
}


sub InitFastCodebase {
  my($dir, $address, $result);

  my $fh = new FileHandle;
  unless ($fh->open($Codebasefile)) {
    MailScanner::Log::WarnLog("Could not read fast codebase tags rules " .
                              "from %s", $Codebasefile);
    return;
  }

  my $counter = 0;
  while(<$fh>) {
    chomp;
    s/#.*$//;     # Remove comments
    s/\s+$//;     # and trailing whitespace
    s/^\s+//;     # and leading whitespace
    next if /^$/; # and blank lines
    ($dir, $address, $result) = split(" ", $_, 3);
    $Fastcodebase{$address} = $result;
    $counter++;
  }

  MailScanner::Log::InfoLog("Read %d fast codebase tags rules", $counter);
  #print STDERR "Read $counter rules\n";
}


sub InitFastExternal {
  my($dir, $address, $result);

  my $fh = new FileHandle;
  unless ($fh->open($Externalfile)) {
    MailScanner::Log::WarnLog("Could not read fast external bodies rules " .
                              "from %s", $Externalfile);
    return;
  }

  my $counter = 0;
  while(<$fh>) {
    chomp;
    s/#.*$//;     # Remove comments
    s/\s+$//;     # and trailing whitespace
    s/^\s+//;     # and leading whitespace
    next if /^$/; # and blank lines
    ($dir, $address, $result) = split(" ", $_, 3);
    $Fastexternal{$address} = $result;
    $counter++;
  }

  MailScanner::Log::InfoLog("Read %d fast external bodies rules", $counter);
  #print STDERR "Read $counter rules\n";
}


sub InitFastFilename {
  my($dir, $address, $result);

  my $fh = new FileHandle;
  unless ($fh->open($Filenamefile)) {
    MailScanner::Log::WarnLog("Could not read fast filename-rules rules " .
                              "from %s", $Filenamefile);
    return;
  }

  my $counter = 0;
  while(<$fh>) {
    chomp;
    s/#.*$//;     # Remove comments
    s/\s+$//;     # and trailing whitespace
    s/^\s+//;     # and leading whitespace
    next if /^$/; # and blank lines
    ($dir, $address, $result) = split(" ", $_, 3);
    $Fastfilename{$address} = $result;
    $counter++;
  }

  MailScanner::Log::InfoLog("Read %d fast filename-rules rules", $counter);
  #print STDERR "Read $counter rules\n";
}


sub InitFastHtmlToText {
  my($dir, $address, $result);

  my $fh = new FileHandle;
  unless ($fh->open($Htmltotextfile)) {
    MailScanner::Log::WarnLog("Could not read fast HTML To Text rules " .
                              "from %s", $Htmltotextfile);
    return;
  }

  my $counter = 0;
  while(<$fh>) {
    chomp;
    s/#.*$//;     # Remove comments
    s/\s+$//;     # and trailing whitespace
    s/^\s+//;     # and leading whitespace
    next if /^$/; # and blank lines
    ($dir, $address, $result) = split(" ", $_, 3);
    $Fasthtmltotext{$address} = $result;
    $counter++;
  }

  MailScanner::Log::InfoLog("Read %d fast HTML To Text rules", $counter);
  #print STDERR "Read $counter rules\n";
}


sub InitFastIframe {
  my($dir, $address, $result);

  my $fh = new FileHandle;
  unless ($fh->open($Iframefile)) {
    MailScanner::Log::WarnLog("Could not read fast IFrame Tags rules " .
                              "from %s", $Iframefile);
    return;
  }

  my $counter = 0;
  while(<$fh>) {
    chomp;
    s/#.*$//;     # Remove comments
    s/\s+$//;     # and trailing whitespace
    s/^\s+//;     # and leading whitespace
    next if /^$/; # and blank lines
    ($dir, $address, $result) = split(" ", $_, 3);
    $Fastiframe{$address} = $result;
    $counter++;
  }

  MailScanner::Log::InfoLog("Read %d fast IFrame Tags rules", $counter);
  #print STDERR "Read $counter rules\n";
}


sub InitFastPartial {
  my($dir, $address, $result);

  my $fh = new FileHandle;
  unless ($fh->open($Partialfile)) {
    MailScanner::Log::WarnLog("Could not read fast partial message rules " .
                              "from %s", $Partialfile);
    return;
  }

  my $counter = 0;
  while(<$fh>) {
    chomp;
    s/#.*$//;     # Remove comments
    s/\s+$//;     # and trailing whitespace
    s/^\s+//;     # and leading whitespace
    next if /^$/; # and blank lines
    ($dir, $address, $result) = split(" ", $_, 3);
    $Fastpartial{$address} = $result;
    $counter++;
  }

  MailScanner::Log::InfoLog("Read %d fast partial message rules", $counter);
  #print STDERR "Read $counter rules\n";
}


#---------------------------------
# Now for all the lookup functions
#---------------------------------

# Get the list of unique RBLs for a given message that has lots of
# recipients. For each recipient, add the list for that user (or for
# that domain if there is no user-specific rule). Add these lists
# together and remove all the repeated entries.
sub FastSpamList {
  my($message) = @_;

  return "" unless $message; # Default if no message passed in

  my(%rbls, $rbllist, @rbllist, $to, $user, $domain);

  $rbllist = "";
  foreach $to (@{$message->{to}}, $message->{from}) {
    ($user, $domain) = split(/\@/, $to, 2);
    # Add the rbl list for the user if it exists,
    # otherwise add the rbl list for the domain.
    if ($Fastspamlist{$to}) {
      $rbllist .= " " . $Fastspamlist{$to};
    } else {
      $rbllist .= " " . $Fastspamlist{$domain};
    }
    #print STDERR "RBLList is now $rbllist\n";
  }
  # And the default
  $rbllist = $Fastspamlist{'default'} if $rbllist =~ /^\s*$/;

  @rbllist = split(" ", $rbllist);
  foreach (@rbllist) {
    $rbls{$_} = 1;
  }
  #print STDERR "Result is " . join(' ', keys %rbls) . "\n";
  return join(' ', keys %rbls);
}

sub FastSpamDomainList {
  my($message) = @_;

  return "" unless $message; # Default if no message passed in

  my(%rbls, $rbllist, @rbllist, $to, $user, $domain);

  $rbllist = "";
  foreach $to (@{$message->{to}}, $message->{from}) {
    ($user, $domain) = split(/\@/, $to, 2);
    # Add the rbl list for the user if it exists,
    # otherwise add the rbl list for the domain.
    if ($Fastspamlist{$to}) {
      $rbllist .= " " . $Fastspamdomainlist{$to};
    } else {
      $rbllist .= " " . $Fastspamdomainlist{$domain};
    }
  #print STDERR "RBLList is now $rbllist\n";
  }
  # And the default
  $rbllist = $Fastspamdomainlist{'default'} if $rbllist =~ /^\s*$/;

  @rbllist = split(" ", $rbllist);
  foreach (@rbllist) {
    $rbls{$_} = 1;
  }
  #print STDERR "Result is " . join(' ', keys %rbls) . "\n";
  return join(' ', keys %rbls);
}


sub FastCodebase {
  my($message) = @_;

  return 0 unless $message; # Default if no message passed in

  my($to, $user, $domain);
  my($codebase, $found);

  $codebase = 1; # Start by allowing it
  $found    = 0;

  # Test each of the exact addresses
  foreach $to (@{$message->{to}}, $message->{from}) {
    ($user, $domain) = split(/\@/, $to, 2);
    if (exists $Fastcodebase{$to}) {
      $found = 1;
      $codebase = $codebase && $Fastcodebase{$to};
      next; # Don't check domain default if exact address matched
    }
    if (exists $Fastcodebase{$domain}) {
      $found = 1;
      $codebase = $codebase && $Fastcodebase{$domain};
    }
  }
  return $codebase if $found;

  return $Fastcodebase{'default'} + 0; # Make it 0 if it's undef
}


sub FastExternal {
  my($message) = @_;

  return 0 unless $message; # Default if no message passed in

  my($to, $user, $domain);
  my($external, $found);

  $external = 1; # Start by allowing it
  $found    = 0;

  # Test each of the exact addresses
  foreach $to (@{$message->{to}}, $message->{from}) {
    ($user, $domain) = split(/\@/, $to, 2);
    if (exists $Fastexternal{$to}) {
      $found = 1;
      $external = $external && $Fastexternal{$to};
      next; # Don't check domain default if exact address matched
    }
    if (exists $Fastexternal{$domain}) {
      $found = 1;
      $external = $external && $Fastexternal{$domain};
    }
  }
  return $external if $found;

  return $Fastexternal{'default'} + 0; # Make it 0 if it's undef
}

sub FastFilename {
  my($message) = @_;

  return "" unless $message; # Default if no message passed in

  my(%filenames, $filenamelist, @filenamelist, $to, $user, $domain);

  $filenamelist = "";
  foreach $to (@{$message->{to}}, $message->{from}) {
    ($user, $domain) = split(/\@/, $to, 2);
    # Add the filename list for the user if it exists,
    # otherwise add the filename list for the domain.
    if ($Fastfilename{$to}) {
      $filenamelist .= " " . $Fastfilename{$to};
    } else {
      $filenamelist .= " " . $Fastfilename{$domain};
    }
  #print STDERR "Filenamelist is now $filenamelist\n";
  }
  # And the default
  $filenamelist = $Fastfilename{'default'} if $filenamelist =~ /^\s*$/;

  @filenamelist = split(" ", $filenamelist);
  foreach (@filenamelist) {
    $filenames{$_} = 1;
  }
  #print STDERR "Result is " . join(' ', keys %filenames) . "\n";
  return join(' ', keys %filenames);
}

sub FastHtmlToText {
  my($message) = @_;

  return 0 unless $message; # Default if no message passed in

  my($to, $user, $domain);
  my($htmltotext, $found);

  $htmltotext = 1; # Start by allowing it
  $found      = 0;

  # Test each of the exact addresses
  foreach $to (@{$message->{to}}, $message->{from}) {
    ($user, $domain) = split(/\@/, $to, 2);
    if (exists $Fasthtmltotext{$to}) {
      $found = 1;
      $htmltotext = $htmltotext && $Fasthtmltotext{$to};
      next; # Don't check domain default if exact address matched
    }
    if (exists $Fasthtmltotext{$domain}) {
      $found = 1;
      $htmltotext = $htmltotext && $Fasthtmltotext{$domain};
    }
  }
  return $htmltotext if $found;

  return $Fasthtmltotext{'default'} + 0; # Make it 0 if it's undef
}


sub FastIframe {
  my($message) = @_;

  return 0 unless $message; # Default if no message passed in

  my($to, $user, $domain);
  my($iframe, $found);

  $iframe = 1; # Start by allowing it
  $found  = 0;

  # Test each of the exact addresses
  foreach $to (@{$message->{to}}, $message->{from}) {
    ($user, $domain) = split(/\@/, $to, 2);
    if (exists $Fastiframe{$to}) {
      $found = 1;
      $iframe = $iframe && $Fastiframe{$to};
      next; # Don't check domain default if exact address matched
    }
    if (exists $Fastiframe{$domain}) {
      $found = 1;
      $iframe = $iframe && $Fastiframe{$domain};
    }
  }
  return $iframe if $found;

  return $Fastiframe{'default'} + 0; # Make it 0 if it's undef
}


sub FastPartial {
  my($message) = @_;

  return 0 unless $message; # Default if no message passed in

  my($to, $user, $domain);
  my($partial, $found);

  $partial = 1; # Start by allowing it
  $found   = 0;

  # Test each of the exact addresses
  foreach $to (@{$message->{to}}, $message->{from}) {
    ($user, $domain) = split(/\@/, $to, 2);
    if (exists $Fastpartial{$to}) {
      $found = 1;
      $partial = $partial && $Fastpartial{$to};
      next; # Don't check domain default if exact address matched
    }
    if (exists $Fastpartial{$domain}) {
      $found = 1;
      $partial = $partial && $Fastpartial{$domain};
    }
  }
  return $partial if $found;

  return $Fastpartial{'default'} + 0; # Make it 0 if it's undef
}


#----------------------------------
# Lastly all the shutdown functions
#----------------------------------

sub EndFastSpamList {
  # No shutdown code needed here at all.
  MailScanner::Log::InfoLog("Ending SpamList");
}

sub EndFastSpamDomainList {
  # No shutdown code needed here at all.
  MailScanner::Log::InfoLog("Ending SpamDomainList");
}

sub EndFastCodebase {
  # No shutdown code needed here at all.
  MailScanner::Log::InfoLog("Ending Codebase");
}

sub EndFastExternal {
  # No shutdown code needed here at all.
  MailScanner::Log::InfoLog("Ending External");
}

sub EndFastFilename {
  # No shutdown code needed here at all.
  MailScanner::Log::InfoLog("Ending Filename");
}

sub EndFastHtmlToText {
  # No shutdown code needed here at all.
  MailScanner::Log::InfoLog("Ending HtmlToText");
}

sub EndFastIframe {
  # No shutdown code needed here at all.
  MailScanner::Log::InfoLog("Ending Iframe");
}

sub EndFastPartial {
  # No shutdown code needed here at all.
  MailScanner::Log::InfoLog("Ending Partial");
}


1;

__DATA__

#
#
# This is the start of the IPBlock cron job, run this once an hour.
#
#

#!/usr/bin/perl -I/usr/lib/MailScanner

#
#   MailScanner - SMTP E-Mail Virus Scanner
#   Copyright (C) 2002  Julian Field
#
#   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
#
#   The author, Julian Field, can be contacted by email at
#      Jules@JulianField.net
#   or by paper mail at
#      Julian Field
#      Dept of Electronics & Computer Science
#      University of Southampton
#      Southampton
#      SO17 1BJ
#      United Kingdom
#
push @INC,"/usr/lib/MailScanner","/opt/MailScanner/lib";
use FileHandle;
use Fcntl qw(:DEFAULT :flock);
use Sys::Syslog;
BEGIN { @AnyDBM_File::ISA = qw(DB_File GDBM_File NDBM_File SDBM_File) }
use AnyDBM_File;
require MailScanner::Config;
use strict 'vars';
use strict 'refs';
no  strict 'subs'; # Allow bare words for parameter %'s

my $FailCode = '550';

my $ConfFile = $ARGV[0];
die "Usage : <scriptname> /path/to/MailScanner.conf\n" if($ConfFile eq "");

my ($AccessDB, $Refusal, $my_mta); # Generalized here for Multiple MTA support
$my_mta = lc(MailScanner::Config::QuickPeek($ConfFile, 'mta'));
if( $my_mta eq "postfix" ) {

	$AccessDB = "/etc/postfix/access.db";
	$Refusal = "$FailCode Site blocked by MailScanner due to excessive email";

}elsif( $my_mta eq "sendmail"){

	$AccessDB = "/etc/mail/access.db";
	$Refusal = "\"$FailCode Site blocked by MailScanner due to excessive email\"";

}else{
# Set some values for these variables	
	$AccessDB = "";
	$Refusal = "";
	print STDERR "IPBlock is currently not supported for your MTA\n";
	exit 0; # No need to do further processing if I don't know the MTA
}



my $OneHour      = 3600; # seconds
my $LockFile     = '/var/spool/MailScanner/IPBlock.lock';
#my $LogFile     = '/var/spool/MailScanner/IPBlock.log';
my $BlockDB      = '/var/spool/MailScanner/IPBlock.db';
#my $AccessDB     = '/etc/mail/access.db';
#my $Refusal      = '"451 Site blocked by MailScanner due to excessive email"';

# Start logging
Sys::Syslog::openlog("IPBlock", 'pid, nowait', 'mail');

#
# Lock out everything else for the whole of this script
#
my $LockFileH = new FileHandle;
openlock($LockFileH, ">$LockFile");

#
# Find all the entries to be deleted from the BlockDB file.
#

#Bind to BlockDB
my(%BlockDB, %AccessDB);
tie %BlockDB, "AnyDBM_File", $BlockDB, O_RDWR, 0644
  or BailOut("Failed to open $BlockDB, it may not exist yet, $!");
  
tie %AccessDB, "AnyDBM_File", $AccessDB, O_RDWR, 0644
  or BailOut("Failed to open $AccessDB, have you got the path wrong? $!");

# Read through the entire DB finding all the old records
my $now = time;
my(%DeleteMe, $ip, $value, $hostname, $count, $time, $donealready);
my $countrec = 0;
my $countdel = 0;
my $maxcount = 0;
my $maxip    = 'undefined';
while(($ip, $value) = each %BlockDB) {
  ($hostname, $count, $time, $donealready) = split(/,/, $value);
  # Is it more than an hour old, or has time_t wrapped (happens in year 2036)
  #print STDERR "Examining record for $ip, $count, $time\n";
  $countrec++;
  if ($count > $maxcount) {
    $maxcount = $count;
    $maxip = $ip;
  }
  if ($time>$now || $now>=$time+$OneHour) {
    $DeleteMe{$ip} = 1;
    $DeleteMe{$hostname} = 1;
    #print STDERR "Deleting old record for $ip\n";
    $countdel++;
  }
}

print STDERR "IPBlock cronjob: $countrec DB records examined, $countdel " .
             "old records deleted\n";
print STDERR "IPBlock cronjob: maximum: $maxcount emails from $maxip\n";

# Delete all the old db entries
my $counter = 0;
while(($ip, $value) = each %DeleteMe) {
  #print STDERR "Studying IP \"$ip\"\n";
  next unless $ip;
  #print STDERR "AccessDB is \"" . $AccessDB{$ip} . "\"\n";
  delete $BlockDB{$ip};
  if ($AccessDB{$ip} eq $Refusal) {
    delete $AccessDB{$ip};
    $counter++;
  }
  #delete $AccessDB{$hostname} if $AccessDB{$hostname} eq $Refusal;
}

# Unlock and close the DB file
untie %BlockDB;
untie %AccessDB;

Sys::Syslog::syslog('info', "Deleted $counter entries from sendmail access database");

unlockclose($LockFileH);

Sys::Syslog::closelog();
exit 0;

sub openlock {
  my($fh, $fn) = @_;

  if (open($fh, $fn)) {
    flock($fh, LOCK_EX) or die;
  } else {
    die "Died opening $fn, $!";
  }
}

sub unlockclose {
  my($fh) = @_;

  flock($fh, LOCK_UN);
  close ($fh);
}

sub BailOut {
  Sys::Syslog::syslog('err', @_);
  Sys::Syslog::closelog();
  warn "@_, $!";
  exit 1;
}

#---SNIP---

#
#
# This is the end of the IPBlock cron job
#
#

