#!/usr/bin/perl

# $Id: cgpsa,v 1.68 2010/02/05 22:37:26 dmz Exp $

# CGPSA - A Spam Filter for CommuniGate Pro - Version 1.7b1
# Copyright (C) 2002-2010 TFF Enterprises
# Written by Daniel M. Zimmerman
# All Rights Reserved
#
# This CommuniGate Pro filter uses SpamAssassin technology to scan messages
# being transferred by the mail server. It has two primary operating modes, 
# "full-featured" and "headers-only".
#
# In "full-featured" mode, the filter checks the recipients of each message 
# (using the CGP router, through the CGP CLI) to determine which are local 
# and which are remote, and scans messages accordingly (depending on your 
# configuration). In this mode, it has the ability to use per-user 
# preferences, which are stored in the user's CommuniGate Pro account 
# directory, as well as to use systemwide preferences (either exclusively or 
# just for those users that don't have individual preferences), and performs 
# one SpamAssassin check per preferences set applicable to a given message. 
# It can also use either per-user or systemwide state (for Razor, 
# auto-whitelists, Bayesian filtering, etc) in this mode. This mode works by 
# resubmitting messages using the CommuniGate PIPE Module.
#
# In "headers-only" mode, the filter scans all messages with the same 
# set of SpamAssassin preferences and adds headers to each message based on 
# the results of the scan; all recipients, local and remote, see exactly the 
# same added headers. This mode does not resubmit messages at all, and is 
# therefore faster, but is also much less flexible as a result.
#
# In either mode, the filter can safely be used with multiple CommuniGate 
# Pro enqueuer threads; if you have CLI usage enabled, your server must be 
# set to allow at least as many concurrent PWD connections as you have 
# enqueuer threads (preferably a couple more, for safety margin).
#
# Information on user-configurable options is available in the sample 
# configuration file supplied with the filter, which must be installed as
# "<your CGP base directory>/Settings/cgpsa.conf" (unless you
# change the "$root_config_directory" or "$root_config_pathname" variable below). 
# Note that you _must_ properly set the "$cgp_base" variable below for this
# filter to work at all.

# CGPSA requires Perl 5.6.1 or higher to run.

require 5.6.1;

#
# Customizable Variables
# Many other customizations are possible, but they are all carried out
# through the configuration file.
#

# Location of the CommuniGate Pro base directory. This is used,
# among other things, for finding the configuration file and the
# Submitted directory. It must have a trailing "/".

# Unix
our $cgp_base = "/var/CommuniGate/";

# Windows
#our $cgp_base = "c:/CommuniGate Files/";

# Location for the directory containing the configuration and lock files, 
# relative to the CommuniGate Pro base directory. It must have a trailing
# "/".

our $root_config_directory = "Settings/";

# Location for the directory in each domain that contains the CGPSA
# configuration file, relative to the domain's directory within the 
# CommuniGate Pro filesystem. It must have a trailing "/".

our $domain_config_directory = "Settings/";

# Full paths to the configuration, lock, and standard error files.

our $root_config_pathname = $cgp_base.$root_config_directory."cgpsa.conf";
our $lock_pathname = $cgp_base.$root_config_directory."cgpsa.lock";
our $error_pathname = $cgp_base.$root_config_directory."cgpsa.err";

# Path components for domain configuration files. The domain prefix is
# the path up to the domain name, including a trailing "/"; it is only
# used in cases where the domain location cannot be determined from the CLI.
# The domain suffix is the path to a domain configuration file starting 
# after the domain name, including a leading "/".

our $legacy_domain_prefix = "Domains/";
our $domain_config_filename = "cgpsa.domainconf";
our $domain_config_suffix = 
    "/".$domain_config_directory.$domain_config_filename;

# The path to append to a home directory to get the SpamAssassin preference
# file, in components.

our $sa_prefs_dir_suffix = "/.spamassassin";
our $sa_prefs_filename_suffix = "/user_prefs";
our $sa_prefs_suffix = $sa_prefs_dir_suffix.$sa_prefs_filename_suffix;

# The name to use for CGPSA user configuration files. This name will be
# appended to the path to the user's "account.web" directory to find their
# CGPSA configuration.

our $user_config_filename = ".cgpsa.conf";

#
# End of Customizable Variables
# Don't modify anything below this line unless you're absolutely
# sure you know what you're doing.
#

use strict;
use Sys::Hostname;
use POSIX ":sys_wait_h";
use Fcntl qw(:DEFAULT :flock); # import LOCK_* constants
use Mail::SpamAssassin;
use Text::Wrap;

# Signal handlers

$SIG{'HUP'} = \&signalHUP;

# Version string for this filter.

our $VERSION = "1.7b1";

# Version information header to be added to messages (includes platform,
# perl version, hostname; primarily used for debugging)

our $VERSIONHEADERNAME = "X-TFF-CGPSA-Version";
our $VERSIONHEADERTEXT = "$VERSION";

# Other constants

our $SQLSTRING = "<sql>";
our $DEFAULTSTRING = "<default>";

# Set standard output to autoflush.

select(STDOUT);
$|=1;

# Set up Text::Wrap

$Text::Wrap::columns = 100;
if ($Text::Wrap::VERSION >= 2001.0929)
{
  $Text::Wrap::tabstop = 4;
  $Text::Wrap::unexpand = 0;
}
  
# Clean up any old lock file that might exist.

unlink <$lock_pathname>;
 
# Print a hello.

print("* TFF Enterprises CGPSA Filter $VERSION Starting\n");

# Declare some "global" variables.

our %root_conf_hash = ();
our $root_conf = \%root_conf_hash;
our %domain_conf_cache = ();
our %domain_dir_cache = ();
our %user_conf_cache = ();
our @scan_domains = undef;
our @headers_to_include = undef;
our $spamassassin = undef;
our $fresh_spamassassin_conf = undef;

# Load Time::HiRes if available

$root_conf->{have_time_hires} = 0;

eval 
{ 
  require Time::HiRes; 
  $root_conf->{have_time_hires} = 1;
};

# Do all the initialization stuff we have to do.

initialize();

# And, we're finally read for the Main Loop!

mainLoop();

#
# Subroutines
#

# Catches a SIGHUP signal, and sets a flag accordingly. 

sub signalHUP
{
  our $hup_received = 1;
}

# Handles a SIGHUP, by reloading the preferences file and creating
# a new SpamAssassin. This is called by the main event loop, when a
# HUP has been detected.

sub processHUP
{
  our %pids;
  my $pid;
  
  # Give child processes a chance to be cleaned up...
    
  foreach $pid (keys %pids)
  {
    waitpid($pid, 0); # blocking wait
    delete $pids{$pid};
  }

  lockStandardOutput();
    
  select(STDERR);
  print("* ".scalar localtime().
        " HUP Signal Received, Re-initializing CGPSA.\n");
  select(STDOUT);

  if ($root_conf->{redirect_stderr})
  {
    # stderr is redirected, so output to stdout as well
    print("* HUP Signal Received, Re-initializing CGPSA.\n");
  }

  our $spamassassin;
  $spamassassin->finish_learner();
  undef $spamassassin;
  undef our @scan_domains;
  undef our %root_conf_hash;
  undef our $root_conf;
  undef our %domain_conf_cache;
  undef our %domain_dir_cache;
  undef our %user_conf_cache;
  
  initialize();
  unlockStandardOutput();
  
  our $hup_received = 0;
}
 
# Loads the CGPSA preferences, and performs initialization tasks. This 
# routine assumes that it has exclusive access to standard output.
# Therefore, calls to it must occur in a context where there are
# guaranteed to be no forked processes running (i.e., startup), or 
# must be bracketed by a lock/unlockStandardOutput pair.

sub initialize
{
  our %domain_dir_cache;
  our $root_config_pathname;
  our $root_conf;
  our $VERSION;
  our $sa_prefs_dir_suffix;
  our $sa_prefs_suffix;
  
  # The path to the "" domain is always "Accounts"

  $domain_dir_cache{""} = "Accounts";

  print("* Loading Configuration File ".$root_config_pathname."\n");
  loadRootConfig();
  if ($root_conf->{no_configuration})
  {
    # Exit if there's no configuration, since we can't do anything useful.
    
    error_exit("Unable to read CGPSA configuration file, exiting.");
  }
  loadDomainConfig("");
  print("* Configuration File Loaded\n");
  
  print("* Parallel Requests Mode: ");
  if ($root_conf->{parallel_requests})
  {
    print("On\n");
  }
  else
  {
    print("Off\n");
  }
  
  if ($root_conf->{max_requests} > 0)
  {
    print("* Max Requests: $root_conf->{max_requests} - Be sure ". 
          "Auto-Restart is not disabled in CGP Helper Settings.\n");
  }
  
  # Redirect standard error to a file, if option is set.
  
  if ($root_conf->{redirect_stderr})
  {
    close(STDERR);
    open(STDERR, ">>$error_pathname");
    select(STDERR);
    $|=1;
    print("* ".scalar localtime()." Standard Error Redirected By ".
          "TFF Enterprises CGPSA Filter $VERSION\n");
    select(STDOUT);
  }
  
  # Locale change, to "C", for SpamAssassin's well-being, if option is set.
  
  if ($root_conf->{use_c_locale})
  {
    use POSIX qw(locale_h);
    $ENV{'LANG'} = "C";
    setlocale(LC_ALL, "C");
    $ENV{'LC_ALL'} = "C";
  }
  
  # We should set $ENV{'HOME'} to the default home directory, to prevent 
  # creating files anywhere else in the system aside from our CGPro user 
  # dirs and the default home directory. We also set $ENV{'PATH'}, to avoid 
  # problems with Perl's Taint Mode.
  
  $ENV{'HOME'} = $root_conf->{default_home_dir}; 
  $ENV{'PATH'} = $root_conf->{helper_path};
  
  if (!(-r $root_conf->{default_home_dir} && -w _ && -x _ && -d _))
  {
    mkdir("$ENV{'HOME'}", 0700)
      or error_exit("Can't create default home directory ".$ENV{'HOME'}.
                    ", exiting.");
  }
  
  if (!(-r $root_conf->{default_home_dir}.$sa_prefs_dir_suffix && 
        -w _ && -x _ && -d _))
  {
    mkdir("$ENV{'HOME'}$sa_prefs_dir_suffix", 0700)
      or error_exit ("Can't create directory ".$ENV{'HOME'}.
                     $sa_prefs_dir_suffix.", exiting.");
  }
  
  # Create a SpamAssassin, using the configuration parameters we've
  # read and the system defaults.
  
  # first, determine the version
  
  if ($Mail::SpamAssassin::VERSION < 3.0)
  {
    error_exit("CGPSA requires SpamAssassin version 3.0 or higher ". 
               "(installed SpamAssassin is version ". 
               $Mail::SpamAssassin::VERSION.")");
  }
  elsif ($Mail::SpamAssassin::VERSION < 3.003)  # prior to 3.3.0
  {
    $root_conf->{spamassassin_api_version} = 3;
  }
  else # 3.3.0 or higher
  {
    $root_conf->{spamassassin_api_version} = 3.3;
  }
  
  $spamassassin = Mail::SpamAssassin->new
    ({
      dont_copy_prefs => 1,
      local_tests_only => !($root_conf->{do_network_tests}),
      stop_at_threshold => $root_conf->{stop_at_threshold},
      home_dir_for_helpers => $root_conf->{helper_state_dir},
      userstate_dir => 
        $root_conf->{default_home_dir}.$sa_prefs_dir_suffix,
      debug => $root_conf->{sa_debug},
      PREFIX => $root_conf->{sa_prefix},
      DEF_RULES_DIR => $root_conf->{sa_default_rules_dir},
      LOCAL_RULES_DIR => $root_conf->{sa_local_rules_dir}
     });
    
  # Set up persistent address list factories, for auto-whitelisting.
  
  require Mail::SpamAssassin::DBBasedAddrList;
  require Mail::SpamAssassin::SQLBasedAddrList;
  our $dbaddrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
  our $sqladdrlistfactory = Mail::SpamAssassin::SQLBasedAddrList->new();
    
  # If we're not using domain or user prefs, finish setting up 
  # SpamAssassin...
  
  if (!$root_conf->{use_domain_prefs} && !$root_conf->{use_user_prefs})
  {
    if ($root_conf->{use_auto_whitelist})
    {
      if ($root_conf->{sql_auto_whitelist})
      {
        $spamassassin->set_persistent_address_list_factory
          ($sqladdrlistfactory);
      }
      else
      {
        $spamassassin->set_persistent_address_list_factory
          ($dbaddrlistfactory);
      }
    }
    if (-e "$root_conf->{default_home_dir}$sa_prefs_suffix")
    {
      $spamassassin->read_scoreonly_config
        ($root_conf->{default_home_dir}.$sa_prefs_suffix);
    }
  }
  
  # Compile the SpamAssassin so we don't have to do it over and over again 
  # later. If user or domain prefs are being used, we'll read them as we 
  # process messages...
  
  $spamassassin->compile_now(0);
  $/ = "\n";      # argh, Razor resets this!  Bad Razor! (from spamd.raw)
  
  print("* Using SpamAssassin Version ".Mail::SpamAssassin::Version().
        " ($Mail::SpamAssassin::SUB_VERSION)\n");
  
  if ($root_conf->{sa_prefix} ne "")
  {
    print("* Custom SpamAssassin Installation Prefix: \n*     ".
          $root_conf->{sa_prefix}."\n");
  }
  
  if ($root_conf->{sa_default_rules_dir} ne "")
  {
    print("* Custom SpamAssassin Default Rules Directory: \n*     ".
          $root_conf->{sa_default_rules_dir}."\n");
  }
  
  if ($root_conf->{sa_local_rules_dir} ne "")
  {
    print("* Custom SpamAssassin Local Rules Directory: \n*     ".
          $root_conf->{sa_local_rules_dir}."\n");
  }
  
  if ($root_conf->{helper_path} ne "")
  {
    print("* SpamAssassin Helper Path: \n*     ".
          $root_conf->{helper_path}."\n");
  }
  
  print("* Default SpamAssassin Settings Location: \n");
  if (-e "$root_conf->{default_home_dir}$sa_prefs_suffix")
  {
    print("*     ".$root_conf->{default_home_dir}.$sa_prefs_suffix."\n");
  }
  else
  {
    print("*     (local.cf settings only)\n");
  }
  print("* Initialization Complete\n");
}


# The Main Loop. Repeatedly reads from stdin, and handles CGP's
# commands appropriately. This function exits when it completes.

sub mainLoop
{
  print("* TFF Enterprises CGPSA Filter $VERSION Ready\n");
  
  my $request = <STDIN>;
  my $request_count = 0;
  our $hup_received;
  our %pids;
  my $pid;
  my $cli_initialized = 0;
  our $seqnum; # global, so it can be printed in debug lines
  
  while ($request) 
  {
    if ($hup_received)
    {
      # We've received a SIGHUP since our last time around; let's
      # deal with it.
      
      processHUP();
    }

    # Give child processes a chance to be cleaned up...
  
    foreach $pid (keys %pids)
    {
      if (waitpid($pid, &WNOHANG) == -1)
      {
        # child $pid has terminated
        delete $pids{$pid};
      }
    }
    
    if ($request =~ /([0-9]+)\s*([A-Za-z]+)\s*(.*)/si) 
    {
      $seqnum = $1;
      my $command = $2;
      my $parameter = $3;
        
      if ($command =~ /INTF/i) 
      {
        # CGPro wants to know our interface version (it's 2, because 
        # we use ADDHEADER). After we tell it so, we check for 
        # availability of the CGP CLI with respect to our 
        # configuration (assuming we haven't already). 
        printResult("INTF 2");      
        if (!$cli_initialized)
        {
          initializeCLI();
          $cli_initialized = 1;
        }              
      }
      elsif ($command =~ /FILE/i) 
      { 
        # CGPro is giving us a file to examine
        $request_count = $request_count + 1;
        
        # Fork off a process, if we're running with parallel requests
        $pid = 0;
        if ($root_conf->{parallel_requests})
        {         
          # generate a random number; this compensates for the fact that
          # the new process' random number generator is just initialized
          # from this process's due to shared memory.
          rand(); 
          $pid = fork();
        }
        if ($pid == 0) 
        {
          process_file($parameter);
          if ($root_conf->{parallel_requests})
          {
            # We're a finished child process, so we must...
            exit();
          }
        }
        else 
        {
          $pids{$pid} = 1;
        } 
      }
      elsif ($command =~ /QUIT/i)
      {
        # CGPro wants us to quit
        lockStandardOutput();
        print("* QUIT message received, exiting.\n");
        unlockStandardOutput();
        last;
      }
      else 
      {
        # CGPro gave us an unknown command
        debug(1, "Unknown Command $2");
        printResult("OK");
      }
    }

    if (($root_conf->{max_requests} > 0) && 
        ($request_count >= $root_conf->{max_requests}))
    {
      lockStandardOutput();
      print("* Handled maximum number of requests (".
            $root_conf->{max_requests}."), exiting\n");
      unlockStandardOutput();
      last;
    }
                
    $request = <STDIN>;
  }
  
  # And now we're done. We need to exit immediately.
  # We lock stdout here, just in case some children are still running...
  
  lockStandardOutput();
  print("* TFF Enterprises CGPSA Filter $VERSION Done\n");
  unlockStandardOutput();
  
  exit();
}


# Initializes the CommuniGate Pro CLI. This checks the version of the 
# CommuniGate Pro CLI, to see which form of the Route command to use. If the 
# CLI is not being used, this routine is essentially a no-op. 

sub initializeCLI
{
  if ($root_conf->{use_cli})
  {
    eval { require CGP::CLI; };
     
    if ($@)
    {
      # CGP::CLI didn't load from CGP/CLI...
  
      eval { require CLI; };
  
      if ($@)
      {
        # OK, it's not anywhere...
        error_exit("Current CGPSA configuration requires CGP::CLI module.");
      }
    }
     
    debug(1, "Checking for CommuniGate Pro CLI Access");
    
    my $attempts = 0;
    my $cli = undef;
    
    while (!$cli)
    {
      $cli = new CGP::CLI
        ({ PeerAddr => $root_conf->{cgp_hostname},
           PeerPort => $root_conf->{cgp_port},
           login    => $root_conf->{cgp_username},
           password => $root_conf->{cgp_password}
         });
     
      if (!$cli)
      {
        debug(1, "Could not connect to CLI (Error: ".
                 $CGP::ERR_STRING.")");
        $attempts = $attempts + 1;
        if ($attempts < 3)
        {
          debug(1, "Sleeping for 20 seconds before next attempt");
          sleep 20;
        }
        else
        {
          debug(1, "Too many failed attempts");
          error_exit("Could not connect to CLI, exiting");
        }
      }
    }
    
    # Check to see if the installed CommuniGate supports the "mail"
    # extension the routing CLI command.
    
    my $data=$cli->Route("postmaster MAIL");
    if ($cli->isSuccess)
    {
      # Support for the extension
      $root_conf->{route_mail_extension} = 1;
      debug(2, "CGP supports ROUTE ... MAIL extension");
    }
    else
    {
      # No support for the extension
      $root_conf->{route_mail_extension} = 0;
      debug(2, "CGP does not support ROUTE ... MAIL extension");
    }
    
    # Check to see if the installed CommuniGate (and CLI) support the
    # blacklist and whitelist functions
    
    $root_conf->{temp_blacklist_support} = 0;
    
    eval 
    {
      $cli->GetWhiteHoleIPs();
      
      if ($cli->isSuccess)
      {
        $cli->GetTempBlacklistedIPs();
      
        if ($cli->isSuccess)
        {
          # support for blacklist/whitelist functions exists
          $root_conf->{temp_blacklist_support} = 1;
          debug(2, "CLI and CGP support temporary blacklisting");
        }
        else
        {
          debug(2, "CGP does not support temporary blacklisting");
        }
      }
      else
      {
        debug(2, "CGP does not support temporary blacklisting");
      }
    };
    
    if ($@)
    {
      debug(2, "CLI does not support temporary blacklisting");
    }
    
    $cli->Logout();
    undef $cli;
  }
}


# Finds or creates a mailbox belonging to the specified user and having
# the specified name, using the specified CLI. 

sub findOrCreateMailbox
{
  my ($account_name, $mailbox_name, $cli_ptr) = @_;
  my $cli = ${$cli_ptr};
  my $result = 0;
  
  debug(7, "Attempting to find mailbox ".$mailbox_name." in account ".
           $account_name);
  
  # try to create the mailbox
           
  lockStandardOutput();
  $result = $cli->CreateMailbox($account_name, $mailbox_name);
  unlockStandardOutput();
  
  if (!$result)
  {
    # something went wrong, but what?
    
    if ($cli->getErrCode() == 532)
    {
      # the mailbox already exists
      debug(7, "Mailbox ".$mailbox_name." already exists");
      $result = 1;
    }
    else
    {
      # we couldn't create the mailbox
      debug(7, "Mailbox ".$mailbox_name." could not be created");
    }
  }
  else
  { 
    # the mailbox already existed
    debug(7, "Mailbox ".$mailbox_name." created");
    $result = 1;
  }
  
  return $result;
}
   
    
# Submit a message using the CommmuniGate Pro "Submitted" directory.
# Parameters are a pointer to the CommuniGate Pro CLI, a pointer to the
# message body, a return-path, an destination account name, and a list of 
# envelope-to addresses. If the destination account name is not "", we
# attempt to do DMA delivery to the spam mailbox of the specified 
# destination account, if the various preference files tell us to do so.
# If the destination account is not "", only the first of the addresses
# is used.

sub submitMessage 
{
  my ($cli_ptr, $msg_ptr, $return_path, $account_name, @addresses) = @_;
  my $cli = ${$cli_ptr};
  my $filename = our $cgp_base."Submitted/".getUniqueFilename(@addresses);
  my $orig_address = "";
  my $address_changed = 0;
    
  if ($account_name ne "")
  {
    # we need to attempt direct mailbox addressing to a spambox
    # let's first get the domain and user prefs (both must have been 
    # previously loaded) for the first element of the address list

    my $address = @addresses[0];
    
    # strip quotation marks out of the local address (we'll replace them
    # later, if necessary)
              
    $address =~ s/\"//g;

    my $domain_name = "";
    if ($account_name =~ /\S*@(\S*)/)
    {
      $domain_name = $1;
    }
    my $user_conf = $user_conf_cache{$account_name};
    my $receiver_address = "";
    
    if ($cli && $root_conf->{cli_account_info} && 
        $user_conf->{use_dma_spam_mailbox})
    {
      # Verify that DMA is enabled on the server
      
      my $local_settings;
      if (my $local_settings = $cli->GetModule("LOCAL"))
      {
        my %local_settings_hash = %{$local_settings};
        if ($local_settings_hash{DirectMailboxes} eq "YES")
        {
          # DMA is enabled! Create the DMA spam mailbox if necessary
          
          debug(7, "DMA spam filing enabled for ".$address);
          if (findOrCreateMailbox
                ($account_name, $user_conf->{spam_mailbox_name}, \$cli))
          {
            # we found or created the mailbox, so let's modify the
            # local address accordingly
         
            $receiver_address = $address;
            
            if ($receiver_address =~ /^(.*)\+(\S*)@(\S*)$/)
            {
              # it has an account detail; we need to get rid of that, 
              # for DMA to work reliably
              
              $orig_address = $address;
              $address_changed = 1;
              $receiver_address = $1."@".$3;
            }
            
            if ($receiver_address =~ /^(.*)#(\S*)@(\S*)$/)
            {
              # it already has a DMA, so we remove it and put it in
              # an X-Original-To header
              
              if ($address_changed == 0)
              {
                $address_changed = 1;
                $orig_address = $address;
              }
              
              $receiver_address = 
                "\"".$user_conf->{spam_mailbox_name}."#".$2."\"@".$3;
            }
            elsif ($receiver_address =~ /^(\S*)@(\S*)$/)
            {
              # it's got a domain
              
              $receiver_address = 
                "\"".$user_conf->{spam_mailbox_name}."#".$1."\"@".$2;
            }
            else
            {
              # it's got no domain
              
              $receiver_address = 
                "\"".$user_conf->{spam_mailbox_name}."#".$address."\"";
            }
        
            @addresses = ();
            push(@addresses, $receiver_address);
          }
          else
          {
            debug(7, "Failed to create or locate DMA spam mailbox for ".
                     $address);
          }
        }
      }
      else
      {
        debug(7, "Could not determine if DMA enabled on server");
      }
    }
  }
  
  open(OUTFILE, ">$filename");
  print OUTFILE "Return-Path: <".$return_path.">\n";
  foreach my $local_addr (@addresses) 
  {
    print OUTFILE "Envelope-To: <".$local_addr.">\n";
    if ($address_changed != 0)
    {
      print OUTFILE "X-TFF-CGPSA-Original-To: ".$orig_address."\n";
    }
  }
  print OUTFILE @$msg_ptr;
  close(OUTFILE);
  rename($filename, $filename.".sub");
}
    
  
# Load information for the specified user using the specified CommuniGate 
# Pro CLI. This determines the "effective home directory" for SpamAssassin,
# based on the root configuration, the domain configuration (if any), the
# user configuration (if any), and the presence or absence of a 
# ".spamassassin/user_prefs" file in the user's CommuniGate Pro 
# "account.web" directory.  A return value of undef means that SpamAssassin 
# should not be run for the specified user.

sub getUserInformation
{
  my ($cli_ptr, $account_name) = @_;
  my $cli = ${$cli_ptr};
  my $account_dir = $cli->GetAccountLocation($account_name);
  my $domain_name = "";
  
  if ($account_name =~ /\S*@(\S*)/)
  {
    $domain_name = $1;
  }
  
  # construct the full path to the account directory
  
  $account_dir = $cgp_base.$domain_dir_cache{$domain_name}."/".$account_dir;

  # get the user's configuration (this also loads the domain's configuration
  # because it needs it to do the configuration inheritance).
  
  loadUserConfig($account_name, $account_dir."/account.web/");    
  my $domain_conf = $domain_conf_cache{$domain_name};
  my $user_conf = $user_conf_cache{$account_name};

  
  # decide whether to use the user's preferences or not
  
  if ($domain_conf->{allow_user_prefs} && $user_conf->{use_user_prefs} &&  
      (-e "$account_dir/account.web$sa_prefs_suffix"))
  {
    # the user has preferences, so let's use them
    
    $account_dir = "$account_dir/account.web";
  }
  elsif (!$domain_conf->{require_user_prefs})
  {
    if ($domain_conf->{use_domain_prefs})
    {
      # the domain's default home directory is the effective home 
      # directory
      
      $account_dir = $domain_conf->{default_home_dir};
    }
    else
    {
      # the global default home directory is the effective home directory
      
      $account_dir = $root_conf->{default_home_dir};
    }
  }
  else
  {
    # undef is the effective home directory
    
    $account_dir = undef;
  }
  
  return $account_dir;
}

# Check the specified message with SpamAssassin, and add appropriate headers
# to it using CGP's "ADDHEADER" functionality. This is used when the "add 
# header with CGP filter mechanism" option is turned on in the preferences.

sub checkMessageAddHeader
{
  my ($msg_ptr, $score_ptr) = @_;
  my $msg_length = 0;
  my @scanlines;
  my $start = getTime();
  my $newheaders = undef;
  my $mail;
  
  # if the message is longer than 128K characters, scan only the first
  # 128K characters
  
  foreach my $line (@$msg_ptr)
  {
    $msg_length = $msg_length + length($line);
    if ($msg_length < $root_conf->{max_scan_length})
    {
      push(@scanlines, $line);
    }
    else
    {
      debug(5, "Scanning first ".$root_conf->{max_scan_length}.
               " bytes of message only");
      last;
    }
  }

  # Strip all previous SpamAssassin markup from the message

  $mail = $spamassassin->parse([@scanlines], 1);

  my @scanlines = 
    split (/^/m, $spamassassin->remove_spamassassin_markup($mail));

  $mail = $spamassassin->parse([@scanlines], 1);

  # Let's do SpamAssassin! No need to read config, because we must have read
  # it already... Similarly, no need to set state directory.
  
  eval 
  {
    if (my $status = $spamassassin->check($mail))
    {
      my $was_it_spam;
      my $spamd_log_line = "result: ";
      
      if ($status->is_spam()) 
      { 
        $was_it_spam = "Identified spam"; 
        $spamd_log_line .= "Y ";
      } 
      else 
      { 
        $was_it_spam = "Identified non-spam"; 
        $spamd_log_line .= ". ";
      }
      my $msg_score = sprintf("%.1f", $status->get_hits());
      my $msg_threshold = sprintf("%.1f", $status->get_required_hits());
      ${$score_ptr} = $status->get_hits();
      
      debug(4, "$was_it_spam ($msg_score/$msg_threshold) in ".
               sprintf("%.1f", getTime() - $start)." seconds");

      if ($root_conf->{spamd_style_log})
      {
        # print the spamd-style log to standard error
        
        $spamd_log_line .= sprintf("%d", int($status->get_hits() + 0.5));
        $spamd_log_line .= " - ".$status->get_names_of_tests_hit();
        $spamd_log_line .= " scantime=".sprintf("%.1f", getTime() - $start);
        $spamd_log_line .= ",size=".$msg_length;
        $spamd_log_line .= ",mid=";
        $spamd_log_line .= ",user=global";
        $spamd_log_line .= ",bayes=";
        $spamd_log_line .= ",autolearn=".$status->get_autolearn_status();
        select(STDERR);
        print("* ".$spamd_log_line."\n");
        select(STDOUT);
      }
      
      # Add headers. This is very similar to the way SpamAssassin actually
      # does it, and we respect all of SpamAssassin's preferences (because
      # it's a good thing to do, and prevents confusion). 
      
      my $added_sa_version_header = 0;
      
      my $rewritten_mail_scalar = $status->rewrite_mail();
      
      $newheaders = "";
      my $sa_headers;
      my $headerlist;
      
      if ($status->is_spam())
      {
        $sa_headers = $status->{conf}->{headers_spam};
      }
      else
      {
        $sa_headers = $status->{conf}->{headers_ham};
      }

      my $rewritten_mail = $spamassassin->parse($rewritten_mail_scalar);
      
      if ($root_conf->{spamassassin_api_version} == 3.3)
      {
        # SpamAssassin 3.3 does something different with its header structure
        
        foreach my $hf_ref (@{$sa_headers}) 
        {
          my($hfname, $hfbody) = @$hf_ref;
          $headerlist->{$hfname} = $hfbody;
        }
      }
      else
      {
        $headerlist = $sa_headers;
      }
      
      foreach my $header (keys %{$headerlist})
      {
        $newheaders .= 
          "X-Spam-".$header.": ".
          $rewritten_mail->get_pristine_header("X-Spam-".$header).
          "\e";
      }
        
      if (!(exists $headerlist->{"Checker-Version"}))
      {
        # add X-Spam-Checker-Version if it isn't there already

        $newheaders .= "X-Spam-Checker-Version: SpamAssassin ".
                       Mail::SpamAssassin::Version().
                       " ($Mail::SpamAssassin::SUB_VERSION)\e";
      }  
        
      # always add CGPSA Version header and loop prevention header
      
      $newheaders .= mandatory_headers("Scanned");
      
      # CommuniGate Pro can only take a limited-size response from the 
      # plug-in (4K, in the versions of CommuniGate Pro released to date); 
      # let's see if our headers fit within that (taking into account the
      # "<number> ADDHEADER" part of the response)
      
      if (cgp_string_length($newheaders) >
          $root_conf->{cgp_max_response_length})
      {
        # the headers are too long, so we try to reduce the length of
        # one SpamAssassin header
        
        debug(9, "expanded header length ".
                 cgp_string_length($newheaders).
                 " exceeds maximum of ".
                 $root_conf->{cgp_max_response_length}.
                 ", attempting header reduction");
        
        $newheaders = mandatory_headers("Scanned, Header Reduced");
        my $header_to_reduce = "";
        
        foreach my $header (keys %{$headerlist})
        {
          if ($header eq $root_conf->{header_to_reduce})
          {
            $header_to_reduce =
              "X-Spam-".$header.": ".
              $rewritten_mail->get_pristine_header("X-Spam-".$header).
              "\e";
          }
          else
          {
            $newheaders .= 
              "X-Spam-".$header.": ".
              $rewritten_mail->get_pristine_header("X-Spam-".$header).
              "\e";
          }
        }
        
        # See if there's enough space to add the header we want to reduce,
        # if we reduce it (to a minimum of "...\e" as the header body); 
        # first we see how much CGP will expand the header, then we 
        # calculate whether any of it will fit.
        
        my $header_expansion = cgp_string_length($header_to_reduce) - 
                               length $header_to_reduce;
                        
        my $reduced_header_length = 
          $root_conf->{cgp_max_response_length} -
          cgp_string_length($newheaders) -
          $header_expansion - 5; # the 5 is the "...\e" at the end
        
        if ($reduced_header_length > 0)
        {
          $header_to_reduce = 
            substr($header_to_reduce, 0, $reduced_header_length).
            "...\e";
        }
        
        $newheaders .= $header_to_reduce;
      }
      
      # OK... but the headers could still be too long, so let's check and
      # see if that's the case.
      
      if (cgp_string_length($newheaders) >
          $root_conf->{cgp_max_response_length})
      {
        # The headers _are_ still too long, so now we add headers in the 
        # order specified in the settings.
        
        our @headers_to_include;
        
        # First, add the required headers
        
        $newheaders = "X-Spam-Checker-Version: SpamAssassin ".
                      Mail::SpamAssassin::Version().
                      " ($Mail::SpamAssassin::SUB_VERSION)\e";        
        $newheaders .= mandatory_headers("Scanned, Headers Truncated");
        
        # Next, add headers one at a time until we can't anymore
        
        foreach my $header (@headers_to_include)
        {
          my $header_value = 
            $rewritten_mail->get_pristine_header("X-Spam-".$header);
          if ($header_value)
          {
            my $header_text = "X-Spam-".$header.": ".$header_value."\e";
            
            # if we can add this header, let's do it
            
            if ((cgp_string_length($newheaders) + 
                 cgp_string_length($header_text)) <
                $root_conf->{max_response_length})
            {
              $newheaders .= $header_text;
            }
          }
        }
      }
      
      $rewritten_mail->finish();
      $status->finish();
      $mail->finish();
    }
  };

  if ($@) 
  {
    # Something bad happened with SpamAssassin
    
    debug(4, "SpamAssassin failed in ".sprintf("%.1f", getTime() - $start).
             " seconds: $@");
    $newheaders = $root_conf->{loop_prevention_header}.": Scan Failed\e";
    ${$score_ptr} = 0.0;
  }
  
  # remove any double carriage returns...
  
  $newheaders =~ s/\n/\e/g;   # replace \n's with \e's      
  $newheaders =~ s/\e\e/\e/g; # remove any double \e's
      
  return $newheaders;
}


# Check the specified message with SpamAssassin, using the specified 
# directory as the home directory for the check and the specified
# account name in the debugging output.

sub checkMessage
{
  my ($msg_ptr, $local_address, $domain_name, $account_name, $account_dir, 
      $is_spam_ptr, $score_ptr) = @_;
  my $msg_length = 0;
  my @scanlines;
  my $sa_prefs_text = "";
  my $mail;
  our $domain_conf_cache;
  our $user_conf_cache;
  our $root_conf;
  my $domain_conf = $root_conf;
  my $user_conf = $root_conf;
  our $sa_prefs_suffix;
  our $DEFAULTSTRING;
  
  if ($root_conf->{cli_account_info})
  { 
    # initialize domain/user configurations if necessary

    $domain_conf = $domain_conf_cache{$domain_name};
    if ($account_name ne $DEFAULTSTRING)
    {
      $user_conf = $user_conf_cache{$account_name};
    }
    else
    {
      $user_conf = $domain_conf;
    }
  }
  else
  {
    # the account directory is always the default home directory
    # when we're not using the CLI
    
    $account_dir = $root_conf->{default_home_dir};
  }
  
  my $start = time;

  # Let's do SpamAssassin!

  my $sa_settings_text = '';
  my $sa3_user_dir = $root_conf->{default_home_dir};
  
  # First, read the domain preferences if they're supposed to be used
  
  if (($domain_conf != $root_conf) &&
      $domain_conf->{use_domain_prefs} && ($domain_name ne ""))
  {
    $sa3_user_dir = $domain_conf->{default_home_dir};
    
    if (open(IN, $domain_conf->{default_home_dir}.$sa_prefs_suffix))
    {
      debug(7, "Reading domain ".$domain_name.
               " SpamAssassin preferences from ".
               $domain_conf->{default_home_dir}.$sa_prefs_suffix);
      $sa_settings_text = $sa_settings_text . "\n" . 
                          join("", <IN>);
      close(IN);
    }
  }
  
  # Finally, read the user preferences if they're supposed to be used
  
  if (($account_dir ne $domain_conf->{default_home_dir}) &&
      $domain_conf->{allow_user_prefs} && $user_conf->{use_user_prefs})
  {
    $sa3_user_dir = $account_dir;
    
    if (open(IN, $account_dir.$sa_prefs_suffix))
    {
      debug(7, "Reading user SpamAssassin preferences from ".
               $account_dir.$sa_prefs_suffix);
      $sa_settings_text = $sa_settings_text . "\n" . 
                          join("", <IN>);
      close(IN);
    }
  }
  
  # And now pass the preferences to SpamAssassin
  
  $spamassassin->{conf}->{main} = $spamassassin;
  $spamassassin->{conf}->parse_scores_only($sa_settings_text);
  
  if ($spamassassin->{conf}->{allow_user_rules})
  {
    $spamassassin->{conf}->finish_parsing();
  }
  
  delete $spamassassin->{conf}->{main};
    
  # Now, if SQL user prefs are enabled, let's load them
  
  if ($domain_conf->{allow_user_prefs} && $user_conf->{use_user_prefs} &&
      $user_conf->{sql_user_prefs} && $account_name ne $DEFAULTSTRING)
  {
    debug(7, "Reading user SpamAssassin scores from SQL for user ".
             $account_name);
    $spamassassin->load_scoreonly_sql($account_name);
  }
  
  # Now we've got preferences, let's deal with state files. If auto
  # whitelisting is on for this domain, we set the persistent address
  # list factory.
  
  if ($domain_conf->{allow_auto_whitelist} &&
      $user_conf->{use_auto_whitelist})
  {
    if ($user_conf->{sql_auto_whitelist})
    {
      $spamassassin->set_persistent_address_list_factory
        (our $sqladdrlistfactory);
    }
    else
    {
      $spamassassin->set_persistent_address_list_factory
        (our $dbaddrlistfactory);
    }
  }
  else
  {
    $spamassassin->set_persistent_address_list_factory(undef);
  }
  
  # If user state is on for this domain, we point SpamAssassin to the
  # user home directory; otherwise, if domain state is turned on, we point
  # SpamAssassin to the default home directory for this domain; otherwise,
  # we point SpamAssassin to the default home directory for the root prefs.
  
  my $sa3_userstate_dir = 
    $root_conf->{default_home_dir}.$sa_prefs_dir_suffix;
  
  if ($user_conf->{sql_user_prefs})
  {
    debug(7, "SQL preferences in use, no state or user home directory");
    $sa3_user_dir = undef;
    $sa3_userstate_dir = undef;
  }
  elsif ($domain_conf->{allow_user_state} && $user_conf->{use_user_state})
  {
    $sa3_userstate_dir = $account_dir.$sa_prefs_dir_suffix;
    debug(7, "State directory is in user home directory (".
             $sa3_userstate_dir.")");
  }
  elsif ($domain_conf->{use_domain_state} && 
         $domain_conf->{default_home_dir} ne $root_conf->{default_home_dir})
  {
    $sa3_userstate_dir = 
      $domain_conf->{default_home_dir}.$sa_prefs_dir_suffix;
    debug(7, "State directory is in domain default home directory (".
             $sa3_userstate_dir.")");
  }
  else
  {
    debug(7, "State directory is in system default home directory (".
             $sa3_userstate_dir.")");
  }
    
  $spamassassin->signal_user_changed 
    ({
      username => $account_name,
      user_dir => $sa3_user_dir,
      userstate_dir => $sa3_userstate_dir
     });
  
  # if the message is longer than the max message length, scan only that
  # many characters
  
  foreach my $line (@$msg_ptr)
  {
    $msg_length = $msg_length + length($line);
    if ($msg_length < $root_conf->{max_scan_length})
    {
      push(@scanlines, $line);
    }
    else
    {
      debug(5, "Scanning first ".$root_conf->{max_msg_length}.
               " bytes of message only");
      last;
    }
  }

  my $status = undef;
  my $rewritten_mail_scalar = undef;
  
  eval 
  {
    # Strip all previous SpamAssassin markup from the message
  
    $mail = $spamassassin->parse([@scanlines], 1);
  
    @scanlines = 
      split (/^/m, $spamassassin->remove_spamassassin_markup($mail));
    
    $mail->finish();
    $mail = $spamassassin->parse([@scanlines], 1);
    
    # And now we're ready to scan!
    
    if ($status = $spamassassin->check($mail))
    {      
      my @msg_copy = @$msg_ptr;
      $mail->finish();
      $mail = $spamassassin->parse([@msg_copy], 1);
      $status->{msg} = $mail;

      # remove "Envelope-To" headers, because they cause deliveries to
      # bypass spam filtering
        
      $mail->delete_header("Envelope-To");
      $mail->delete_header("Envelope-to");
      
      # remove "Return-Path" header that we added earlier for SPF purposes
      
      $mail->delete_header("Return-Path");
      
      $rewritten_mail_scalar = $status->rewrite_mail();
      
      my $was_it_spam;
      my $spamd_log_line = "result: ";
      if ($status->is_spam()) 
      { 
        $was_it_spam = "Identified spam";
        $spamd_log_line = "Y ";
        ${$is_spam_ptr} = 1;
      } 
      else 
      { 
        $was_it_spam = "Identified non-spam";
        $spamd_log_line = ". ";
        ${$is_spam_ptr} = 0;
      }
      my $msg_score = sprintf("%.1f", $status->get_hits());
      my $msg_threshold = sprintf("%.1f", $status->get_required_hits());
      ${$score_ptr} = $status->get_hits();
      
      debug(4, $was_it_spam." (".$msg_score."/".$msg_threshold.") for ".
               $local_address." in ".sprintf("%.1f", getTime() - $start).
               " seconds");
               
      if ($root_conf->{spamd_style_log})
      {
        # print the spamd-style log to standard error
        
        $spamd_log_line .= sprintf("%d", int($status->get_hits() + 0.5));
        $spamd_log_line .= " - ".$status->get_names_of_tests_hit();
        $spamd_log_line .= " scantime=".sprintf("%.1f", getTime() - $start);
        $spamd_log_line .= ",size=".$msg_length;
        $spamd_log_line .= ",mid=";
        $spamd_log_line .= ",user=".$account_name;
        $spamd_log_line .= ",bayes=";
        $spamd_log_line .= ",autolearn=".$status->get_autolearn_status();
        select(STDERR);
        print("* ".$spamd_log_line."\n");
        select(STDOUT);
      }
    }
  };
  
  if ($@) 
  {
    # Something bad happened with SpamAssassin, so we need to add failure
    # headers to the original email
    
    my $blank = 0;
    $rewritten_mail_scalar = "";
    
    # add the new headers at the top, so as to not break DomainKeys
    
    $rewritten_mail_scalar .= mandatory_headers("Scan Failed");
    $rewritten_mail_scalar =~ s/\e/\n/g; # they're returned with \e, not \n
          
    foreach my $line (@$msg_ptr)
    {
      $rewritten_mail_scalar .= $line;
    }

    ${$is_spam_ptr} = 0;
    ${$score_ptr} = 0.0;
    
    debug(4, "SpamAssassin failed for $local_address in ".
             sprintf("%.1f", getTime() - $start)." seconds: $@");
  }
  else
  {
    # We need to add success headers to the rewritten email
    
    my $blank = 0;
    my @rewritten_lines = split (/^/m, $rewritten_mail_scalar);
    $rewritten_mail_scalar = "";
    
    # add the new headers at the top, so as to not break DomainKeys
    
    $rewritten_mail_scalar .=
      $VERSIONHEADERNAME . ": " . $VERSIONHEADERTEXT . "\n";
    $rewritten_mail_scalar .= 
      $root_conf->{loop_prevention_header} . ": Scanned\n";
          
    foreach my $line (@rewritten_lines)
    {
      $rewritten_mail_scalar .= $line;
    }
  }

  if ($status)
  {
    $status->finish();
  }

  $mail->finish();  
  return $rewritten_mail_scalar;
}


# Process A CommuniGate Pro Queue File
# This subroutine takes a filename and generates output appropriate
# for the CommuniGate Pro External Filter Interface.

sub process_file 
{
  our $seqnum;
  our $root_conf;
  our %domain_conf_cache;
  our %domain_dir_cache;
  our %user_conf_cache;
  my $filename = shift;
  my $return_path = "";
  my $source_host = "";
  my @msglines;
  my $cgp_header;
  my $all_destination;
  my %default_recipients = ();
  my %custom_recipient_accts = ();
  my %custom_recipient_dirs = ();
  my @remote_recipients;
      
  # ensure the queue file exists
  
  chomp($filename);
  if (!$root_conf->{absolute_queue_filenames}) 
  {
    $filename = our $cgp_base.$filename;
  };
  
  if (!(-e $filename))
  {
    debug(1, "Queue file $filename doesn't exist");
    printResult("OK");
    return;
  }
 
  # If we're not in parallel requests mode, flush the domain and
  # user preference caches
  
  if (!$root_conf->{parallel_requests})
  {
    %domain_conf_cache = ();
    %user_conf_cache = ();
  }
  
  # Log in to the CommuniGate Pro CLI, if necessary
  
  my $cli = undef;
  
  if ($root_conf->{use_cli})
  {
    $cli = new CGP::CLI
      ({ PeerAddr => $root_conf->{cgp_hostname},
         PeerPort => $root_conf->{cgp_port},
         login    => $root_conf->{cgp_username},
         password => $root_conf->{cgp_password}
       });
     
    if (!$cli) 
    {
      printResult("FAILURE");
      debug(1, "CLI Error: ".$CGP::ERR_STRING);
      return;
    }
  }
  
  # At this point, if $cli is undef, we know we're not using it... 
  # In any event, open the message file!
  
  open(MSGFILE, "$filename");
  
  # find all the destinations for the message, determine which are local
  # and remote, and which use individual or systemwide SpamAssassin 
  # preferences.
  
  $cgp_header = 1;
  
  # assume initially that this message is not destined for an "all@" address
  
  $all_destination = 0;
  
  foreach my $line (<MSGFILE>) 
  {
    if ($cgp_header) 
    { 
      debug(8, "Processing CGP header line: $line");
      # if we're in the headers, read an address
      if (($line =~ /^R/) && ($line =~ /<(\S*)>/) && !$all_destination)
      { 
        # a recipient
        my $raw_recipient = $1;
        
        # if we're using the CLI, go ahead and route the address to
        # figure out if it's local or not, and whether it has custom 
        # preferences or not.
        
        if ($cli && $root_conf->{cli_account_info})
        {
          my $data; 
          if ($root_conf->{route_mail_extension})
          {
            $data = $cli->Route("\"$1\" MAIL");
          }
          else
          {
            $data = $cli->Route("\"$1\"");
          }
          debug(8, "Recipient, Result from CLI: ".
                   $data->[0].",".$data->[1].",".$data->[2]);
          
          my $module = $data->[0];
          my $host = $data->[1];
          my $dma_mailbox = $data->[2];
          $data = undef;
          
          if ($cli->isSuccess) 
          {
            if (($module eq "LOCAL") || ($module eq "LIST"))
            {
              # local (or system) delivery, add to appropriate
              # local list. we preserve the envelope address as much
              # as possible.
              
              my $local_address = $raw_recipient;
              my $domain_name = "";
              my $domain_conf;
              if ($host =~ /\S*@(\S*)/)
              {
                $domain_name = $1;
              }
              
              if (!$domain_dir_cache{$domain_name})
              {
                # we don't know the domain directory for this domain, so...
                
                my $domain_dir = $cli->GetDomainLocation($domain_name);
                if (!$domain_dir)
                {
                  # CLI call failed, so we'll do this the old-fashioned way
      
                  $domain_dir = $legacy_domain_prefix.$domain_name;
                }
                
                $domain_dir_cache{$domain_name} = $domain_dir;
              }
              
              loadDomainConfig($domain_name);
              $domain_conf = $domain_conf_cache{$domain_name};
              
              if (!$default_recipients{$domain_name})
              {
                %{$default_recipients{$domain_name}} = ();
              }
              
              if ($host =~ /\*@(\S*)/)
              {
                # this is an "all@" message, so we need to process it with
                # ADDHEADER for security reasons (since PIPE is a trusted
                # source - silly CGP); this short-circuits everything else
                  
                debug(5, "all@ recipient address detected, using ".
                         "ADDHEADER mode for all recipients");
                $all_destination = 1;
              }
                  
              if ($domain_conf->{use_cgpsa} && !$all_destination &&
                  !(($module eq "LIST") && !$domain_conf->{list_scan}))
              {
                # Let's get the user's home directory and try to load 
                # their preferences
                
                my $account_dir = $domain_conf->{default_home_dir};
                my $user_conf = $domain_conf;
                
                if ($module ne "LIST")
                {
                  $account_dir = getUserInformation(\$cli, $host);
                  $user_conf = $user_conf_cache{$host};
                }
                  
                if ($user_conf->{direct_mailbox_rewrite} &&
                    ($dma_mailbox ne ""))
                {
                  # there's direct mailbox addressing, and we're supposed 
                  # to replace it by account detail. 
                  
                  # first, make sure account detail is enabled on the server
                  
                  my $local_settings;
                  if (my $local_settings = $cli->GetModule("LOCAL"))
                  {
                    my %local_settings_hash = %{$local_settings};
                    if ($local_settings_hash{Detailing} eq "Enabled")
                    {
                      # we can go ahead and rewrite the address
                      if ($local_address =~ /^(\S*)#(\S*)@(\S*)$/)
                      {
                        # it's in a secondary domain
                        $local_address = $2."+".$1."@".$3;
                      }
                      elsif ($local_address =~ /^(\S*)#(\S*)$/)
                      {
                        # it's in the main domain
                        $local_address = $2."+".$1;
                      }
 
                      debug(5, "Rewrote DMA as account detail: ".
                               $local_address);
                    }
                    else
                    {
                      debug(5, "DMA rewriting is enabled for ".$host.
                               "but account detailing is not available ".
                               "on server");
                    }
                  }
                  else
                  {
                    debug(5, "DMA rewriting is enabled for ".$host.
                             "but account detailing setting cannot be ".
                             "setting on server");
                  }
                }
                
                if (($dma_mailbox ne "") && 
                    !$user_conf->{direct_mailbox_scan})
                {
                  # there's DMA, so this is a "remote" address
                  debug(7, "Using direct mailbox addressing for ".
                           $local_address. " (no scan)");
                  push(@remote_recipients, $local_address);
                }
                else
                {                  
                  if ($domain_conf->{allow_cgpsa} && 
                      $user_conf->{use_cgpsa})
                  {
                    debug(8, "Local address ".$local_address.
                          ", account name ".$host.
                          ", effective home directory ".$account_dir);
                
                    if ($account_dir eq $root_conf->{default_home_dir} &&
                        !$domain_conf->{sql_user_prefs})
                    {
                      # use systemwide SpamAssassin prefs
                      debug(7, "Using system default SpamAssassin ".
                               "settings for ".$host); 
                      if ($user_conf->{use_dma_spam_mailbox})
                      {
                        $default_recipients{$domain_name}{$local_address} = 
                          $host;
                      }
                      else
                      {
                        $default_recipients{$domain_name}{$local_address} = 
                          "";
                      }
                    }
                    elsif ($account_dir eq $domain_conf->{default_home_dir} &&
                           !$domain_conf->{sql_user_prefs})
                    {
                      # use domain SpamAssassin prefs
                      debug(7, "Using domain default SpamAssassin ". 
                               "settings for ".$host);
                      if ($user_conf->{use_dma_spam_mailbox})
                      {
                        $default_recipients{$domain_name}{$local_address} = 
                          $host;
                      }
                      else
                      { 
                        $default_recipients{$domain_name}{$local_address} =
                          "";
                      }
                    }
                    elsif ($account_dir)
                    {
                      # use user's SpamAssassin prefs
                      debug(7, "Using user customized SpamAssassin ".
                               "settings for ".$host);
                      $custom_recipient_accts{$local_address} = $host;
                      $custom_recipient_dirs{$local_address} = $account_dir;
                    }
                    elsif ($domain_conf->{sql_user_prefs})
                    {
                      # attempt to use SQL SpamAssassin prefs
                      debug(7, "Attempting to use SQL SpamAssassin " .
                               "scores for ".$host);
                      $custom_recipient_accts{$local_address} = $host;
                      $custom_recipient_dirs{$local_address} = $SQLSTRING;
                    }
                    else
                    {
                      # no SpamAssassin prefs => treat as remote
                      debug(7, "Not running SpamAssassin for ".$host);
                      push(@remote_recipients, $local_address);
                    }
                  }
                  else
                  {
                    debug(7, "CGPSA disabled by user configuration for ".
                             $host);
                    push(@remote_recipients, $local_address);
                  }
                }
              }
              elsif ($module eq "LIST")
              {
                debug(7, "CGPSA disabled by domain configuration for ".
                         "mailing lists");
                push(@remote_recipients, $local_address);
              }
              elsif (!$all_destination)
              {
                debug(7, "CGPSA disabled by domain configuration for ".
                         $host);
                push(@remote_recipients, $local_address);
              }
            }
            elsif (our @scan_domains)
            {
              # check to see if the destination is in one of our scan
              # domains. if so, add it to the default local list.

              my $matched = 0;

              foreach my $domain (@scan_domains)
              {
                if ($host eq $domain)
                {
                  debug(7, "Using default SpamAssassin settings for ".
                           "remote address ".$raw_recipient.
                           " (in scan domain $domain)");
                  $default_recipients{""}{$raw_recipient} = "";
                  $matched = 1;
                  last;
                }
              }

              if (!$matched)
              {
                # we didn't match any of the special domains, so let's
                # treat the recipient as remote
                debug(7, "Not running SpamAssassin for remote address ".
                         $raw_recipient);
                push(@remote_recipients, $raw_recipient);
              }
            } 
            else 
            {
              # add the address to the remote list
              debug(7, "Not running SpamAssassin for remote address ".
                       $raw_recipient);
              push(@remote_recipients, $raw_recipient);
            }
          }
          else 
          {
            # Failed routing, probably a bad address, add to remote list.
            debug(8, "Unable to route address ".$raw_recipient);
            push(@remote_recipients, $raw_recipient);
          }
        }
        else 
        {
          # no CLI for account info, so...
          debug(7, "Using default SpamAssassin settings for ".
                   $raw_recipient);
          $default_recipients{""}{$raw_recipient} = "";
        }
      }
      elsif ($line =~ /^P.*<(\S*)>/ && !$all_destination) 
      {
        # the return-path
        $return_path = $1;
        debug(8, "Return-Path: $return_path");
        # add it to the message, so SPF inside SA will work
        push(@msglines, "Return-Path: <".$return_path.">\n");
      }
      elsif ($line =~ /^S.*\[(\S*)\]/)
      {
        $source_host = $1;
      }
      elsif ($line =~ /^\s+$/) 
      {
        # a blank line, end of CGP headers
        debug(8, "Finished processing CGP headers");
        $cgp_header = 0;
      }
      
      next;
    }
    
    if ($line =~ /^$root_conf->{loop_prevention_header}.*/) 
    {
      # we've already processed this message, so let's not
      # waste time processing it again - be sure to clean up
      # by closing the CLI and the queue file
      
      debug(4, "Previously-scanned message detected");
      if ($cli)
      {
        $cli->Logout();
        undef $cli;
      }
      close(MSGFILE);
      printResult("OK");
      return;
    }
    
    push(@msglines, $line);
  }

  # close the queue file
  
  close(MSGFILE);
  
  # Now determine our course of action
  # First count the number of recipients using default SpamAssassin prefs
  
  my $default_recipient_count = 0;
  foreach my $domain_name (keys %default_recipients)
  {
    my %domain_recipients = %{$default_recipients{$domain_name}};
    $default_recipient_count = 
      $default_recipient_count + scalar(keys %domain_recipients);
  }
    
  # Now scan the message (or not), and get its score, according to the
  # appropriate preferences and settings
  
  my $msg_score = 0.0;
  
  if ($root_conf->{headers_only} || $all_destination)
  {
    # we're in "headers-only" mode, so we always run SA
    
    debug(6, "Running SpamAssassin in ADDHEADER mode");
    my $newheaders = checkMessageAddHeader(\@msglines, \$msg_score);
    
    # if the message score exceeds the threshold and we're throwing out
    # messages, we discard the message... otherwise we add headers
             
    if ($root_conf->{auto_discard} && 
        $msg_score >= $root_conf->{discard_threshold})
    {
      debug(6, "Discarding spam above threshold score (".
               sprintf("%.1f", $msg_score)."/".
               $root_conf->{discard_threshold}.")");
      printResult("DISCARD");
    }
    else
    {
      printResult("ADDHEADER", $newheaders);
    }
    
    # do temporary blacklisting, if necessary
    
    if ($root_conf->{use_cli} && $root_conf->{temp_blacklist_support} && 
        $root_conf->{use_temp_blacklist} &&
        ($msg_score >= $root_conf->{temp_blacklist_threshold}))
    {
      tempBlacklist($source_host, \$cli);
    }
  }
  elsif (($default_recipient_count == 0) &&
         (scalar %custom_recipient_accts == 0))
  {
    # if there are no addresses for which SpamAssassin must be run,
    # we let the message through unaltered.
    
    debug(6, "No SpamAssassin run required");
    printResult("OK");
  }
  else
  {
    # we need to submit a bunch of new messages, so do that and
    # then DISCARD this message.
    
    # first, submit a message for all remote addresses
    
    if (scalar(@remote_recipients) > 0) 
    {
      debug(6, "Submitting message for ".scalar(@remote_recipients).
               " addresses without SpamAssassin run");
      submitMessage(\$cli, \@msglines, $return_path, "", 
                    @remote_recipients);
    }
    
    # submit a message for all local addresses using default
    # SpamAssassin prefs for the various domains
    
    my $rewritten_msg;
    
    foreach my $domain_name (keys %default_recipients)
    {
      # we've got a domain, let's get its settings and 
      # the list of recipients in it
      
      my $domain_conf = $domain_conf_cache{$domain_name};
      my %domain_recipients = %{$default_recipients{$domain_name}};
     
      if (scalar(keys %domain_recipients) > 0)
      {
        my $plural = "";
        if (scalar(keys %domain_recipients) > 1)
        {
          $plural = "es";
        }
      
        if ($domain_name eq "")
        {
          debug(6, "Running SpamAssassin with system default settings for ".
                   scalar(keys %domain_recipients)." address".$plural);
        }
        else
        {
          debug(6, "Running SpamAssassin with domain ".$domain_name.
                   " default settings for ".scalar(keys %domain_recipients).
                   " address".$plural);
        }  
        
        my $is_spam = 0;
        $rewritten_msg = 
          checkMessage(\@msglines, $DEFAULTSTRING, $domain_name, $DEFAULTSTRING, 
                       $domain_conf->{default_home_dir}, 
                       \$is_spam, \$msg_score);

        if ($domain_conf->{auto_discard} &&
            ($msg_score >= $domain_conf->{discard_threshold}))
        {
          # it's spam, and it's above the domain's discard threshold
          
          debug(6, "Discarding above-threshold spam for all domain ".
                   $domain_name." addresses (".
                   sprintf("%.1f", $msg_score)."/".
                   $domain_conf->{discard_threshold}.")");                   
        }
        else
        {
          # it may or may not be spam, but we have to deliver it
          
          my @no_dma_list;
          foreach my $recipient (keys %domain_recipients)
          {
            if (($domain_recipients{$recipient} eq "") || !$is_spam)
            {
              push(@no_dma_list, $recipient);
            }
            else            
            { 
              # it's spam, and we have the info to attempt DMA
              
              submitMessage(\$cli, [($rewritten_msg)], $return_path, 
                            $domain_recipients{$recipient}, $recipient);
            }
          }
              
          if (scalar(@no_dma_list) > 0)
          {
            # submit spam for all non-DMA users
            
            submitMessage(\$cli, [($rewritten_msg)], $return_path, "",
                          @no_dma_list);
          }
        }
      }
    }
    
    foreach my $local_address (keys %custom_recipient_accts)
    {
      my $account_name = $custom_recipient_accts{$local_address};
      my $account_dir = $custom_recipient_dirs{$local_address};
      my $user_conf = $user_conf_cache{$account_name};
      my $is_spam = 0;
      
      if ($account_dir eq $SQLSTRING)
      {
        debug(6, "Running SpamAssassin with settings from SQL ".
                 " for local address ".$local_address);
      }
      else
      {
        debug(6, "Running SpamAssassin with settings from ".
               $account_dir.$sa_prefs_suffix.
               " for local address ".$local_address);
      }
      my $domain_name = "";
      if ($account_name =~ /^\S*@(\S*)$/)      
      {
        $domain_name = $1;
      }
       
      $rewritten_msg = checkMessage(\@msglines, $local_address, 
                                    $domain_name, $account_name, 
                                    $account_dir, \$is_spam, \$msg_score);
                                    
      debug(9, "user_conf->{auto_discard} = ".
            $user_conf->{auto_discard}.
            ", user_conf->{discard_threshold} = ".
            $user_conf->{discard_threshold});

      if ($user_conf->{auto_discard} &&
          ($msg_score >= $user_conf->{discard_threshold}))
      {
        # it's spam and it's above the user's threshold, so we
        # don't submit it at all
        
        debug(6, "Discarding above-threshold spam for local address ".
                 $local_address." (".sprintf("%.1f", $msg_score)."/".
                 $user_conf->{discard_threshold}.")");
      }
      elsif ($is_spam)
      {
        # it's spam, so we should attempt DMA
        
        submitMessage(\$cli, [($rewritten_msg)], $return_path,
                      $account_name, $local_address);        
      }
      else
      {
        # it's not spam
        
        submitMessage(\$cli, [($rewritten_msg)], $return_path, "",
                      $local_address);
      }
    }
    
    # finally, do temporary blacklisting if necessary and then
    # discard the message
    
    if ($root_conf->{use_cli} && $root_conf->{temp_blacklist_support} &&
        $root_conf->{use_temp_blacklist} &&
        ($msg_score >= $root_conf->{temp_blacklist_threshold}))
    {
      tempBlacklist($source_host, \$cli);
    }

    printResult("DISCARD");
  }
  
  # close the CLI
  if ($cli)
  {
    $cli->Logout();
    undef $cli;
  }
}

# Do temporary blacklisting, given a source host
# and a pointer to an active CLI (in that order)

sub tempBlacklist
{
  my ($source_host, $cli_ptr) = @_;
  my $cli = ${$cli_ptr};
  
  if ($source_host eq "")
  {
    debug(9, "No source host, skipping temporary blacklisting");
  }
  else
  {
    # check to see if the host is whitelisted; if it isn't, we'll
    # blacklist it
    
    my $is_whitelisted = 0;
    my $whitelist = $cli->GetWhiteHoleIPs();
    if ($cli->isSuccess)
    {
      debug(9, "Retrieved whitelist");
      my @whitelist_array = split(/\\e/, $whitelist);
      foreach my $whitelist_entry (@whitelist_array)
      {
        if (($whitelist_entry =~ /^(\S+)\s*/) &&
            ($source_host == $1))
        {
          $is_whitelisted = 1;
        }
      }
    
      # if the host isn't whitelisted, we can blacklist it
    
      if (!$is_whitelisted)
      {
        my $blacklist = $cli->GetTempBlacklistedIPs();
        my $new_blacklist = "";
        if ($cli->isSuccess)
        {
          debug(9, "Retrieved blacklist");
          
          # filter the current host out of the blacklist
          
          my @blacklist_array = split(",", $blacklist);
          foreach my $blacklist_entry (@blacklist_array)
          {
            if (!($blacklist_entry =~ /^\[$source_host\].*/))
            {
              if ($new_blacklist)
              {
                $new_blacklist .= ",";
              }
              
              $new_blacklist .= $blacklist_entry;
            }
          }
          
          debug(7, "Temporarily blacklisting ".$source_host.
                   " for ".$root_conf->{temp_blacklist_duration}.
                   " seconds");
                   
          if ($new_blacklist)
          {
            $new_blacklist = $new_blacklist.",";
          }
          
          $new_blacklist .= "[".$source_host."]-".
                            $root_conf->{temp_blacklist_duration};
          $cli->SetTempBlacklistedIPs($new_blacklist);
          if (!$cli->isSuccess)
          {
            debug(7, "Error updating blacklist: ".
                     $cli->getErrMessage);
          }
        }
        else
        {
          debug(7, "Error getting blacklist: ".
                    $cli->getErrMessage);
        }
      }
      else
      {
        debug(7, "Not blacklisting ".$source_host.
                 " (whitelisted).");
      }
    }
    else
    {
      debug(7, "Error getting whitelist: ".
               $cli->getErrMessage);
    }
  }
}


# Read the root preferences. 

sub loadRootConfig
{
  our $root_conf;
  our $root_config_pathname;
  our @scan_domains;
  our @headers_to_include;
  
  # set default values
  
  $root_conf->{cgp_username} = "username";
  $root_conf->{cgp_password} = "password";
  $root_conf->{cgp_hostname} = "localhost";
  $root_conf->{allow_cgpsa} = 1;
  $root_conf->{use_cgpsa} = 1;
  
  # parallel_requests defaults to on if we're not on Windows
  if ($^O =~ /^(?:mswin|dos|os2)/oi)
  {
    $root_conf->{parallel_requests} = 0;
  }
  else
  {
    $root_conf->{parallel_requests} = 1;
  }
  
  $root_conf->{max_requests} = 0;
  $root_conf->{max_scan_length} = 131072;
  $root_conf->{headers_only} = 0;
  $root_conf->{use_cli} = 1;
  $root_conf->{cli_account_info} = 1;
  $root_conf->{cgp_port} = 106;
  $root_conf->{loop_prevention_header} = "X-TFF-CGPSA-Filter";
  $root_conf->{scan_domains} = 0;
  undef @scan_domains;
  $root_conf->{list_scan} = 0;
  $root_conf->{direct_mailbox_scan} = 0;
  $root_conf->{direct_mailbox_rewrite} = 0;
  $root_conf->{debug} = 1;
  $root_conf->{debug_level} = 8;
  $root_conf->{sa_debug} = 0;
  $root_conf->{spamd_style_log} = 0;
  $root_conf->{default_home_dir} = 
    $cgp_base.$root_config_directory."SpamAssassin";
  $root_conf->{helper_path} = "";
  $root_conf->{sa_prefix} = "";
  $root_conf->{sa_default_rules_dir} = "";
  $root_conf->{sa_local_rules_dir} = "";
  $root_conf->{allow_user_cgpsa_conf} = 1;
  $root_conf->{allow_user_prefs} = 1;
  $root_conf->{use_user_prefs} = 0;
  $root_conf->{sql_user_prefs} = 0;
  $root_conf->{require_user_prefs} = 0;
  $root_conf->{allow_domain_cgpsa_conf} = 1;
  $root_conf->{use_domain_prefs} = 1;
  $root_conf->{allow_user_state} = 1;
  $root_conf->{use_user_state} = 0;
  $root_conf->{use_domain_state} = 1;
  $root_conf->{use_dma_spam_mailbox} = 0;
  $root_conf->{spam_mailbox_name} = "SPAM";
  $root_conf->{auto_discard} = 0;
  $root_conf->{discard_threshold} = 25.0; 
  $root_conf->{allow_auto_whitelist} = 1;
  $root_conf->{use_auto_whitelist} = 1;
  $root_conf->{sql_auto_whitelist} = 0;
  $root_conf->{do_network_tests} = 1;
  $root_conf->{stop_at_threshold} = 0;
  $root_conf->{use_temp_blacklist} = 0;
  $root_conf->{temp_blacklist_threshold} = 8.0;
  $root_conf->{temp_blacklist_duration} = 900;
  $root_conf->{cgp_max_response_length} = 4078;
  $root_conf->{header_to_reduce} = "Status";
  @headers_to_include = split /\s+/, "Flag Level Status";
  $root_conf->{headers_to_include} = 1;
  $root_conf->{use_c_locale} = 1;
  $root_conf->{redirect_stderr} = 1;
  $root_conf->{absolute_queue_filenames} = 0;
 #$root_conf->{route_mail_extension} set at runtime initialization

  # load root configuration file
  
  readConfigFile($root_conf, $root_config_pathname);
  
  # Set the home dir for helpers (this is important, so it defaults
  # to the system prefs dir).
  
  if (!$root_conf->{helper_state_dir})
  {
    $root_conf->{helper_state_dir} = $root_conf->{default_home_dir};
  }
  
  # If we're in "headers-only" mode, change other settings
  # appropriately.
  
  if ($root_conf->{headers_only})
  {
    $root_conf->{use_cli} = 0;
    $root_conf->{use_user_prefs} = 0;
    $root_conf->{require_user_prefs} = 0;
    $root_conf->{use_user_state} = 0;
  }
}


# Load the domain configuration for the specified domain into the
# domain_conf_cache. This will be identical to the root configuration if the 
# specified domain is "", if it doesn't exist, or if there is no domain
# configuration for the specified domain. If the domain configuration for 
# the specified domain has already been loaded, it is not loaded again. All 
# settings not explicitly specified in the domain configuration file are 
# taken from the root configuration file.

sub loadDomainConfig
{
  my $domain_name = shift;
  my $print_domain_name = $domain_name;
  my %conf = ();
  my $domain_dir = "";
  our %root_conf_hash;
  our %domain_conf_cache;
  our %domain_dir_cache;
  our $root_conf;
  
  if (!(exists $domain_conf_cache{$domain_name}))
  {
    if ($root_conf->{allow_domain_cgpsa_conf})
    {
      if ($print_domain_name eq "")
      {
        $print_domain_name = "main domain";
      }
      
      debug(7, "Attempting to load domain configuration for ".
               $print_domain_name);

      # initialize it to the root configuration
  
      foreach my $setting (keys %root_conf_hash)
      {
        $conf{$setting} = $root_conf_hash{$setting};
      }
    
      if ($domain_name ne "")
      {
        
        readConfigFile
          (\%conf, $domain_dir_cache{$domain_name}.$domain_config_suffix);
      }
      else
      { 
        readConfigFile
          (\%conf, 
           $cgp_base.$root_config_directory.$domain_config_filename);
      }
      
      if (!$conf{no_configuration})
      {
        # we successfully loaded something, let's cache it
        
        debug(7, "Loaded domain configuration for ".$print_domain_name);
        $domain_conf_cache{$domain_name} = \%conf;
      }
      else
      {
        # we didn't load anything, so for this domain we use the root
        # preferences
        
        debug(7, "Using root configuration for ".$print_domain_name);        
        $domain_conf_cache{$domain_name} = $root_conf;
      }
    }
    else
    {
      # we didn't load anything because domain configurations are disallowed
      
      $domain_conf_cache{$domain_name} = $root_conf;
    }
  }
  else
  {
    debug(9, "domain configuration already loaded, discard_threshold = ".
             $domain_conf_cache{$domain_name}->{discard_threshold});
  }
}

# Load the user configuration for the specified user into the 
# user_conf_cache, from the specified user settings directory. This will be 
# identical to the domain configuration if there is no user configuration
# for the specified user. All settings not explicitly specified in the user 
# configuration file are taken from the domain configuration file. This
# function assumes that the provided settings directory is valid, and
# that it ends with a "/".

sub loadUserConfig
{
  my ($user_name, $settings_directory) = @_;
  my %conf = ();
  my $domain_name;
  my $domain_conf;
  our %domain_conf_cache;
  our %user_conf_cache;
  our $root_conf;
  
  if ($user_name =~ /^\S*@(\S*)$/)
  {
    $domain_name = $1;
  }
  else
  {
    $domain_name = "";
  }
  
  loadDomainConfig($domain_name);
  $domain_conf = $domain_conf_cache{$domain_name};
  
  if (!(exists $user_conf_cache{$user_name}))
  {
    if ($domain_conf->{allow_user_cgpsa_conf})
    {
      debug(7, "Attempting to load user configuration for ".$user_name);

      # initialize to values of domain configuration
    
      foreach my $setting (keys %{$domain_conf})
      {
        $conf{$setting} = $domain_conf->{$setting};
      }

      readConfigFile(\%conf, $settings_directory.our $user_config_filename);

      if (!$conf{no_configuration})
      {
        # we successfully loaded something, let's cache it
      
        debug(7, "Loaded user configuration for ".$user_name);
        $user_conf_cache{$user_name} = \%conf;
      }
      else
      {
        # we didn't load anything, so for this domain we use the root
        # preferences
      
        debug(7, "Using domain configuration for ".$user_name);
        $user_conf_cache{$user_name} = $domain_conf;
      }
    }
    else
    {
      # we didn't load anything because user configurations are disallowed
      
      $user_conf_cache{$user_name} = $domain_conf;
    }
  }
  else
  {
    debug(9, "user configuration already loaded, discard_threshold = ".
             $user_conf_cache{$user_name}->{discard_threshold});
  }
}


# Read preferences from a preferences file into a preferences 
# hash. This does no initialization on the preferences hash. 

sub readConfigFile 
{
  my ($conf, $filename) = @_;
  our @scan_domains;
  our @headers_to_include;
  
  if (!$filename || !(-f $filename) || !open(IN,"<$filename")) 
  {
    # the file doesn't exist
    $conf->{no_configuration} = 1;
    return;
  }

  # read the file, line by line
   
  foreach my $line (<IN>) 
  {
    $line =~ s/\r//g;
    $line =~ s/(^|(?<!\\))\#.*$/$1/;
    $line =~ s/^\s+//; 
    $line =~ s/\s+$//; 
    
    next if ($line =~ /^$/);

    if ($line =~ /^cgp[-_]username[\s\=]+(\S+)$/) 
    {
      $conf->{cgp_username} = $1;
    }
    elsif ($line =~ /^cgp[-_]password[\s\=]+(.+)$/) 
    {
      $conf->{cgp_password} = $1;
    }
    elsif ($line =~ /^cgp[-_]hostname[\s\=]+(\S+)$/)
    {
      $conf->{cgp_hostname} = $1;
    }
    elsif ($line =~ /^allow[-_]cgpsa[\s\=]+(\S+)$/)
    {
      $conf->{allow_cgpsa} = evaluateBoolean($1);
    }
    elsif ($line =~ /^use[-_]cgpsa[\s\=]+(\S+)$/)
    {
      $conf->{use_cgpsa} = evaluateBoolean($1);
    }
    elsif ($line =~ /^parallel[-_]requests[\s\=]+(\S+)$/)
    {
      $conf->{parallel_requests} = evaluateBoolean($1);
    }
    elsif ($line =~ /^max[-_]requests[\s\=]+(\d+)$/) 
    {
      $conf->{max_requests} = scalar($1);
      if ($conf->{max_requests} < 0)
      {
        $conf->{max_requests} = 0;
      }
    }
    elsif ($line =~ /^max[-_]scan[-_]length[\s\=]+(\S+)$/)
    {
      $conf->{max_scan_length} = $1;
    }
    elsif ($line =~ /^headers[-_]only[\s\=]+(\S+)$/)
    {
      $conf->{headers_only} = evaluateBoolean($1);
    }
    elsif ($line =~ /^use[-_]cli[\s\=]+(\S+)$/)
    {
      $conf->{use_cli} = evaluateBoolean($1);
    }
    elsif ($line =~ /^cli[-_]account[-_]info[\s\=]+(\S+)$/)
    {
      $conf->{cli_account_info} = evaluateBoolean($1);
    }
    elsif ($line =~ /^cgp[-_]port[\s\=]+(\S+)$/) 
    {
      $conf->{cgp_port} = $1;
    }
    elsif ($line =~ /^loop[-_]prevention[-_]header[\s\=]+(\S+)$/) 
    {
      $conf->{loop_prevention_header} = $1;
    }
    elsif ($line =~ /^scan[-_]domains[\s\=]+(.+)$/)
    {
      # got to parse a space-separated list of domain names
      @scan_domains = split /\s+/, $1;
      $conf->{scan_domains} = 1;
    }
    elsif ($line =~ /^list[-_]scan[\s\=]+(\S+)$/)
    {
      $conf->{list_scan} = evaluateBoolean($1);
    }
    elsif ($line =~ /^direct[-_]mailbox[-_]scan[\s\=]+(\S+)$/) 
    {
      $conf->{direct_mailbox_scan} = evaluateBoolean($1);
    }
    elsif ($line =~ /^direct[-_]mailbox[-_]rewrite[\s\=]+(\S+)$/)
    {
      $conf->{direct_mailbox_rewrite} = evaluateBoolean($1);
    }
    elsif ($line =~ /^debug[\s\=]+(\S+)$/) 
    {
      $conf->{debug} = evaluateBoolean($1);
    }
    elsif ($line =~ /^debug[-_]level[\s\=]+(\d+)$/) 
    {
      $conf->{debug_level} = $1;
    }
    elsif ($line =~ /^sa[-_]debug[\s\=]+(.+)$/)
    {
      if (isBoolean($1))
      {
        $conf->{sa_debug} = evaluateBoolean($1);
      }
      else
      {
        $conf->{sa_debug} = $1;
      }
    }
    elsif ($line =~ /^spamd[-_]style[-_]log[\s\=]+(\S+)$/)
    {
      $conf->{spamd_style_log} = evaluateBoolean($1);
    }
    elsif ($line =~ /^default[-_]home[-_]dir[\s\=]+(.+)$/) 
    {
      $conf->{default_home_dir} = $1;
    }
    elsif ($line =~ /^helper[-_]path[\s\=]+(.+)$/)
    {
      $conf->{helper_path} = $1;
    }
    elsif ($line =~ /^helper[-_]state[-_]dir[\s\=]+(.+)$/) 
    {
      $conf->{helper_state_dir} = $1;
    }
    elsif ($line =~ /^allow[-_]user[-_]cgpsa[-_]conf[\s\=]+(\S+)$/)
    {
      $conf->{allow_user_cgpsa_conf} = evaluateBoolean($1);
    }
    elsif ($line =~ /^allow[-_]user[-_]prefs[\s\=]+(\S+)$/) 
    {
      $conf->{allow_user_prefs} = evaluateBoolean($1);
    }
    elsif ($line =~ /^use[-_]user[-_]prefs[\s\=]+(\S+)$/) 
    {
      $conf->{use_user_prefs} = evaluateBoolean($1);
    }
    elsif ($line =~ /^require[-_]user[-_]prefs[\s\=]+(\S+)$/) 
    {
      $conf->{require_user_prefs} = evaluateBoolean($1);
    }
    elsif ($line =~ /^sql[-_]user[-_]prefs[\s\=]+(\S+)$/) 
    {
      $conf->{sql_user_prefs} = evaluateBoolean($1);
    }
    elsif ($line =~ /^allow[-_]domain[-_]cgpsa[-_]conf[\s\=]+(\S+)$/)
    {
      $conf->{allow_domain_cgpsa_conf} = evaluateBoolean($1);
    }
    elsif ($line =~ /^use[-_]domain[-_]prefs[\s\=]+(\S+)$/) 
    {
      $conf->{use_domain_prefs} = evaluateBoolean($1);
    }
    elsif ($line =~ /^allow[-_]user[-_]state[\s\=]+(\S+)$/) 
    {
      $conf->{allow_user_state} = evaluateBoolean($1);
    }
    elsif ($line =~ /^use[-_]user[-_]state[\s\=]+(\S+)$/) 
    {
      $conf->{use_user_state} = evaluateBoolean($1);
    }
    elsif ($line =~ /^use[-_]domain[-_]state[\s\=]+(\S+)$/) 
    {
      $conf->{use_domain_state} = evaluateBoolean($1);
    }
    elsif ($line =~ /^allow[-_]auto[-_]whitelist[\s\=]+(\S+)$/) 
    {
      $conf->{allow_auto_whitelist} = evaluateBoolean($1);
    }
    elsif ($line =~ /^use[-_]auto[-_]whitelist[\s\=]+(\S+)$/) 
    {
      $conf->{use_auto_whitelist} = evaluateBoolean($1);
    }
    elsif ($line =~ /^sql[-_]auto[-_]whitelist[\s\=]+(\S+)$/)
    {
      $conf->{sql_auto_whitelist} = evaluateBoolean($1);
    }
    elsif ($line =~ /^use[-_]dma[-_]spam[-_]mailbox[\s\=]+(\S+)$/)
    {
      $conf->{use_dma_spam_mailbox} = evaluateBoolean($1);
    }
    elsif ($line =~ /^spam[-_]mailbox[-_]name[\s\=]+(.+)$/)
    {
      $conf->{spam_mailbox_name} = $1;
    }
    elsif ($line =~ /^auto[-_]discard[\s\=]+(\S+)$/)
    {
      $conf->{auto_discard} = evaluateBoolean($1);
    }
    elsif ($line =~ /^discard[-_]threshold[\s\=]+(\S+)$/)
    {
      $conf->{discard_threshold} = $1;
    }
    elsif ($line =~ /^do[-_]network[-_]tests[\s\=]+(\S+)$/) 
    {
      $conf->{do_network_tests} = evaluateBoolean($1);
    } 
    elsif ($line =~ /^stop[-_]at[-_]threshold[\s\=]+(\S+)$/) 
    {
      $conf->{stop_at_threshold} = evaluateBoolean($1);
    }
    elsif ($line =~ /^use[-_]temp[-_]blacklist[\s\=]+(\S+)$/)
    {
      $conf->{use_temp_blacklist} = evaluateBoolean($1);
    }
    elsif ($line =~ /^temp[-_]blacklist[-_]threshold[\s\=]+(\S+)$/)
    {
      $conf->{temp_blacklist_threshold} = $1;
    }
    elsif ($line =~ /^temp[-_]blacklist[-_]duration[\s\=]+(\S+)$/)
    {
      $conf->{temp_blacklist_duration} = $1;
    }
    elsif ($line =~ /^cgp[-_]max[-_]response[-_]length[\s\=]+(\S+)$/)
    {
      $conf->{cgp_max_response_length} = $1;
    }
    elsif ($line =~ /^header[-_]to[-_]reduce[\s\=]+(\S+)$/)
    {
      $conf->{header_to_reduce} = $1;
    }
    elsif ($line =~ /^headers[-_]to[-_]include[\s\=]+(.+)$/)
    {
      # got to parse a space-separated list of header names
      @headers_to_include = split /\s+/, $1;
      $conf->{headers_to_include} = 1;
    }
    elsif ($line =~ /^use[-_]c[-_]locale[\s\=]+(\S+)$/)
    {
      $conf->{use_c_locale} = evaluateBoolean($1);
    }
    elsif ($line =~ /^redirect[-_]stderr[\s\=]+(\S+)$/)
    {
      $conf->{redirect_stderr} = evaluateBoolean($1);
    }
    elsif ($line =~ /^sa[-_]prefix[\s\=]+(.+)$/)
    {
      $conf->{sa_prefix} = $1;
    }
    elsif ($line =~ /^sa[-_]default[-_]rules[-_]dir[\s\=]+(.+)$/)
    {
      $conf->{sa_default_rules_dir} = $1;
    }
    elsif ($line =~ /^sa[-_]local[-_]rules[-_]dir[\s\=]+(.+)$/)
    {
      $conf->{sa_local_rules_dir} = $1;
    }
   elsif ($line =~ /^absolute[-_]queue[-_]filenames[\s\=]+(\S+)$/) 
    {
      $conf->{absolute_queue_filenames} = evaluateBoolean($1);
    }
  }

  # close the prefs file
  
  close(IN);
}

# Return 1 if the passed parameter is one of the various acceptable
# ways of writing a Boolean (true/false, yes/no, on/off, 1/0), and 0
# otherwise.

sub isBoolean
{
  my $boolean = shift;
  
  if (evaluateBoolean($boolean) || ($boolean =~ /false/i) || 
      ($boolean =~ /no/i) || ($boolean =~ /off/i) || ($boolean eq "0"))
  {
    return 1;
  }
  else
  {
    return 0;
  }
}


# Return 1 if the passed parameter is one of the various acceptable
# ways of writing "true" (yes, on, 1, etc.), and 0 otherwise.

sub evaluateBoolean 
{
  my $boolean = shift;
  
  if (($boolean =~ /true/i) || ($boolean =~ /yes/i) ||
      ($boolean =~ /on/i) || ($boolean eq "1")) 
  {
    return 1;
  }
  else
  {
    return 0;
  }
}


# Lock standard output (using a lock file).

sub lockStandardOutput
{
  if (our $root_conf->{parallel_requests})
  {
    sysopen(FH, $lock_pathname, O_WRONLY | O_CREAT);
    flock(FH, LOCK_EX);
  }
}

# Unlock standard output (using a lock file). Must be called after 
# lockStandardOutput.

sub unlockStandardOutput
{
  if (our $root_conf->{parallel_requests})
  {
    close(FH);
  }
}


# Generate a unique filename (just a filename, not a full path to anywhere).

sub getUniqueFilename 
{
  my $uniquestring = shift;
  our $seqnum;
  my $basetime=time;
  my $rand=int(rand(10000));
  my $uniquename=$seqnum.".".$basetime.$rand.$uniquestring;
  return $uniquename;
}


# Output a result to standard output. Takes two parameters, the 
# result keyword (OK, FAILURE, etc) and the parameter if any (such
# as the headers to add with ADDHEADER). The result keyword must be
# in CommuniGate Pro String Format, the parameter need not be. Because
# the parameter will be quoted, it is inappropriate to send the interface
# version as a parameter.
#
# This routine handles the sequence numbering automatically.

sub printResult
{
  my $result = shift;
  my $param = shift;
  our $seqnum;
  
  lockStandardOutput();
  print("$seqnum $result");
  if ($param)
  {
    print(" \"".to_cgp_string($param)."\"");
  }
  print("\n");
  unlockStandardOutput();
}


# Output a string to standard output, with leading asterisks and 
# sequence number, for the CommuniGate log. It need not be in
# CommuniGate Pro String Format.

sub printExtraInfo
{
  my $info_string = to_cgp_string(shift);
  our $seqnum;
  
  # Wrap the string, if necessary.

  my $output_line = wrap("* $seqnum ", "* $seqnum   ", $info_string);
  lockStandardOutput();
  print($output_line."\n");
  unlockStandardOutput();
}


# Return the current time. If we have Time::HiRes, we use it.

sub getTime
{
  my $result = time;
  if (our $root_conf->{have_time_hires})
  {
    $result = Time::HiRes::time();
  }
  return $result;
}


# Output a debugging string, if appropriate, to standard output. 
# It will appear in the CommuniGate Pro log, if CommuniGate Pro's
# debug level is set appropriately. It need not be in CommuniGate Pro
# string format.

sub debug 
{
  my $debug_level = scalar(shift);
  my $debug_string = shift;
  our $root_conf;
  
  if ($root_conf->{debug} && $debug_level <= $root_conf->{debug_level}) 
  {
    printExtraInfo($debug_string);
  }
}


# Output an error string to standard error and, if standard error is
# redirected to a file, to standared output as well; then exit. This
# is used instead of Perl's "die" to report fatal errors in a way such
# that they are guaranteed to appear in the CommuniGate Pro log (if
# CommuniGate Pro's debug level is set appropriately). The string need not 
# be in CommuniGate Pro string format.

sub error_exit
{
  my $error_string = shift;
  my $cgp_error_string = to_cgp_string($error_string);
  
  select(STDERR);
  print("* ".scalar localtime()." CGPSA Error: ".$cgp_error_string."\n");
  select(STDOUT);
  
  if (our $root_conf->{redirect_stderr})
  {
    # stderr is redirected, so output to stdout as well
    lockStandardOutput();
    print("* CGPSA Error: ".$cgp_error_string."\n");
    unlockStandardOutput();
  }
  exit();
}


# Convert an arbitrary string to CommuniGate Pro string format, and
# return it as a result.

sub to_cgp_string
{
  my $orig_string = shift;
  $orig_string =~ s/\\/\\\\/g;
  $orig_string =~ s/\e/\\e/g;
  $orig_string =~ s/\r/\\r/g;
  $orig_string =~ s/\n/\\n/g;
  $orig_string =~ s/\t/\\t/g;
  $orig_string =~ s/\"/\\"/g;
  return $orig_string;
}
    

# Get the expanded length of a string in CommuniGate Pro string format

sub cgp_string_length
{
  my $cgp_string = shift;
  $cgp_string =~ s/\e/\r\n/g;
  $cgp_string =~ s/\t/   /g;
  return (length $cgp_string);
}


# Generate the mandatory headers (CGPSA information, loop prevention)
# with the specified string as the text of the loop prevention header,
# with line endings "\e".

sub mandatory_headers
{
  my $header_text = shift;
  my $newheaders = $VERSIONHEADERNAME.": ".$VERSIONHEADERTEXT."\e";
  $newheaders .= $root_conf->{loop_prevention_header}.
                 ": ".$header_text."\e";
  return $newheaders
}

# End of CGPSA
