summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormpaperno <max@wdg.us>2012-08-30 06:01:15 +0200
committermpaperno <max@wdg.us>2012-08-30 06:01:15 +0200
commitc3c8a5107d39e3b7980d5aa25ad77cacd8b77085 (patch)
treedbbceb3e3401124fe22a94aa692a364dfd3048b1
parent0e4cb39c6d37f85f7ea82853444de287c5a04bf4 (diff)
downloadspampd-c3c8a5107d39e3b7980d5aa25ad77cacd8b77085.tar.gz
spampd-c3c8a5107d39e3b7980d5aa25ad77cacd8b77085.tar.xz
Old versions archived.
-rw-r--r--previous-versions/spampd-0.0.1.pl523
-rw-r--r--previous-versions/spampd-0.0.5.pl510
-rw-r--r--previous-versions/spampd-1.0.2.pl595
-rw-r--r--previous-versions/spampd-2.00.pl1071
-rw-r--r--previous-versions/spampd-2.10.pl1392
-rw-r--r--previous-versions/spampd-2.11.pl1401
-rw-r--r--previous-versions/spampd-2.12.pl1413
-rw-r--r--previous-versions/spampd-2.13.pl1417
-rw-r--r--previous-versions/spampd-2.2.pl1418
-rw-r--r--previous-versions/spampd-2.30.pl1506
-rw-r--r--previous-versions/spampd-2.32.pl1559
-rw-r--r--previous-versions/spampd-2.40.forceuser.pl1592
-rw-r--r--previous-versions/spampd-2.40.pl1591
-rw-r--r--previous-versions/spampd.1.0.1.pl584
14 files changed, 16572 insertions, 0 deletions
diff --git a/previous-versions/spampd-0.0.1.pl b/previous-versions/spampd-0.0.1.pl
new file mode 100644
index 0000000..4d0e598
--- /dev/null
+++ b/previous-versions/spampd-0.0.1.pl
@@ -0,0 +1,523 @@
+#!/usr/bin/perl -w
+
+############################################################
+# This code is a merging of spamd (copyright 2001 by Craig Hughes)
+# and spamproxyd by Ian R. Justman.
+# Like spamproxyd, it is written with Postfix "advanced" content
+# filtering in mind. See FILTER_README in the Postfix distribution
+# for more information on how to set this up.
+#
+# The primary difference between spamproxyd and spampd is that
+# spampd acutally tags the spams and sends them on, even making
+# use of the auto-whitelist feature (and soon the SQL lookups
+# based on the recipient email address, maybe).
+#
+# The primary difference between spamd and spampd is that spampd
+# talks SMTP protocol for its I/O stream (via Net::SMTP::Server
+# and Mail::SpamAssassin::SMTP::SmartHost).
+#
+# WARNING: Use at your own risk. Basically I have no idea what
+# I'm doing with the process forking stuff, I just copied it and
+# messed around until it worked (for me, YMMV). Also the demonizing
+# stuff seems screwy when you go to kill the daemon (at least via inet.d
+# script), but it does exit and clean up so no harm seems to be done.
+#
+# Development/production environment is RH Linux 7.x on various x86 hardware.
+#
+# BUG: for some reason the log and warn (if -D) messages don't print during
+# SIGTERM or when a child dies (after processing it's allotted # of msgs).
+# I have no idea why (see above)
+#
+# spampd is licensed for use under the terms of the Perl Artistic License
+#
+############################################################
+
+# use lib '../lib'; # added by jm for use inside the distro
+use strict;
+# use Socket;
+use Carp;
+use Net::SMTP::Server;
+use Net::SMTP::Server::Client;
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::NoMailAudit;
+use Mail::SpamAssassin::SMTP::SmartHost;
+use Net::DNS;
+use Sys::Syslog qw(:DEFAULT setlogsock);
+use POSIX qw(setsid);
+use Getopt::Std;
+use POSIX ":sys_wait_h";
+
+my %resphash = (
+ EX_OK => 0, # no problems
+ EX_USAGE => 64, # command line usage error
+ EX_DATAERR => 65, # data format error
+ EX_NOINPUT => 66, # cannot open input
+ EX_NOUSER => 67, # addressee unknown
+ EX_NOHOST => 68, # host name unknown
+ EX_UNAVAILABLE => 69, # service unavailable
+ EX_SOFTWARE => 70, # internal software error
+ EX_OSERR => 71, # system error (e.g., can't fork)
+ EX_OSFILE => 72, # critical OS file missing
+ EX_CANTCREAT => 73, # can't create (user) output file
+ EX_IOERR => 74, # input/output error
+ EX_TEMPFAIL => 75, # temp failure; user is invited to retry
+ EX_PROTOCOL => 76, # remote error in protocol
+ EX_NOPERM => 77, # permission denied
+ EX_CONFIG => 78, # configuration error
+ );
+
+sub usage
+{
+ warn <<EOUSAGE;
+
+Usage: spampd [options]
+
+Options:
+
+ -w Use auto-whitelists.
+ -a Tag all messages (not just spam)
+ -h Print this usage message and exit
+ -q Enable SQL config (currently inactive)
+ -s facility Specify the syslog facility (default: mail)
+ -u username Run as named user, instead of running as current user
+ -g hostname Use specified hostname in the SMTP HELO command when
+ forwarding mail (Default: spamfilter.localdomain)
+
+Source:
+ -i ipaddr Listen on the specified IP address (default: 127.0.0.1,
+ use 0.0.0.0 to listen on all available addresses)
+ -p port Listen on the specific port (default: 10025)
+
+Destination:
+ -t ipaddr Use specified IP address as relay (To) host (default: 127.0.0.1)
+ -v port Use specified port on the relay host specified with -t (default: 10026)
+
+Process Control:
+ -d Daemonize, detach from parent process
+ -C Number of child processes to create (Default: 4)
+ -m Minumum number of connections to handle per child before exiting (Default: 5)
+ -M Maximum number of connections to handle per child before exiting (Default: 10)
+
+SpamAssassin passthrough flags:
+ -D Print debugging messages (some from spampd also)
+ -L Use local tests only (no DNS or other network lookups)
+ -P Die upon user errors (does not exist or user = root) instead of
+ running as 'nobody' with defaults.
+ -F 0|1 remove/add 'From ' line at start of output (default: 1)
+
+
+EOUSAGE
+ exit $resphash{EX_USAGE};
+}
+
+use vars qw{
+ $opt_d $opt_h $opt_L $opt_p $opt_A $opt_x $opt_s $opt_D $opt_u
+ $opt_P $opt_c $opt_a $opt_i $opt_q $opt_F $opt_t $opt_v $opt_C
+ $opt_m $opt_M $opt_w $opt_g
+};
+
+getopts('wacdhg:i:p:qs:t:u:v:xA:DLPF:C:m:M:') or usage();
+
+$opt_h and usage();
+
+my $log_facility = 'mail';
+if($opt_s) { $log_facility = $opt_s; }
+
+my $dontcopy = 1;
+if ($opt_c) { $dontcopy = 0; }
+
+my $relayServer = "127.0.0.1";
+if ($opt_t) { $relayServer = $opt_t; }
+my $relayPort = "10026";
+if ($opt_v) { $relayPort = $opt_v; }
+
+my $smarthost = $relayServer . ":" . $relayPort;
+
+my $children = 4;
+if ($opt_C) { $children = $opt_C; }
+my $minperchild = 5;
+if ($opt_m) { $minperchild = $opt_m; }
+my $maxperchild = 10;
+if ($opt_M) { $maxperchild = $opt_M; }
+
+my $port = $opt_p || 10025;
+my $addr = $opt_i || '127.0.0.1';
+
+my $myhelo = $opt_g || 'spamfilter.localdomain';
+
+($port) = $port =~ /^(\d+)$/ or die "invalid port";
+
+
+#if (defined $ENV{'HOME'}) {
+# delete $ENV{'HOME'}; # we do not want to use this when running spamd
+#}
+
+my $spamtest = Mail::SpamAssassin->new({
+ dont_copy_prefs => $dontcopy,
+ local_tests_only => $opt_L,
+ debug => $opt_D,
+ paranoid => ($opt_P || 0),
+});
+
+$opt_w and eval
+{
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $spamtest->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+sub logmsg; # forward declaration
+
+setlogsock('unix');
+
+# Use Net::SMTP::Server here to talk regular SMTP
+my $server = new Net::SMTP::Server($addr, $port) ||
+ die "Unable to create server: $! : $addr, $port\n";
+
+# support non-root use (after we bind to the port)
+my $setuid_to_user = 0;
+if ($opt_u) {
+ my $uuid = getpwnam($opt_u);
+ if (!defined $uuid || $uuid == 0) {
+ die "fatal: cannot run as nonexistent user or root with -u option\n";
+ }
+ $> = $uuid; # effective uid
+ $< = $uuid; # real uid. we now cannot setuid anymore
+ if ($> != $uuid) {
+ die "fatal: setuid to uid $uuid failed\n";
+ }
+}
+
+$spamtest->compile_now(); # ensure all modules etc. are loaded
+$/ = "\n"; # argh, Razor resets this! Bad Razor!
+
+$opt_d and daemonize();
+
+my $current_user;
+
+if ($opt_D) {
+ warn "server started on port $port\n";
+ warn "server pid: $$\n";
+}
+logmsg "server started on $addr:$port; server pid: $$\n";
+
+# Ian R. Justman writes in spamproxyd:
+# This is the preforking and option-parsiong section taken from the MSDW
+# smtpproxy code by Bennett Todd. Any comments from that code are not my
+# own comments (marked with "[MSDW]") unless otherwise noted.
+#
+# Depending on your platform, you may need his patch which uses
+# IPC/semaphores to get information which may be required to allow two
+# simultaneous instances to accept() a connection, which can be obtained at
+# http://bent.latency.net/smtpprox/smtpprox-semaphore-patch. It is best to
+# apply the patch to the original script, then port it to this one.
+#
+# --irj
+
+# [MSDW]
+# This should allow a kill on the parent to also blow away the
+# children, I hope
+my %children;
+use vars qw($please_die);
+$please_die = 0;
+$SIG{INT} = sub { $please_die = 1; };
+$SIG{TERM} = sub { $please_die = 1; }; # logmsg "server killed by SIGTERM, shutting down";
+
+# [MSDW]
+# This sets up the parent process
+
+PARENT: while (1) {
+ while (scalar(keys %children) >= $children) {
+ my $child = wait;
+ delete $children{$child} if exists $children{$child};
+ if ($please_die) { kill 15, keys %children; exit 0; }
+ }
+ my $pid = fork;
+ die "$0: fork failed: $!\n" unless defined $pid;
+ last PARENT if $pid == 0;
+ $children{$pid} = 1;
+ select(undef, undef, undef, 0.1);
+ if ($please_die) { kill 15, keys %children; exit 0; }
+}
+
+# [MSDW]
+# This block is a child service daemon. It inherited the bound
+# socket created by SMTP::Server->new, it will service a random
+# number of connection requests in [minperchild..maxperchild] then
+# exit
+
+my $lives = $minperchild + (rand($maxperchild - $minperchild));
+
+while(my $conn = $server->accept()) {
+
+ my $client = new Net::SMTP::Server::Client($conn) ||
+ next;
+
+ my $start = time;
+
+ # [MSDW]
+ # Process the client. This command will block until
+ # the connecting client completes the SMTP transaction.
+ $client->process || next;
+
+# we'll have to revisit this later
+# if ($opt_q) {
+# handle_user_sql($1);
+# }
+
+ my $resp = "EX_OK";
+
+ # Now read in message
+ my $message = $client->{MSG};
+ my @msglines = split ("\r\n", $message);
+ my $arraycont = @msglines; for(0..$arraycont) { $msglines[$_] .= "\r\n"; }
+ # Audit the message
+ my $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines,
+ add_From_line => $opt_F
+ );
+
+ # Check spamminess and rewrite mail if high spam factor or option -a (tag All)
+ my $status = $spamtest->check($mail);
+ if ( $status->is_spam || $opt_a ) {
+ $status->rewrite_mail;
+ }
+
+ # Build the message to send back
+ my $msg_resp = join '',$mail->header,"\n",@{$mail->body};
+
+ # Relay the (rewritten) message through perl SmartHost module
+ my $relay = new Mail::SpamAssassin::SMTP::SmartHost($client->{FROM},
+ $client->{TO},
+ $msg_resp,
+ "$smarthost",
+ "$myhelo");
+
+ # Log what we did, FWIW
+ my $was_it_spam;
+ if($status->is_spam) { $was_it_spam = 'identified spam'; } else { $was_it_spam = 'clean message'; }
+ my $msg_score = int($status->get_hits);
+ my $msg_threshold = int($status->get_required_hits);
+ #$current_user ||= '(unknown)';
+ logmsg "$was_it_spam ($msg_score/$msg_threshold) in ".
+ sprintf("%3d", time - $start) ." seconds.\n";
+
+ $status->finish(); # added by jm to allow GC'ing
+
+ # Zap this instance if this child's processing limit has been reached.
+ # --irj
+ delete $server->{"s"};
+ if ($lives-- <= 0) {
+ if ($opt_D) {
+ warn "killing child process\n";
+ }
+ exit 0;
+ }
+}
+
+sub handle_user_sql
+{
+ $current_user = shift;
+ $spamtest->load_scoreonly_sql ($current_user);
+ return 1;
+}
+
+sub logmsg
+{
+ openlog('spamd','cons,pid',$log_facility);
+ syslog('info',"@_");
+ if ($opt_D) { warn "logmsg: @_\n"; }
+}
+
+sub kill_handler
+{
+ my ($sig) = @_;
+ logmsg "server killed by SIG$sig, shutting down";
+ $please_die = 1;
+ return 1;
+ #close Server;
+ #exit 0;
+}
+
+use POSIX 'setsid';
+sub daemonize
+{
+ chdir '/' or die "Can't chdir to '/': $!";
+ open STDIN,'/dev/null' or die "Can't read '/dev/null': $!";
+ open STDOUT,'>/dev/null' or die "Can't write '/dev/null': $!";
+ defined(my $pid=fork) or die "Can't fork: $!";
+ exit if $pid;
+ setsid or die "Can't start new session: $!";
+ open STDERR,'>&STDOUT' or die "Can't duplicate stdout: $!";
+}
+
+=head1 NAME
+
+spampd - daemonized version of spamassassin with SMTP IO interface
+
+=head1 SYNOPSIS
+
+spampd [options]
+
+=head1 OPTIONS
+
+=over
+
+=item B<-w>
+
+Use auto-whitelists. These will automatically create a list of
+senders whose messages are to be considered non-spam by monitoring the total
+number of received messages which weren't tagged as spam from that sender.
+Once a threshold is exceeded, further messages from that sender will be given a
+non-spam bonus (in case you correspond with people who occasionally swear in
+their emails).
+
+=item B<-a>
+
+Tag All messages with SpamAssassin X-Spam-Status header, even if non spam. Default
+is to tag spam only.
+
+=item B<-d>
+
+Detach from starting process and run in background (daemonize).
+
+=item B<-h>
+
+Print a brief help message, then exit without further action.
+
+=item B<-i> I<ipaddress>
+
+Tells spamd to listen on the specified IP address [defaults to 127.0.0.1]. Use
+0.0.0.0 to listen on all interfaces.
+
+=item B<-p> I<port>
+
+Optionally specifies the port number for the server to listen on.
+
+=item B<-t> I<ipaddress>
+
+Use specified IP address as relay (To) host (default: 127.0.0.1)
+
+=item B<-v> I<port>
+
+Use specified port on the relay host specified with -t (default: 10026)
+
+=item B<-g> I<hostname>
+
+Use specified hostname in the SMTP HELO greeting to the relay host (default: spamfilter.localdomain)
+
+=item B<-q>
+
+Turn on SQL lookups even when per-user config files have been disabled
+with B<-x>. this is useful for spamd hosts which don't have user's
+home directories but do want to load user preferences from an SQL
+database.
+
+=item B<-s> I<facility>
+
+Specify the syslog facility to use (default: mail).
+
+=item B<-u> I<username>
+
+Run as the named user. The alternative, default behaviour is to setuid() to
+the user running C<spamc>, if C<spamd> is running as root.
+
+=item B<-D>
+
+Print debugging messages
+
+=item B<-C>
+
+Number of child processes to create (Default: 4)
+
+=item B<-m>
+
+Minumum number of connections to handle per child before exiting (Default: 5)
+
+=item B<-M>
+
+Maximum number of connections to handle per child before exiting (Default: 10)
+
+=item B<-L>
+
+Perform only local tests on all mail. In other words, skip DNS and other
+network tests. Works the same as the C<-L> flag to C<spamassassin(1)>.
+
+=item B<-P>
+
+Die on user errors (for the user passed from spamc) instead of falling back to
+user I<nobody> and using the default configuration.
+
+=item B<-F> I<0 | 1>
+
+Ensure that the output email message either always starts with a 'From ' line
+(I<1>) for UNIX mbox format, or ensure that this line is stripped from the
+output (I<0>). (default: 1)
+
+=back
+
+=head1 DESCRIPTION
+
+The purpose of this program is to provide a daemonized version of the
+spamassassin executable. The goal is improving throughput performance for
+automated mail checking.
+
+This version uses SMTP as the I/O transport. It is inteded to be used as a
+Postfix content_filter or other transport agent.
+
+This code is a merging of spamd (copyright 2001 by Craig Hughes)
+and spamproxyd by Ian R. Justman.
+Like spamproxyd, it is written with Postfix "advanced" content
+filtering in mind. See FILTER_README in the Postfix distribution
+for more information on how to set this up.
+
+The primary difference between spamproxyd and spampd is that
+spampd acutally tags the spams and sends them on, even making
+use of the auto-whitelist feature (and soon the SQL lookups
+based on the recipient email address, maybe).
+
+The primary difference between spamd and spampd is that spampd
+talks SMTP protocol for its I/O stream (via Net::SMTP::Server
+and Mail::SpamAssassin::SMTP::SmartHost).
+
+WARNING: Use at your own risk. Basically I have no idea what
+I'm doing with the process forking stuff, I just copied it and
+messed around until it worked (for me, YMMV). Also the demonizing
+stuff seems screwy when you go to kill the daemon (at least via inet.d
+script), but it does exit and clean up so no harm seems to be done.
+
+Development/production environment is RH Linux 7.x on various x86 hardware.
+
+BUG: for some reason the log and warn (if -D) messages don't print during
+SIGTERM or when a child dies (after processing it's allotted # of msgs).
+I have no idea why (see above)
+
+=head1 SEE ALSO
+
+spamassassin(1)
+Mail::SpamAssassin(3)
+
+=head1 AUTHOR
+
+Maxim Paperno E<lt>MPaperno@worldDesign.comE<gt>
+
+=head1 CREDITS
+
+Justin Mason and Craig Hughes for B<Mail::SpamAssassin>
+and B<spamd>
+
+Ian R. Justman for his B<spamproxyd> implementation
+
+Habeeb J. "MacGyver" Dihu for his B<Net::SMTP::Server> code
+
+Bennett Todd for the perforking code and option-parsing code from his
+ pacakge, smtpproxy (used via spamproxyd code)
+
+=head1 PREREQUISITES
+
+C<Mail::SpamAssassin>
+C<Mail::SpamAssassin::SMTP::SmartHost>
+
+=cut
diff --git a/previous-versions/spampd-0.0.5.pl b/previous-versions/spampd-0.0.5.pl
new file mode 100644
index 0000000..05c32de
--- /dev/null
+++ b/previous-versions/spampd-0.0.5.pl
@@ -0,0 +1,510 @@
+#! /usr/bin/perl
+
+# $Id: assassind,v 1.1 2002/04/28 19:08:22 dave Exp $
+# $Source: /var/cvs/src/assassind/assassind,v $
+# Copyright (c) 2002 Dave Carrigan
+
+package Assassind;
+
+use strict;
+use Net::Server::PreFork;
+use Net::SMTP::Server::Client;
+use IO::File;
+use Getopt::Long;
+use Data::Dumper;
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::NoMailAudit;
+#use Mail::Audit;
+use Net::SMTP;
+use Error qw(:try);
+
+our @ISA = qw(Net::Server::PreFork);
+our $VERSION = '1.0.1';
+
+sub dead_letter {
+ my($self, $client, $message) = @_;
+
+ my $filename = join("/", $self->{assassind}->{dead_letters},
+ sprintf("assassind.%d.%d.%f.dead", time(), $$, rand));
+
+ my $dead = IO::File->new;
+ unless ($dead->open(">$filename")) {
+ $self->log(0, "Can't open dead letter file $filename: $!");
+ return;
+ }
+ chmod 0600, $filename;
+
+ try {
+ if (defined $message) {
+ $dead->print($message, "\n") or
+ throw Error -text => "Can't print to dead letter: $!";
+ }
+ foreach (@{$client->{TO}}) {
+ $dead->print("TO $_\n") or
+ throw Error -text => "Can't print to dead letter: $!";
+ }
+ $dead->print("FROM ", $client->{FROM}, "\n") or
+ throw Error -text => "Can't print to dead letter: $!";
+ $dead->print($client->{MSG}) or
+ throw Error -text => "Can't print to dead letter: $!";
+ } catch Error with {
+ my $e = shift;
+ $self->log(0, "Warning!!!! Couldn't print dead letter: " . $e->stringify);
+ };
+
+ unless ($dead->close) {
+ $self->log(0, "Warning!!!! Could not close the dead letter file: $!");
+ }
+}
+
+sub relay_message {
+ my($self, $client) = @_;
+
+ my $start = time;
+ my $msg_resp;
+
+ # Now read in message
+ my $message = $client->{MSG};
+
+ # Skip processing message over 256K (need to make this an option)
+ if ( length($message) < ($self->{assassind}->{maxsize} * 1024) ) {
+
+ my @msglines = split (/\r?\n/, $message);
+ my $arraycont = @msglines; for(0..$arraycont) { $msglines[$_] .= "\r\n"; }
+ # Audit the message
+ my $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines,
+ add_From_line => 0
+ );
+
+ my $assassin = $self->{assassind}->{assassin};
+ # Check spamminess and rewrite mail if high spam factor or option -a (tag All)
+ my $status = $assassin->check($mail);
+ if ( $status->is_spam || $self->{assassind}->{tagall} ) {
+ $status->rewrite_mail;
+ }
+
+ # Build the message to send back
+ $msg_resp = join '',$mail->header,"\n",@{$mail->body};
+
+ # Log what we did, FWIW
+ my $was_it_spam;
+ if($status->is_spam) { $was_it_spam = 'identified spam'; } else { $was_it_spam = 'clean message'; }
+ my $msg_score = int($status->get_hits);
+ my $msg_threshold = int($status->get_required_hits);
+ #$current_user ||= '(unknown)';
+ $self->log(2, "$was_it_spam ($msg_score/$msg_threshold) in ". sprintf("%3d", time - $start) ." seconds.");
+
+ $status->finish();
+
+ } else {
+
+ $msg_resp = $message;
+ $self->log(2, "Scanning skipped due to size (". length($message) .")");
+
+ }
+
+# my $message = [split(/\r?\n/, $client->{MSG})];
+# my $auditor = Mail::Audit->new(data => $message);
+# my $assassin = $self->{assassind}->{assassin};
+# my $status = $assassin->check($auditor);
+
+# my $score = $status->get_hits;
+# my $spam_color = 'red';
+# foreach my $color (qw(green blue yellow orange)) {
+# if ($score <= $self->{assassind}->{$color}) {
+# $spam_color = $color;
+# last;
+# }
+# }
+
+# $auditor->put_header('X-Spam-Color', $spam_color);
+# my $is_spam =$status->is_spam? 'Yes' : 'No';
+# $auditor->put_header('X-Spam-Status',
+# sprintf("%s, hits=%.2f required=%.2f tests=%s",
+# $is_spam,
+# $status->get_hits,
+# $status->get_required_hits,
+# $status->get_names_of_tests_hit));
+
+# if ($spam_color ne 'green') {
+# foreach (split(/\n/, $status->get_report)) {
+# $auditor->put_header('X-Spam-Report', $_);
+# }
+# }
+
+# $status->finish;
+
+ my $smtp = Net::SMTP->new($self->{assassind}->{relayhost}, Hello => $self->{assassind}->{heloname});
+ unless (defined $smtp) {
+ $self->log(1, "Connection to SMTP server failed");
+ $self->dead_letter($client);
+ return;
+ }
+
+ try {
+ $smtp->mail($client->{FROM});
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+
+ foreach (@{$client->{TO}}) {
+ $smtp->recipient($_);
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+ }
+
+ $smtp->data($msg_resp);
+# $smtp->data;
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+
+# $smtp->datasend($auditor->header);
+# $smtp->datasend("\n");
+# foreach (@{$auditor->body}) {
+# $smtp->datasend($_ . "\r\n");
+# }
+# $smtp->dataend;
+# throw Error -text => sprintf("Relay failed; server said %s %s",
+# $smtp->code, $smtp->message) unless $smtp->ok;
+
+ $smtp->quit;
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+ $self->log(4, "Message relayed successfully.");
+ } catch Error with {
+ my $e = shift;
+ $self->dead_letter($client, $e->stringify);
+ };
+}
+
+sub process_request {
+ my $self = shift;
+ my $client = Net::SMTP::Server::Client->new($self->{server}->{client});
+ if ($client->process) {
+ $self->log(4, "Received message");
+ $SIG{TERM} = sub {
+ $self->dead_letter($client, "Process interrupted by SIGTERM");
+ };
+ $self->relay_message($client);
+ $SIG{TERM} = sub { exit 0; };
+ } else {
+ $self->log(1, "An error occurred while receiving message");
+ }
+ $self->{assassind}->{instance} = 1 unless defined $self->{assassind}->{instance};
+ exit 0 if $self->{assassind}->{instance} > $self->{assassind}->{maxrequests}++;
+}
+
+my $relayhost = 'localhost';
+my $host = 'localhost';
+my $port = 2025;
+my $maxrequests = 20;
+my $dead_letters = '/var/tmp';
+my $pidfile = '/var/run/assassind.pid';
+my $user = 'mail';
+my $group = 'mail';
+my $tagall = 0;
+my $maxsize = 256;
+my $heloname = 'spamfilter.localdomain';
+# my $auto_whitelist = 0;
+# my $stop_at_threshold = 0;
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ 'dead-letters' => \$dead_letters,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ tagall => \$tagall,
+ maxsize => \$maxsize,
+ heloname => \$heloname
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'maxrequests=i',
+ 'dead-letters=s',
+ 'user=s',
+ 'group=s',
+ 'pid=s',
+ 'tagall=i',
+ 'maxsize=i',
+ 'heloname=s',
+ 'auto-whitelist',
+ 'stop-at-threshold',
+ 'debug',
+ 'help');
+usage(0) if $options{help};
+
+my $assassin = Mail::SpamAssassin->new({
+ 'dont_copy_prefs' => 1,
+ 'stop_at_threshold' => $options{'stop_at_threshold'} || 0,
+ 'debug' => $options{'debug'} || 0 });
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now();
+$/ = "\n"; # argh, Razor resets this! Bad Razor!
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ },
+ assassind => {maxrequests => $maxrequests,
+ relayhost => $relayhost,
+ dead_letters => $dead_letters,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ heloname => $heloname,
+ },
+ }, 'Assassind';
+$server->run;
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ --port=port ]
+
+Options:
+ --port=n Port to listen on. Defaults to 2025.
+ --host=host Hostname/IP to listen on. Default is localhost.
+ --relayhost=host[:port] Host to relay mail to. Defaults to localhost.
+ --maxrequests=n Maximum requests that each child can process before exiting.
+ Defaults to 20.
+ --pid=filename Store the daemon's process ID in this file.
+ --user=username Specifies the user that the daemon runs as. Default is mail.
+ --group=groupname Specifies the group that the daemon runs as. Default is mail.
+ --dead-letters=path Path to store letters that couldn't be relayed.
+ Defaults to /tmp.
+ --tagall=n Tag all messages not just spam (specify 1/0). Defaults to 0.
+ --maxsize=n Maximum size of mail to scan (in KB). Defaults to 256.
+ --heloname=hostname Hostname to use in HELO command when sending mail.
+ Defaults to 'spamfilter.localdomain'.
+
+ --auto-whitelist Use the global SA auto-whitelist feature.
+ --stop-at-threshold Use SA feature to stop scanning once threshold is reached.
+ --debug Turn on SA debugging.
+
+ --help This message
+EOF
+ exit shift;
+}
+
+=pod
+
+=head1 NAME
+
+assassind - Spam filtering SMTP proxy that uses SpamAssassin
+
+=head1 SYNOPSIS
+
+B<assassind>
+[B<--port=n>]
+[B<--host=host>]
+[B<--relayhost=hostname[:port]>]
+[B<--user=username>]
+[B<--group=groupname>]
+[B<--maxrequests=n>]
+[B<--dead-letters=/path>]
+[B<--pid=filename>]
+[B<--tagall=n>]
+[B<--maxsize=n>]
+[B<--auto-whitelist>]
+[B<--stop-at-threshold>]
+[B<--debug>]
+[B<--heloname=hostname>]
+
+B<assassind> B<--help>
+
+=head1 DESCRIPTION
+
+I<assassind> is a relaying SMTP proxy that filters spam using
+SpamAssassin. The proxy is designed to be robust in the face of
+exceptional errors, and will (hopefully) never lose a message.
+
+I<assassind> is meant to be used as a system-wide message processor, so
+the proxy does not make any changes to existing message contents or
+headers; instead choosing just to add three headers of its own, which
+end users can use to make decisions about filtering (or not filtering)
+their spam.
+
+The most important header that I<assassind> adds is the B<X-Spam-Color>
+header. This header will have one of five values: I<green>, I<blue>,
+I<yellow>, I<orange> and I<red>. Green messages are very unlikely to be
+spam, while red messages are almost guaranteed to be spam. You can use
+this header as the basis for your own message filtering rules, using any
+common message filtering system (procmail, sieve, etc.).
+
+I<assassind> also adds a B<X-Spam-Status> filter. This header is the
+same as the header generated by the standard SpamAssassin message
+processor, and contains the message's SpamAssassin score and other
+information.
+
+Finally, I<assassind> adds one or more B<X-Spam-Report> headers, which
+contain a plain-text report of the rules that SpamAssassin used to
+assign the message its score.
+
+I<assassind> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+=head1 OPERATION
+
+I<assassind> is meant to operate as a mail relay that sits between the
+Internet and your internal mail system. The three most common
+configurations include
+
+=over 5
+
+=item Running between firewall and internal mail server
+
+The firewall would be configured to forward all of its mail to the port
+that I<assassind> listens on, and I<assassind> would relay its messages
+to port 25 of your internal server. I<assassind> could either run on its
+own host (and listen on any port) or it could run on the mail server
+(and listen on any port except port 25). This is I<assassind> default
+mode of operation.
+
+=item Running on the firewall with an internal mail server
+
+I<assassind> would accept messages on port 25 and forward them to the
+mail server that is also listening on port 25. Note that I<assassind>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. If your current mail system is configured
+correctly for anti-relaying, it should continue to work correctly in
+this configuration, but you may want to verify this using one of the
+standard open-relay blackhole testing systems.
+
+=item Running on the mail server, which is not behind a firewall
+
+In this configuration I<assassind> would listen on port 25, while your
+mail server would be configured to listen on some other port.
+
+=back
+
+OPTIONS
+
+=over 5
+
+=item B<--port=n>
+
+Specifies what port I<assassind> listens on. By default, it listens on
+port 2025.
+
+=item B<--relayhost=hostname[:port]>
+
+Specifies the hostname where I<assassind> will relay all
+messages. Defaults to I<localhost>. If the port is not provided, that
+defaults to 25.
+
+=item B<--user=username>
+=item B<--group=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--maxrequests=n>
+
+I<assassind> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--dead-letters=/path>
+
+Specifies the directory where I<assassind> will store any message that
+it fails to deliver. The default is F</var/tmp>. You should periodically
+examine this directory to see if there are any messages that couldn't be
+delivered.
+
+B<Important!> This path should not be on the same partition as your mail
+server's message spool, because if your mail server rejects a message
+because of a full disk, I<assassind> will not be able to save the
+message, and it will be lost.
+
+=item B<--pid=filename>
+
+Specifies a filename where I<assassind> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<assassind> user. The default is
+F</var/run/assassind/assassind.pid>.
+
+=item B<--green=n>
+=item B<--blue=n>
+=item B<--yellow=n>
+=item B<--orange=n>
+
+Specifies the spam score thresholds for each color. The defaults are 5,
+6, 10 and 20. Anything over 20 will have a color of red.
+
+=back
+
+=head1 EXAMPLES
+
+=over 5
+
+=item Running between firewall and internal mail server
+
+This is I<assassind>'s default configuration, where it listens on port
+2025 on the same host as the mail server.
+
+ assassind
+
+=item Running on the firewall with an internal mail server
+
+ assassind --port=25 --relayhost=internal.serv.er
+
+=item Running on the mail server, which is not behind a firewall
+
+This scenario assumes that the real mail server is running on port 2025
+of the same host.
+
+ assassind --port=25 --relayhost=localhost:2025
+
+=back
+
+=head1 AUTHOR
+
+Dave Carrigan, <dave@rudedog.org>
+
+This program is Copyright © 2002, Dave Carrigan. All rights
+reserved. This program is free software; you can redistribute it and/or
+modify it under the same terms as Perl.
+
+This program is distributed "as is", without warranty of any kind,
+either expressed or implied, including, but not limited to, the implied
+warranties of merchantability and fitness for a particular purpose. The
+entire risk as to the quality and performance of the program is with
+you. Should the program prove defective, you assume the cost of all
+necessary servicing, repair or correction.
+
+
+=head1 SEE ALSO
+
+perl(1), Spam::Assassin(3), http://www.rudedog.org/assassind/
+
+=head1 BUGS
+
+Due to the nature of Perl's SMTP::Server module, a SMTP message is
+stored completely in memory. However, as soon as the module receives its
+entire message data from the SMTP client, it returns a 250, signifying
+to the client that the message has been delivered. However, this means
+that there is a period of time where the message is vulnerable to being
+lost if the I<assassind> process is killed before it has relayed or
+saved the message. Caveat Emptor!
diff --git a/previous-versions/spampd-1.0.2.pl b/previous-versions/spampd-1.0.2.pl
new file mode 100644
index 0000000..38fb8d9
--- /dev/null
+++ b/previous-versions/spampd-1.0.2.pl
@@ -0,0 +1,595 @@
+#! /usr/bin/perl
+
+# spampd - spam proxy daemon
+#
+# v1.0.2 - added 'local-only' (13-Apr-03)
+# v1.0.1 - minor bug fix (3-Feb-03)
+# v1.0.0 - initial release (May 2002)
+#
+# Original assassind code by and Copyright (c) 2002 Dave Carrigan
+#(see http://www.rudedog.org/assassind/)
+# Changed and renamed to spampd by Maxim Paperno (MPaperno@WorldDesign.com)
+# whose contributions are placed in the Public Domain.
+#(see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# 1.0.2 update:
+# - added 'local-only' parameter to pass on to SA which turns off all network-based tests (DNS, Razor, etc).
+#
+# 1.0.1 update:
+# - fixed minor but substantial bug preventing child processes
+# from exiting properly since the counter wasn't being incremented (d'oh!).
+# Thanks to Mark Blackman for pointing this out.
+#
+# - fixed typo in pod docs (Thx to James Sizemore for pointing out)
+#
+# Changes to assassind (1.0.0 initial release of spampd):
+# A different message rewriting method (using
+# Mail::SpamAssassin::NoMailAudit instead of Dave Carrigan's
+# custom headers and Mail::Audit);
+# Adding more options for message handling, network/protocol options,
+# some options to pass on to SpamAssassin (such as whitelist usage);
+# More orientation to being used as a content filter for the
+# Postfix MTA, mostly by changing some default values;
+# Documentation changes;
+#
+
+package SpamPD;
+
+use strict;
+use Net::Server::PreFork;
+use IO::File;
+use Getopt::Long;
+use Net::SMTP;
+use Net::SMTP::Server::Client;
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::NoMailAudit;
+use Error qw(:try);
+
+our @ISA = qw(Net::Server::PreFork);
+our $VERSION = '1.0.1';
+
+sub dead_letter {
+ my($self, $client, $message) = @_;
+
+ my $filename = join("/", $self->{spampd}->{dead_letters},
+ sprintf("spampd.%d.%d.%f.dead", time(), $$, rand));
+
+ my $dead = IO::File->new;
+ unless ($dead->open(">$filename")) {
+ $self->log(0, "Can't open dead letter file $filename: $!");
+ return;
+ }
+ chmod 0600, $filename;
+
+ try {
+ if (defined $message) {
+ $dead->print($message, "\r\n") or
+ throw Error -text => "Can't print to dead letter: $!";
+ }
+ foreach (@{$client->{TO}}) {
+ $dead->print("TO $_\r\n") or
+ throw Error -text => "Can't print to dead letter: $!";
+ }
+ $dead->print("FROM ", $client->{FROM}, "\r\n\r\n") or
+ throw Error -text => "Can't print to dead letter: $!";
+ $dead->print($client->{MSG}) or
+ throw Error -text => "Can't print to dead letter: $!";
+ } catch Error with {
+ my $e = shift;
+ $self->log(0, "Warning!!!! Couldn't print dead letter: " . $e->stringify);
+ };
+
+ unless ($dead->close) {
+ $self->log(0, "Warning!!!! Could not close the dead letter file: $!");
+ }
+}
+
+sub relay_message {
+ my($self, $client) = @_;
+
+ my $start = time;
+ my $msg_resp;
+
+ # Now read in message
+ my $message = $client->{MSG};
+
+ # Skip processing message over n KB
+ if ( length($message) < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ # prep the message (is this necessary?)
+ my @msglines = split (/\r?\n/, $message);
+ my $arraycont = @msglines; for(0..$arraycont) { $msglines[$_] .= "\r\n"; }
+
+ # Audit the message
+ my $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines
+ );
+
+ my $assassin = $self->{spampd}->{assassin};
+ # Check spamminess
+ my $status = $assassin->check($mail);
+ # Rewrite mail if high spam factor or option --tagall
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+ $status->rewrite_mail;
+ }
+
+ # Build the message to send back
+ $msg_resp = join '',$mail->header,"\n",@{$mail->body};
+
+ # Log what we did, FWIW
+ my $was_it_spam;
+ if($status->is_spam) { $was_it_spam = 'identified spam'; } else { $was_it_spam = 'clean message'; }
+ my $msg_score = int($status->get_hits);
+ my $msg_threshold = int($status->get_required_hits);
+ $self->log(2, "$was_it_spam ($msg_score/$msg_threshold) in ". sprintf("%3d", time - $start) ." seconds.");
+
+ $status->finish();
+
+ } else {
+
+ $msg_resp = $message;
+ $self->log(2, "Scanning skipped due to size (". length($message) .")");
+
+ }
+
+ my $smtp = Net::SMTP->new($self->{spampd}->{relayhost}, Hello => $self->{spampd}->{heloname});
+ unless (defined $smtp) {
+ $self->log(1, "Connection to SMTP server failed");
+ $self->dead_letter($client);
+ return;
+ }
+
+ try {
+ $smtp->mail($client->{FROM});
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+
+ foreach (@{$client->{TO}}) {
+ $smtp->recipient($_);
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+ }
+
+ $smtp->data($msg_resp);
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+
+ $smtp->quit;
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+ $self->log(4, "Message relayed successfully.");
+ } catch Error with {
+ my $e = shift;
+ $self->dead_letter($client, $e->stringify);
+ };
+}
+
+sub process_request {
+ my $self = shift;
+ my $client = Net::SMTP::Server::Client->new($self->{server}->{client});
+ if ($client->process) {
+ $self->log(2, "Received message from '".$client->{FROM}."'");
+ $SIG{TERM} = sub {
+ $self->dead_letter($client, "Process interrupted by SIGTERM");
+ };
+ $self->relay_message($client);
+ $SIG{TERM} = sub { exit 0; };
+ } else {
+ $self->log(1, "An error occurred while receiving message");
+ }
+ $self->{spampd}->{instance} = 1 unless defined $self->{spampd}->{instance};
+ exit 0 if $self->{spampd}->{instance}++ > $self->{spampd}->{maxrequests};
+}
+
+my $relayhost = '127.0.0.1';
+my $host = '127.0.0.1';
+my $port = 10025;
+my $maxrequests = 20;
+my $dead_letters = '/var/tmp';
+my $pidfile = '/var/run/spampd.pid';
+my $user = 'mail';
+my $group = 'mail';
+my $tagall = 0;
+my $maxsize = 64;
+my $heloname = 'spampd.localdomain';
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ 'dead-letters' => \$dead_letters,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ heloname => \$heloname
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'maxrequests=i',
+ 'dead-letters=s',
+ 'user=s',
+ 'group=s',
+ 'pid=s',
+ 'maxsize=i',
+ 'heloname=s',
+ 'tagall',
+ 'auto-whitelist',
+ 'stop-at-threshold',
+ 'debug',
+ 'help',
+ 'local-only');
+
+usage(0) if $options{help};
+if ( $options{tagall} ) { $tagall = 1; }
+
+my $assassin = Mail::SpamAssassin->new({
+ 'dont_copy_prefs' => 1,
+ 'stop_at_threshold' => $options{'stop_at_threshold'} || 0,
+ 'debug' => $options{'debug'} || 0,
+ 'local_tests_only' => $options{'local-only'} || 0 });
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now();
+$/ = "\n"; # argh, Razor resets this! Bad Razor!
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ },
+ spampd => {maxrequests => $maxrequests,
+ relayhost => $relayhost,
+ dead_letters => $dead_letters,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ heloname => $heloname,
+ },
+ }, 'SpamPD';
+$server->run;
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --port=n Port to listen on. Defaults to 10025.
+ --host=host Hostname/IP to listen on. Default is 127.0.0.1
+ --relayhost=host[:port] Host to relay mail to.
+ Defaults to 127.0.0.1 on port 25.
+ --heloname=hostname Hostname to use in HELO command when sending mail.
+ Defaults to 'spampd.localdomain'.
+
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Defaults to 20.
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+ --dead-letters=path Path to store letters that couldn't be relayed.
+ Defaults to /var/tmp.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --tagall Tag all messages with a header, not just spam.
+
+ --auto-whitelist Use the SA global auto-whitelist feature.
+ --stop-at-threshold Use SA feature to stop scanning once score
+ threshold is reached.
+ --local-only Turn off all SA network-based tests (DNS, Razor, etc).
+ --debug Turn on SA debugging.
+
+ --help This message
+EOF
+ exit shift;
+}
+
+=pod
+
+=head1 NAME
+
+spampd - Spam Proxy Daemon
+
+=head1 SYNOPSIS
+
+B<spampd>
+[B<--port=n>]
+[B<--host=host>]
+[B<--relayhost=hostname[:port]>]
+[B<--heloname=hostname>]
+[B<--user=username>]
+[B<--group=groupname>]
+[B<--maxrequests=n>]
+[B<--dead-letters=/path>]
+[B<--pid=filename>]
+[B<--maxsize=n>]
+[B<--tagall>]
+[B<--auto-whitelist>]
+[B<--stop-at-threshold>]
+[B<--local-only>]
+[B<--debug>]
+
+B<spampd> B<--help>
+
+=head1 DESCRIPTION
+
+I<spampd> is a relaying SMTP proxy that filters spam using
+SpamAssassin (http://www.SpamAssassin.org). The proxy is designed
+to be robust in the face of exceptional errors, and will (hopefully)
+never lose a message.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+=head1 REQUIRES
+
+Perl modules:
+
+B<Error>
+
+B<Mail::SpamAssassin>
+
+B<Net::Server>
+
+B<Net::SMTP>
+
+
+=head1 OPERATION
+
+I<spampd> is meant to operate as an SMTP mail relay which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing!
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+Internet -> [ MX gateway (@inter.net.host:25) ->
+ I<spampd> (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+
+=item Using Postfix advanced content filtering
+
+Please see the FILTER_README that came with the Postfix distribution. You
+need to have a version of Postfix which supports this.
+
+Internet -> [ I<Postfix> (@inter.net.host:25) ->
+ I<spampd> (@localhost:10025) ->
+ I<Postfix> (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+=head1 OPTIONS
+
+=over 5
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025.
+
+=item B<--host=ip>
+
+Specifies what interface/IP I<spampd> listens on. By default, it listens on
+127.0.0.1 (localhost).
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--relayhost=hostname[:port]>
+
+Specifies the hostname where I<spampd> will relay all
+messages. Defaults to 127.0.0.1. If the port is not provided, that
+defaults to 25.
+
+=item B<--heloname=hostname>
+
+Hostname to use in HELO command when sending mail. Default is
+'spampd.localdomain'. The HELO name may show up in the
+Received headers of any processed message, depending on your setup.
+
+=item B<--user=username>
+
+=item B<--group=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--dead-letters=/path>
+
+Specifies the directory where I<spampd> will store any message that
+it fails to deliver. The default is F</var/tmp>. You should periodically
+examine this directory to see if there are any messages that couldn't be
+delivered.
+
+B<Important!> This path should not be on the same partition as your mail
+server's message spool, because if your mail server rejects a message
+because of a full disk, I<spampd> will not be able to save the
+message, and it will be lost.
+
+=item B<--pid=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--tagall>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings).
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KB. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. This includes headers.
+
+=item B<--auto-whitelist>
+
+Turns on the SpamAssassin global whitelist feature. See the SA docs. Note
+that per-user whitelists are not available.
+
+=item B<--stop-at-threshold>
+
+Turns on the SpamAssassin (v2.20 and up) "stop at threshold" feature which
+stops any further scanning of a message once the minimum spam score
+is reached. See the SA docs for more info.
+
+=item B<--local-only>
+
+Turn off all SA network-based tests (DNS, Razor, etc).
+
+=item B<--debug>
+
+Turns on SpamAssassin debug messages.
+
+=item B<--help>
+
+Prints usage information.
+
+=back
+
+=head1 EXAMPLES
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 AUTHORS
+
+Based on I<assassind> by Dave Carrigan, <dave@rudedog.org>
+see http://www.rudedog.org/assassind/
+
+Modified and renamed to I<spampd> (to avoid confusion) by
+Maxim Paperno, <MPaperno@WorldDesign.com>. My modifications are mostly
+based on code included with the SpamAssassin distribution, namely spamd
+and spamproxy.
+
+=head1 COPYRIGHT AND DISCLAIMER
+
+Portions of this program are Copyright © 2002, Dave Carrigan, all rights
+reserved. Other contributions can be considered Public Domain property.
+This program is free software; you can redistribute it and/or
+modify it under the same terms as Perl.
+
+This program is distributed "as is", without warranty of any kind,
+either expressed or implied, including, but not limited to, the implied
+warranties of merchantability and fitness for a particular purpose. The
+entire risk as to the quality and performance of the program is with
+you. Should the program prove defective, you assume the cost of all
+necessary servicing, repair or correction.
+
+=head1 BUGS
+
+Due to the nature of Perl's SMTP::Server module, an SMTP message is
+stored completely in memory. However, as soon as the module receives its
+entire message data from the SMTP client, it returns a 250, signifying
+to the client that the message has been delivered. This means
+that there is a period of time where the message is vulnerable to being
+lost if the I<spampd> process is killed before it has relayed or
+saved the message. Caveat Emptor!
+
+No message loop protection.
+
+Net::SMTP::Server::Client has a "problem" with spaces in email addresses.
+For example during the SMTP dialog, if a mail is
+FROM:<"some spammer"@some.dom.ain> the address gets truncated after
+the first space to just '<"some' . This causes a problem when relaying
+the message to the receiving server, because the sender address is now
+in an illegal format. The mail is then rejected, and it ends
+up in the dead-letters directory. I have actually seen this happen several
+times, and of course they were bogus messages each time. I don't believe
+there are any legitimate envelope email addresses with spaces in them,
+so don't see this as much of an issue (except that it's un elegant).
+
+
+=head1 TO DO
+
+Add option for extracting recipient address(es) and using SpamAssassin's
+SQL lookup capability check for user-specific preferences.
+
+Deal with above bugs.
+
+=head1 SEE ALSO
+
+perl(1), Spam::Assassin(3), http://www.spamassassin.org/,
+http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm, http://www.rudedog.org/assassind/
diff --git a/previous-versions/spampd-2.00.pl b/previous-versions/spampd-2.00.pl
new file mode 100644
index 0000000..6c5b533
--- /dev/null
+++ b/previous-versions/spampd-2.00.pl
@@ -0,0 +1,1071 @@
+#! /usr/bin/perl
+
+######################
+# SpamPD - spam proxy daemon
+#
+# v2.00 - 8-June-03
+# v1.0.2 - 13-Apr-03
+# v1.0.1 - 3-Feb-03
+# v1.0.0 - May 2002
+#
+# spampd is Copyright (c) 2002 by World Design Group and Maxim Paperno
+# (see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# Written and maintained by Maxim Paperno (MPaperno@WorldDesign.com)
+#
+# 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.
+#
+# The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+#
+# spampd v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+# Stanley Dean Witter. These are also distributed under the GNU GPL (see
+# module code for more details). Both modules have been slightly modified
+# from the originals and are included in this file under new names.
+#
+# spampd v1 was based on code by Dave Carrigan named assassind. Trace amounts
+# of his code or documentation may still remain. Thanks to him for the
+# original inspiration and code. (see http://www.rudedog.org/assassind/)
+#
+######################
+
+
+################################################################################
+package SpamPD::Server;
+
+# Originally known as MSDW::SMTP::Server
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =item DESCRIPTION
+#
+# This server simply gathers the SMTP acquired information (envelope
+# sender and recipient, and data) into unparsed memory buffers (or a
+# file for the data), and returns control to the caller to explicitly
+# acknowlege each command or request. Since acknowlegement or failure
+# are driven explicitly from the caller, this module can be used to
+# create a robust SMTP content scanning proxy, transparent or not as
+# desired.
+#
+# =cut
+
+use strict;
+use IO::File;
+#use IO::Socket;
+
+# =item new(interface => $interface, port => $port);
+
+# The #interface and port to listen on must be specified. The interface
+# must be a valid numeric IP address (0.0.0.0 to listen on all
+# interfaces, as usual); the port must be numeric. If this call
+# succeeds, it returns a server structure with an open
+# IO::Socket::INET in it, ready to listen on. If it fails it dies, so
+# if you want anything other than an exit with an explanatory error
+# message, wrap the constructor call in an eval block and pull the
+# error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die.
+#
+# =cut
+
+sub new {
+
+# This now emulates Net::SMTP::Server::Client for use with Net::Server which
+# passes an already open socket.
+
+ my($this, $socket) = @_;
+
+ my $class = ref($this) || $this;
+ my $self = {};
+ $self->{sock} = $socket;
+
+ bless($self, $class);
+
+ die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+ $self->{state} = 'just bound';
+ return $self;
+
+# Original code, removed by MP for spampd use
+#
+# my ($this, @opts) = @_;
+# my $class = ref($this) || $this;
+# my $self = bless { @opts }, $class;
+# $self->{sock} = IO::Socket::INET->new(
+# LocalAddr => $self->{interface},
+# LocalPort => $self->{port},
+# Proto => 'tcp',
+# Type => SOCK_STREAM,
+# Listen => 65536,
+# Reuse => 1,
+# );
+# die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+# $self->{state} = 'just bound',
+# return $self;
+
+}
+
+# =item accept([debug => FD]);
+#
+# accept takes optional args and returns nothing. If an error occurs
+# it dies, otherwise it returns when a client connects to this server.
+# This is factored out as a separate entry point to allow preforking
+# (e.g. Apache-style) or fork-per-client strategies to be implemented
+# on the common protocol core. If a filehandle is passed for debugging
+# it will receive a complete trace of the entire SMTP dialogue, data
+# and all. Note that nothing in this module sends anything to the
+# client, including the initial login banner; all such backtalk must
+# come from the calling program.
+#
+# =cut
+
+# sub accept {
+# my ($self, @opts) = @_;
+# %$self = (%$self, @opts);
+# #($self->{"s"}, $self->{peeraddr}) = $self->{sock}->accept
+# $self->{"s"} = $self->{sock}
+# or die "$0: accept failure: $!\n";
+# $self->{state} = ' accepted';
+# }
+
+
+# =item chat;
+#
+# The chat method carries the SMTP dialogue up to the point where any
+# acknowlegement must be made. If chat returns true, then its return
+# value is the previous SMTP command. If the return value begins with
+# 'mail' (case insensitive), then the attribute 'from' has been filled
+# in, and may be checked; if the return value begins with 'rcpt' then
+# both from and to have been been filled in with scalars, and should
+# be checked, then either 'ok' or 'fail' should be called to accept
+# or reject the given sender/recipient pair. If the return value is
+# 'data', then the attributes from and to are populated; in this case,
+# the 'to' attribute is a reference to an anonymous array containing
+# all the recipients for this data. If the return value is '.', then
+# the 'data' attribute (which may be pre-populated in the "new" or
+# "accept" methods if desired) is a reference to a filehandle; if it's
+# created automatically by this module it will point to an unlinked
+# tmp file in /tmp. If chat returns false, the SMTP dialogue has been
+# completed and the socket closed; this server is ready to exit or to
+# accept again, as appropriate for the server style.
+#
+# The return value from chat is also remembered inside the server
+# structure in the "state" attribute.
+#
+# =cut
+
+sub chat {
+ my ($self) = @_;
+ local(*_);
+ if ($self->{state} !~ /^data/i) {
+ return 0 unless defined($_ = $self->_getline);
+ s/[\r\n]*$//;
+ $self->{state} = $_;
+ if (s/^helo\s+//i) {
+ s/\s*$//;s/\s+/ /g;
+ $self->{helo} = $_;
+ } elsif (s/^rset\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ } elsif (s/^mail\s+from:\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ s/\s*$//;
+ $self->{from} = $_;
+ } elsif (s/^rcpt\s+to:\s*//i) {
+ s/\s*$//; s/\s+/ /g;
+ $self->{to} = $_;
+ push @{$self->{recipients}}, $_;
+ } elsif (/^data/i) {
+ $self->{to} = $self->{recipients};
+ }
+ } else {
+ if (defined($self->{data})) {
+ $self->{data}->seek(0, 0);
+ $self->{data}->truncate(0);
+ # $self->{data} = undef;
+ } else {
+ $self->{data} = IO::File->new_tmpfile;
+ # $self->{data} = undef;
+ }
+ while (defined($_ = $self->_getline)) {
+ if ($_ eq ".\r\n") {
+ $self->{data}->seek(0,0);
+ return $self->{state} = '.';
+ }
+ s/^\.\./\./;
+ $self->{data}->print($_) or die "$0: write error saving data\n";
+ # $self->{data} .= $_;
+ }
+ return(0);
+ }
+ return $self->{state};
+}
+
+# =item ok([message]);
+#
+# Approves of the data given to date, either the recipient or the
+# data, in the context of the sender [and, for data, recipients]
+# already given and available as attributes. If a message is given, it
+# will be sent instead of the internal default.
+#
+# =cut
+
+sub ok {
+ my ($self, @msg) = @_;
+ @msg = ("250 ok.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# =item fail([message]);
+#
+# Rejects the current info; if processing from, rejects the sender; if
+# processing 'to', rejects the current recipient; if processing data,
+# rejects the entire message. If a message is specified it means the
+# exact same thing as "ok" --- simply send that message to the sender.
+#
+# =cut
+
+sub fail {
+ my ($self, @msg) = @_;
+ @msg = ("550 no.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# utility functions
+
+sub _getline {
+ my ($self) = @_;
+ local ($/) = "\r\n";
+ my $tmp = $self->{sock}->getline;
+ if ( defined $self->{debug} ) {
+ $self->{debug}->print($tmp) if ($tmp);
+ }
+ return $tmp;
+}
+
+sub _print {
+ my ($self, @msg) = @_;
+ $self->{debug}->print(@msg) if defined $self->{debug};
+ $self->{sock}->print(@msg);
+}
+
+1;
+
+################################################################################
+package SpamPD::Client;
+
+# Originally known as MSDW::SMTP::Client
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =head1 DESCRIPTION
+#
+# MSDW::SMTP::Client provides a very lean SMTP client implementation;
+# the only protocol-specific knowlege it has is the structure of SMTP
+# multiline responses. All specifics lie in the hands of the calling
+# program; this makes it appropriate for a semi-transparent SMTP
+# proxy, passing commands between a talker and a listener.
+#
+# =cut
+
+use strict;
+use IO::Socket;
+
+# =item new(interface => $interface, port => $port[, timeout = 300]);
+#
+# The interface and port to talk to must be specified. The interface
+# must be a valid numeric IP address; the port must be numeric. If
+# this call succeeds, it returns a client structure with an open
+# IO::Socket::INET in it, ready to talk to. If it fails it dies,
+# so if you want anything other than an exit with an explanatory
+# error message, wrap the constructor call in an eval block and pull
+# the error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die. The timeout parameter is passed
+# on into the IO::Socket::INET constructor.
+#
+# =cut
+
+sub new {
+ my ($this, @opts) = @_;
+ my $class = ref($this) || $this;
+ my $self = bless { timeout => 300, @opts }, $class;
+ $self->{sock} = IO::Socket::INET->new(
+ PeerAddr => $self->{interface},
+ PeerPort => $self->{port},
+ Timeout => $self->{timeout},
+ Proto => 'tcp',
+ Type => SOCK_STREAM,
+ );
+ die "$0: socket connect failure: $!\n" unless defined $self->{sock};
+ return $self;
+}
+
+# =item hear
+#
+# hear collects a complete SMTP response and returns it with trailing
+# CRLF removed; for multi-line responses, intermediate CRLFs are left
+# intact. Returns undef if EOF is seen before a complete reply is
+# collected.
+#
+# =cut
+
+sub hear {
+ my ($self) = @_;
+ my ($tmp, $reply);
+ return undef unless $tmp = $self->{sock}->getline;
+ while ($tmp =~ /^\d{3}-/) {
+ $reply .= $tmp;
+ return undef unless $tmp = $self->{sock}->getline;
+ }
+ $reply .= $tmp;
+ $reply =~ s/\r\n$//;
+ return $reply;
+}
+
+# =item say("command text")
+#
+# say sends an SMTP command, appending CRLF.
+#
+# =cut
+
+sub say {
+ my ($self, @msg) = @_;
+ return unless @msg;
+ $self->{sock}->print("@msg", "\r\n") or die "$0: write error: $!";
+}
+
+# =item yammer(FILEHANDLE)
+#
+# yammer takes a filehandle (which should be positioned at the
+# beginning of the file, remember to $fh->seek(0,0) if you've just
+# written it) and sends its contents as the contents of DATA. This
+# should only be invoked after a $client->say("data") and a
+# $client->hear to collect the reply to the data command. It will send
+# the trailing "." as well. It will perform leading-dot-doubling in
+# accordance with the SMTP protocol spec, where "leading dot" is
+# defined in terms of CR-LF terminated lines --- i.e. the data should
+# contain CR-LF data without the leading-dot-quoting. The filehandle
+# will be left at EOF.
+#
+# =cut
+
+sub yammer {
+ my ($self, $fh) = (@_);
+ local (*_);
+ local ($/) = "\r\n";
+ while (<$fh>) {
+ s/^\./../;
+ $self->{sock}->print($_) or die "$0: write error: $!\n";
+ }
+ $self->{sock}->print(".\r\n") or die "$0: write error: $!\n";
+}
+
+1;
+
+
+################################################################################
+package SpamPD;
+
+use strict;
+use Net::Server::PreFork;
+use IO::File;
+use Getopt::Long;
+# use Net::SMTP;
+# use Net::SMTP::Server::Client;
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::NoMailAudit;
+# use Error qw(:try);
+
+BEGIN {
+ import SpamPD::Server;
+ import SpamPD::Client;
+}
+
+use vars qw(@ISA $VERSION);
+our @ISA = qw(Net::Server::PreFork);
+our $VERSION = '2.00';
+
+sub process_message {
+ my ($self, $fh) = @_;
+
+ my $start = time;
+
+ # this gets info about the message file
+ (my $dev,my $ino,my $mode,my $nlink,my $uid,
+ my $gid,my $rdev,my $size,
+ my $atime,my $mtime,my $ctime,
+ my $blksize,my $blocks) = $fh->stat or die "Can't stat mail file: $!";
+
+ # Only process message under --maxsize KB
+ if ( $size < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ # read message into array of lines to feed to SA
+ # notes in the SA::NoMailAudit code indicate it should take a
+ # filehandle... but that doesn't seem to work
+ my(@msglines);
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ while (<$fh>) { push(@msglines,$_); }
+
+ # Audit the message
+ my $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines
+ );
+
+ # use the assassin object created during startup
+ my $assassin = $self->{spampd}->{assassin};
+
+ # Check spamminess
+ my $status = $assassin->check($mail);
+
+ # Rewrite mail if high spam factor or option --tagall
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+ $status->rewrite_mail;
+
+ # Build the new message to relay
+ my $msg_resp = join '',$mail->header,"\r\n",@{$mail->body};
+ my @resplines = split(/\r?\n/, $msg_resp);
+ my $arraycont = @resplines;
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ $fh->truncate(0) or die "Can't truncate message file: $!";
+ for (0..$arraycont) { $fh->print($resplines[$_] . "\r\n"); }
+
+ }
+
+ # Log what we did
+ my $was_it_spam = 'clean message';
+ if($status->is_spam) { $was_it_spam = 'identified spam'; }
+ my $msg_score = sprintf("%.1f",$status->get_hits);
+ my $msg_threshold = sprintf("%.1f",$status->get_required_hits);
+ $self->log(2, "$was_it_spam ($msg_score/$msg_threshold) in ".
+ sprintf("%.1f", time - $start) ." seconds.");
+
+ $status->finish();
+
+ } else {
+
+ $self->log(2, "Scanning skipped due to size (". $size / 1024 ."KB)");
+
+ }
+
+ return 1;
+
+}
+
+sub process_request {
+ my $self = shift;
+ my $msg;
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Child server process timed out!\n" };
+ my $timeout = $self->{spampd}->{childtimeout};
+
+ # start a timeout alarm
+ alarm($timeout);
+
+ # start an smtp server
+ my $smtp_server = SpamPD::Server->new($self->{server}->{client});
+ unless ( defined $smtp_server ) {
+ die "WARNING!! Failed to create listening Server: $!"; }
+
+ # start an smtp "client" (really a sending server)
+ my $client = SpamPD::Client->new(interface => $self->{spampd}->{relayhost},
+ port => $self->{spampd}->{relayport});
+ unless ( defined $client ) {
+ die "WARNING!! Failed to create sending Client: $!"; }
+
+ # pass on initial client response
+ $smtp_server->ok($client->hear)
+ or die "WARNING!! Error in initial server->ok(client->hear): $!";
+
+ # while loop over incoming data from the server
+ while ( my $what = $smtp_server->chat ) {
+
+ # until end of DATA is sent, just pass the commands on transparently
+ if ($what ne '.') {
+
+ $client->say($what)
+ or die "WARNING!! Failure in client->say(what): $!";
+
+ # but once the data is sent now we want to process it
+ } else {
+
+ # spam checking routine - message might be rewritten here
+ $self->process_message($smtp_server->{data})
+ or die "WARNING!! Error processing message (process_message(data)): $!";
+
+ # $self->log(0, $smtp_server->{data}); #debug
+
+ # need to give the client a rewound file
+ $smtp_server->{data}->seek(0,0)
+ or die "WARNING!! Can't rewind mail file: $!";
+
+ # now send the data on through the client
+ $client->yammer($smtp_server->{data})
+ or die "WARNING!! Failure in client->yammer(smtp_server->{data}): $!";
+
+ #close the file
+ $smtp_server->{data}->close
+ or die "WARNING!! Couldn't close smtp_server->{data} temp file: $!";
+
+ }
+
+ # pass on whatever the relayhost said in response
+ $smtp_server->ok($client->hear)
+ or die "WARNING!! Error in server->ok(client->hear): $!";
+
+ # restart the timeout alarm
+ alarm($timeout);
+
+ } # server ends connection
+
+ # close connections
+ $client->{sock}->close
+ or die "WARNING!! Couldn't close client->{sock}: $!";
+ $smtp_server->{sock}->close
+ or die "WARNING!! Couldn't close smtp_server->{sock}: $!";
+
+ }; # end eval block
+
+ alarm(0); # stop the timer
+ # check for error in eval block
+ if ($@ ne '') {
+ chomp($@);
+ $msg = "WARNING!! Error in process_request eval block: $@";
+ $self->log(0, $msg);
+ die ($msg . "\n");
+ }
+
+ $self->{spampd}->{instance} = 1 unless defined $self->{spampd}->{instance};
+ exit 0 if $self->{spampd}->{instance}++ > $self->{spampd}->{maxrequests};
+}
+
+my $relayhost = '127.0.0.1'; # relay to ip
+my $relayport = 25; # relay to port
+my $host = '127.0.0.1'; # listen on ip
+my $port = 10025; # listen on port
+my $maxrequests = 20; # max requests handled by child b4 dying
+my $childtimeout = 5*60; # child process per-command timeout in seconds
+my $pidfile = '/var/run/spampd.pid'; # write pid to file
+my $user = 'mail'; # user to run as
+my $group = 'mail'; # group to run as
+my $tagall = 0; # mark-up all msgs with SA, not just spam
+my $maxsize = 64; # max. msg size to scan with SA, in KB.
+
+# the following are deprecated as of v.2
+my $heloname = '';
+my $dead_letters = '';
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ relayport => \$relayport,
+ 'dead-letters' => \$dead_letters,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ heloname => \$heloname,
+ childtimeout => \$childtimeout
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'relayport=i',
+ 'maxrequests=i',
+ 'dead-letters=s',
+ 'user=s',
+ 'group=s',
+ 'pid=s',
+ 'maxsize=i',
+ 'heloname=s',
+ 'tagall',
+ 'auto-whitelist',
+ 'stop-at-threshold',
+ 'debug',
+ 'help',
+ 'local-only',
+ 'childtimeout=i');
+
+usage(0) if $options{help};
+
+if ( $options{tagall} ) { $tagall = 1; }
+
+my @tmp = split (/:/, $relayhost);
+$relayhost = $tmp[0];
+if ( $tmp[1] ) { $relayport = $tmp[1]; }
+
+@tmp = split (/:/, $host);
+$host = $tmp[0];
+if ( $tmp[1] ) { $port = $tmp[1]; }
+
+
+my $assassin = Mail::SpamAssassin->new({
+ 'dont_copy_prefs' => 1,
+ 'debug' => $options{'debug'} || 0,
+ 'local_tests_only' => $options{'local-only'} || 0 });
+
+# 'stop_at_threshold' => $options{'stop_at_threshold'} || 0,
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now();
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ },
+ spampd => { maxrequests => $maxrequests,
+ relayhost => $relayhost,
+ relayport => $relayport,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ childtimeout => $childtimeout
+ },
+ }, 'SpamPD';
+
+# call Net::Server to do the rest
+$server->run;
+
+exit 1; # shouldn't need this
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --host=host[:port] Hostname/IP and optional port to listen on.
+ Default is 127.0.0.1 port 10025
+ --port=n Port to listen on (alternate syntax to above).
+ --relayhost=host[:port] Host to relay mail to.
+ Default is 127.0.0.1 port 25.
+ --relayport=n Port to relay to (alternate syntax to above).
+
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Default is 20.
+ --childtimeout=n Time out children after this many seconds during
+ transactions (each S/LMTP command including the
+ time it takes to send the data).
+ Default is 300 seconds (5min).
+
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --tagall Tag all messages with a header, not just spam.
+
+ --auto-whitelist Use the SA global auto-whitelist feature.
+ --local-only Turn off all SA network-based tests (RBL, Razor, etc).
+ --debug Turn on SA debugging (sent to STDERR).
+
+ --help This message
+
+Deprecated Options (still accepted for backwards compatibility):
+ --heloname=hostname No longer used in spampd v.2
+ --dead-letters=path No longer used in spampd v.2
+ --stop-at-threshold No longer implemented in SpamAssassin
+EOF
+ exit shift;
+}
+
+__END__
+
+=pod
+
+=head1 Name
+
+spampd - Spam Proxy Daemon (version 2)
+
+=head1 Synopsis
+
+B<spampd>
+[B<--host=host[:port]>]
+[B<--relayhost=hostname[:port]>]
+[B<--user=username>]
+[B<--group=groupname>]
+[B<--maxrequests=n>]
+[B<--childtimeout=n>]
+[B<--pid=filename>]
+[B<--maxsize=n>]
+[B<--tagall>]
+[B<--auto-whitelist>]
+[B<--local-only>]
+[B<--debug>]
+
+B<spampd> B<--help>
+
+=head1 Description
+
+I<spampd> is a relaying SMTP proxy that filters spam using
+SpamAssassin (http://www.SpamAssassin.org). The proxy is designed
+to be robust in the face of exceptional errors, and will (hopefully)
+never lose a message.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+The latest version can be found at
+http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm
+
+=head1 Requires
+
+=over 5
+
+Perl modules:
+
+=item B<Mail::SpamAssassin>
+
+=item B<Net::Server::PreFork>
+
+=item B<IO::File>
+
+=item B<IO::Socket>
+
+=back
+
+=head1 Operation
+
+I<spampd> is meant to operate as an SMTP mail proxy which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing! It is very easy to set up an open relay this
+way.
+
+Note that I<spampd> U<replaces> I<spamd> from the I<SpamAssassin> distribution
+in function. You do not need to run I<spamd> in order for I<spampd> to function.
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+
+B<Running between firewall/gateway and internal mail server>
+
+=over 3
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+Internet -> [ MX gateway (@inter.net.host:25) ->
+ I<spampd> (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+=back
+
+B<Using Postfix advanced content filtering>
+
+=over 3
+
+Please see the FILTER_README that came with the Postfix distribution. You
+need to have a version of Postfix which supports this.
+
+Internet -> [ I<Postfix> (@inter.net.host:25) ->
+ I<spampd> (@localhost:10025) ->
+ I<Postfix> (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+
+=head1 Installation
+
+I<spampd> can be run directly from the command prompt if desired. This is
+useful for testing purposes, but for long term use you probably want to put
+it somewhere like /usr/bin or /usr/local/bin and execute it at system startup.
+For example on Red Hat-style Linux system one can use a script in
+/etc/rc.d/init.d to start I<spampd> (a sample script is available on the
+I<spampd> Web page @ http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm).
+
+Note that I<spampd> B<replaces> I<spamd> from the I<SpamAssassin> distribution
+in function. You do not need to run I<spamd> in order for I<spampd> to function.
+This has apparently been the source of some confusion, so now you know.
+
+=head2 Postfix-specific Notes
+
+Here is a typical setup for Postfix "advanced" content filtering as described
+in the FILTER_README that came with the Postfix distribution:
+
+ F</etc/postfix/master.cf>:
+
+ smtp inet n - y - - smtpd
+ -o content_filter=smtp:localhost:10025
+ -o myhostname=mx.example.com
+
+ localhost:10026 inet n - n - 10 smtpd
+ -o content_filter=
+ -o myhostname=mx-int.example.com
+
+The first entry is the main public-facing MTA which uses localhost:10025
+as the content filter for all mail. The second entry receives mail from
+the content filter and does final delivery. Both smtpd instances use
+the same Postfix F<main.cf> file. I<spampd> is the process that listens on
+localhost:10025 and then connects to the Postfix listener on localhost:10026.
+Note that the C<myhostname> options must be different between the two instances,
+otherwise Postfix will think it's talking to itself and abort sending.
+
+For the above example you can simply start I<spampd> like this:
+
+ spampd --host=localhost:10025 --relayhost=localhost:10026
+
+=head1 Options
+
+=over 5
+
+=item B<--host=ip or hostname[:port]>
+
+Specifies what hostname/IP and port I<spampd> listens on. By default, it listens
+on 127.0.0.1 (localhost) on port 10025.
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025. This is an alternate to using the above --host=ip:port notation.
+
+=item B<--relayhost=ip or hostname[:port]>
+
+Specifies the hostname where I<spampd> will relay all
+messages. Defaults to 127.0.0.1. If the port is not provided, that
+defaults to 25.
+
+=item B<--relayport=n>
+
+Specifies what port I<spampd> will relay to. Default is 35. This is an
+alternate to using the above --relayhost=ip:port notation.
+
+=item B<--user=username>
+
+=item B<--group=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--childtimeout=n>
+
+This is the number of seconds to allow each child server before it times out
+a transaction. In an SMTP transaction the timer is reset for every command. This
+timeout includes time it would take to send the message data, so it should not
+be too short. Default is 300 seconds (5 minutes).
+
+=item B<--pid=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--tagall>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings).
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KB. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. This includes headers.
+
+=item B<--auto-whitelist>
+
+Turns on the SpamAssassin global whitelist feature. See the SA docs. Note
+that per-user whitelists are not available.
+
+=item B<--local-only>
+
+Turn off all SA network-based tests (DNS, Razor, etc).
+
+=item B<--debug>
+
+Turns on SpamAssassin debug messages.
+
+=item B<--help>
+
+Prints usage information.
+
+=back
+
+=head2 Deprecated Options
+
+=over 5
+
+The following options are no longer used but still accepted for backwards
+compatibility with I<spampd> v1:
+
+=item B<--dead-letters>
+
+=item B<--heloname>
+
+=item B<--stop-at-threshold>
+
+=back
+
+=head1 Examples
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 Credits
+
+I<spampd> is written and maintained by Maxim Paperno <MPaperno@WorldDesign.com>.
+See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info.
+
+I<spampd> v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+Stanley Dean Witter. These are distributed under the GNU GPL (see
+module code for more details). Both modules have been slightly modified
+from the originals and are included in this file under new names.
+
+Also thanks to Bennet Todd for the example smtpproxy script which helped create
+this version of I<spampd>. See http://bent.latency.net/smtpprox/ .
+
+I<spampd> v1 was based on code by Dave Carrigan named assassind. Trace amounts
+of his code or documentation may still remain. Thanks to him for the
+original inspiration and code. See http://www.rudedog.org/assassind/ .
+
+Also thanks to I<spamd> (included with SpamAssassin) and
+I<amavisd-new> (http://www.ijs.si/software/amavisd/) for some tricks.
+
+=head1 Copyright and Disclaimer
+
+I<spampd> is Copyright (c) 2002 by World Design Group and Maxim Paperno
+
+Portions are Copyright (C) 2001 Morgan Stanley Dean Witter as mentioned above
+in the CREDITS section.
+
+ 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.
+
+ The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+
+
+=head1 Bugs
+
+None known. Please report any to MPaperno@WorldDesign.com.
+
+=head1 To Do
+
+Add configurable option for rejecting mail outright based on spam score.
+It would be nice to make this program safe enough to sit in front of a mail
+server such as Postfix and be able to reject mail before it enters our systems.
+The only real problem is that Postfix will see localhost as the connecting
+client, so that disables any client-based checks Postfix can do and creates a
+possible relay hole if localhost is trusted.
+
+Make it handle LMTP protocol.
+
+=head1 See Also
+
+perl(1), Spam::Assassin(3), L<http://www.spamassassin.org/>,
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>
diff --git a/previous-versions/spampd-2.10.pl b/previous-versions/spampd-2.10.pl
new file mode 100644
index 0000000..90e056b
--- /dev/null
+++ b/previous-versions/spampd-2.10.pl
@@ -0,0 +1,1392 @@
+#! /usr/bin/perl -T
+
+######################
+# SpamPD - spam proxy daemon
+#
+# v2.10 - 01-Jul-03
+# v2.00 - 10-Jun-03
+# v1.0.2 - 13-Apr-03
+# v1.0.1 - 03-Feb-03
+# v1.0.0 - May 2002
+#
+# spampd is Copyright (c) 2002 by World Design Group and Maxim Paperno
+# (see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# Written and maintained by Maxim Paperno (MPaperno@WorldDesign.com)
+#
+# 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.
+#
+# The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+#
+# spampd v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+# Stanley Dean Witter. These are also distributed under the GNU GPL (see
+# module code for more details). Both modules have been slightly modified
+# from the originals and are included in this file under new names.
+#
+# spampd v1 was based on code by Dave Carrigan named assassind. Trace amounts
+# of his code or documentation may still remain. Thanks to him for the
+# original inspiration and code. (see http://www.rudedog.org/assassind/)
+#
+######################
+
+
+################################################################################
+package SpamPD::Server;
+
+# Originally known as MSDW::SMTP::Server
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =item DESCRIPTION
+#
+# This server simply gathers the SMTP acquired information (envelope
+# sender and recipient, and data) into unparsed memory buffers (or a
+# file for the data), and returns control to the caller to explicitly
+# acknowlege each command or request. Since acknowlegement or failure
+# are driven explicitly from the caller, this module can be used to
+# create a robust SMTP content scanning proxy, transparent or not as
+# desired.
+#
+# =cut
+
+use strict;
+use IO::File;
+#use IO::Socket;
+
+# =item new(interface => $interface, port => $port);
+
+# The #interface and port to listen on must be specified. The interface
+# must be a valid numeric IP address (0.0.0.0 to listen on all
+# interfaces, as usual); the port must be numeric. If this call
+# succeeds, it returns a server structure with an open
+# IO::Socket::INET in it, ready to listen on. If it fails it dies, so
+# if you want anything other than an exit with an explanatory error
+# message, wrap the constructor call in an eval block and pull the
+# error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die.
+#
+# =cut
+
+sub new {
+
+# This now emulates Net::SMTP::Server::Client for use with Net::Server which
+# passes an already open socket.
+
+ my($this, $socket) = @_;
+
+ my $class = ref($this) || $this;
+ my $self = {};
+ $self->{sock} = $socket;
+
+ bless($self, $class);
+
+ die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+ $self->{state} = 'started';
+ return $self;
+
+# Original code, removed by MP for spampd use
+#
+# my ($this, @opts) = @_;
+# my $class = ref($this) || $this;
+# my $self = bless { @opts }, $class;
+# $self->{sock} = IO::Socket::INET->new(
+# LocalAddr => $self->{interface},
+# LocalPort => $self->{port},
+# Proto => 'tcp',
+# Type => SOCK_STREAM,
+# Listen => 65536,
+# Reuse => 1,
+# );
+# die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+# $self->{state} = 'just bound',
+# return $self;
+
+}
+
+# sub accept { }
+#
+# Removed by MP; not needed for spampd use
+#
+
+
+# =item chat;
+#
+# The chat method carries the SMTP dialogue up to the point where any
+# acknowlegement must be made. If chat returns true, then its return
+# value is the previous SMTP command. If the return value begins with
+# 'mail' (case insensitive), then the attribute 'from' has been filled
+# in, and may be checked; if the return value begins with 'rcpt' then
+# both from and to have been been filled in with scalars, and should
+# be checked, then either 'ok' or 'fail' should be called to accept
+# or reject the given sender/recipient pair. If the return value is
+# 'data', then the attributes from and to are populated; in this case,
+# the 'to' attribute is a reference to an anonymous array containing
+# all the recipients for this data. If the return value is '.', then
+# the 'data' attribute (which may be pre-populated in the "new" or
+# "accept" methods if desired) is a reference to a filehandle; if it's
+# created automatically by this module it will point to an unlinked
+# tmp file in /tmp. If chat returns false, the SMTP dialogue has been
+# completed and the socket closed; this server is ready to exit or to
+# accept again, as appropriate for the server style.
+#
+# The return value from chat is also remembered inside the server
+# structure in the "state" attribute.
+#
+# =cut
+
+sub chat {
+ my ($self) = @_;
+ local(*_);
+ if ($self->{state} !~ /^data/i) {
+ return 0 unless defined($_ = $self->_getline);
+ s/[\r\n]*$//;
+ $self->{state} = $_;
+ if (s/^.?he?lo\s+//i) { # mp: find helo|ehlo|lhlo
+ # mp: determine protocol (for future use)
+ if ( /^L/i ) {
+ $self->{proto} = "lmtp";
+ } elsif ( /^E/i ) {
+ $self->{proto} = "esmtp";
+ } else {
+ $self->{proto} = "smtp"; }
+ s/\s*$//;
+ s/\s+/ /g;
+ $self->{helo} = $_;
+ } elsif (s/^rset\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ } elsif (s/^mail\s+from:\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ s/\s*$//;
+ $self->{from} = $_;
+ } elsif (s/^rcpt\s+to:\s*//i) {
+ s/\s*$//; s/\s+/ /g;
+ $self->{to} = $_;
+ push @{$self->{recipients}}, $_;
+ } elsif (/^data/i) {
+ $self->{to} = $self->{recipients};
+ }
+ } else {
+ if (defined($self->{data})) {
+ $self->{data}->seek(0, 0);
+ $self->{data}->truncate(0);
+ } else {
+ $self->{data} = IO::File->new_tmpfile;
+ }
+ while (defined($_ = $self->_getline)) {
+ if ($_ eq ".\r\n") {
+ $self->{data}->seek(0,0);
+ return $self->{state} = '.';
+ }
+ s/^\.\./\./;
+ $self->{data}->print($_) or die "$0: write error saving data\n";
+ }
+ return(0);
+ }
+ return $self->{state};
+}
+
+# =item ok([message]);
+#
+# Approves of the data given to date, either the recipient or the
+# data, in the context of the sender [and, for data, recipients]
+# already given and available as attributes. If a message is given, it
+# will be sent instead of the internal default.
+#
+# =cut
+
+sub ok {
+ my ($self, @msg) = @_;
+ @msg = ("250 ok.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# =item fail([message]);
+#
+# Rejects the current info; if processing from, rejects the sender; if
+# processing 'to', rejects the current recipient; if processing data,
+# rejects the entire message. If a message is specified it means the
+# exact same thing as "ok" --- simply send that message to the sender.
+#
+# =cut
+
+sub fail {
+ my ($self, @msg) = @_;
+ @msg = ("550 no.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# utility functions
+
+sub _getline {
+ my ($self) = @_;
+ local ($/) = "\r\n";
+ my $tmp = $self->{sock}->getline;
+ if ( defined $self->{debug} ) {
+ $self->{debug}->print($tmp) if ($tmp);
+ }
+ return $tmp;
+}
+
+sub _print {
+ my ($self, @msg) = @_;
+ $self->{debug}->print(@msg) if defined $self->{debug};
+ $self->{sock}->print(@msg);
+}
+
+1;
+
+################################################################################
+package SpamPD::Client;
+
+# Originally known as MSDW::SMTP::Client
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =head1 DESCRIPTION
+#
+# MSDW::SMTP::Client provides a very lean SMTP client implementation;
+# the only protocol-specific knowlege it has is the structure of SMTP
+# multiline responses. All specifics lie in the hands of the calling
+# program; this makes it appropriate for a semi-transparent SMTP
+# proxy, passing commands between a talker and a listener.
+#
+# =cut
+
+use strict;
+use IO::Socket;
+
+# =item new(interface => $interface, port => $port[, timeout = 300]);
+#
+# The interface and port to talk to must be specified. The interface
+# must be a valid numeric IP address; the port must be numeric. If
+# this call succeeds, it returns a client structure with an open
+# IO::Socket::INET in it, ready to talk to. If it fails it dies,
+# so if you want anything other than an exit with an explanatory
+# error message, wrap the constructor call in an eval block and pull
+# the error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die. The timeout parameter is passed
+# on into the IO::Socket::INET constructor.
+#
+# =cut
+
+sub new {
+ my ($this, @opts) = @_;
+ my $class = ref($this) || $this;
+ my $self = bless { timeout => 300, @opts }, $class;
+ $self->{sock} = IO::Socket::INET->new(
+ PeerAddr => $self->{interface},
+ PeerPort => $self->{port},
+ Timeout => $self->{timeout},
+ Proto => 'tcp',
+ Type => SOCK_STREAM,
+ );
+ die "$0: socket connect failure: $!\n" unless defined $self->{sock};
+ return $self;
+}
+
+# =item hear
+#
+# hear collects a complete SMTP response and returns it with trailing
+# CRLF removed; for multi-line responses, intermediate CRLFs are left
+# intact. Returns undef if EOF is seen before a complete reply is
+# collected.
+#
+# =cut
+
+sub hear {
+ my ($self) = @_;
+ my ($tmp, $reply);
+ return undef unless $tmp = $self->{sock}->getline;
+ while ($tmp =~ /^\d{3}-/) {
+ $reply .= $tmp;
+ return undef unless $tmp = $self->{sock}->getline;
+ }
+ $reply .= $tmp;
+ $reply =~ s/\r\n$//;
+ return $reply;
+}
+
+# =item say("command text")
+#
+# say sends an SMTP command, appending CRLF.
+#
+# =cut
+
+sub say {
+ my ($self, @msg) = @_;
+ return unless @msg;
+ $self->{sock}->print("@msg", "\r\n") or die "$0: write error: $!";
+}
+
+# =item yammer(FILEHANDLE)
+#
+# yammer takes a filehandle (which should be positioned at the
+# beginning of the file, remember to $fh->seek(0,0) if you've just
+# written it) and sends its contents as the contents of DATA. This
+# should only be invoked after a $client->say("data") and a
+# $client->hear to collect the reply to the data command. It will send
+# the trailing "." as well. It will perform leading-dot-doubling in
+# accordance with the SMTP protocol spec, where "leading dot" is
+# defined in terms of CR-LF terminated lines --- i.e. the data should
+# contain CR-LF data without the leading-dot-quoting. The filehandle
+# will be left at EOF.
+#
+# =cut
+
+sub yammer {
+ my ($self, $fh) = (@_);
+ local (*_);
+ local ($/) = "\r\n";
+ while (<$fh>) {
+ s/^\./../;
+ $self->{sock}->print($_) or die "$0: write error: $!\n";
+ }
+ $self->{sock}->print(".\r\n") or die "$0: write error: $!\n";
+}
+
+1;
+
+
+################################################################################
+package SpamPD;
+
+use strict;
+use Net::Server::PreForkSimple;
+use IO::File;
+use Getopt::Long;
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::NoMailAudit;
+
+BEGIN {
+ # Load Time::HiRes if it's available
+ eval { require Time::HiRes };
+ Time::HiRes->import( qw(time) ) unless $@;
+
+ # use included modules
+ import SpamPD::Server;
+ import SpamPD::Client;
+}
+
+
+use vars qw(@ISA $VERSION);
+our @ISA = qw(Net::Server::PreForkSimple);
+our $VERSION = '2.00';
+
+sub process_message {
+ my ($self, $fh) = @_;
+
+ # output lists with a , delimeter by default
+ local ($") = ",";
+
+ # start a timer
+ my $start = time;
+ # use the assassin object created during startup
+ my $assassin = $self->{spampd}->{assassin};
+
+ # this gets info about the message temp file
+ (my $dev,my $ino,my $mode,my $nlink,my $uid,
+ my $gid,my $rdev,my $size,
+ my $atime,my $mtime,my $ctime,
+ my $blksize,my $blocks) = $fh->stat or die "Can't stat mail file: $!";
+
+ # Only process message under --maxsize KB
+ if ( $size < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ # read message into array of lines to feed to SA
+ # notes in the SA::NoMailAudit code indicate it should take a
+ # filehandle... but that doesn't seem to work
+ my (@msglines, $msgid, $tmp);
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+
+ # loop over headers first (we may want info from them)
+ while (<$fh>) {
+
+ # if last line
+# if (/^\r?\n$/) {
+# if ( $self->{spampd}->{addheader} && length($self->{spampd}->{myhostname}) ) {
+# $tmp = "X-Spam-Scanned-By: $self->{spampd}->{myhostname}\n";
+# push(@msglines, $tmp);
+# }
+# }
+ push(@msglines, $_);
+
+ # find the Message-ID for logging (code is from spamd)
+ if (/^Message-Id:\s+(.*?)\s*$/i) {
+ $msgid = $1;
+ while($msgid =~ s/\([^\(\)]*\)//) {}; # remove comments and
+ $msgid =~ s/^\s+|\s+$//g; # leading and trailing spaces
+ $msgid =~ s/\s.*$//; # keep only the first token
+ }
+
+ last if (/^\r?\n$/);
+ }
+
+ # finish loop over rest of body
+ while (<$fh>) { push(@msglines, $_); }
+
+ # my @resplines = @msglines;
+
+ my $recips = "@{$self->{smtp_server}->{to}}";
+ $msgid ||= "(unknown)";
+ $recips ||= "(unknown)";
+
+ $self->log(2, "processing message $msgid for ". $recips);
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Timed out!\n" };
+ # save previous timer and start new
+ my $previous_alarm = alarm($self->{spampd}->{satimeout});
+
+ # Audit the message
+ my $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines );
+
+ # Check spamminess
+ my $status = $assassin->check($mail);
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Returned from checking by SpamAssassin"); }
+
+ if ( $self->{spampd}->{addheader} && length($self->{spampd}->{myhostname}) ) {
+ $mail->put_header("X-Spam-Checked-By", $self->{spampd}->{myhostname});
+ }
+
+ # Rewrite mail if high spam factor or options --tagall or --add-sc-header
+ if ( $status->is_spam || $self->{spampd}->{tagall} || $self->{spampd}->{addheader} ) {
+
+ # if spam or --tagall, rewrite using SA
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Rewriting mail using SpamAssassin"); }
+
+ $status->rewrite_mail;
+
+ }
+
+ my $msg_resp = join '',$mail->header,"\r\n",@{$mail->body};
+ my @resplines = split(/\r?\n/, $msg_resp);
+
+ # Build the new message to relay
+ # pause the timeout alarm while we do this (no point in timing
+ # out here and leaving a half-written file).
+ my $pause_alarm = alarm(0);
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ $fh->truncate(0) or die "Can't truncate message file: $!";
+ for (@resplines) {
+ $fh->print($_ . "\r\n")
+ or die "Can't print to message file: $!";
+ }
+ #restart the alarm
+ alarm($pause_alarm);
+
+ }
+
+ # Log what we did
+ my $was_it_spam = 'clean message';
+ if ($status->is_spam) { $was_it_spam = 'identified spam'; }
+ my $msg_score = sprintf("%.2f",$status->get_hits);
+ my $msg_threshold = sprintf("%.2f",$status->get_required_hits);
+ my $proc_time = sprintf("%.2f", time - $start);
+
+ $self->log(2, "$was_it_spam $msgid ($msg_score/$msg_threshold) for ".
+ "$recips in $proc_time seconds, $size bytes.");
+
+ # thanks to Kurt Andersen for this idea
+ if ( $self->{spampd}->{rh} ) {
+ $self->log(2, "rules hit for $msgid: " . $status->get_names_of_tests_hit); }
+
+ $status->finish();
+
+ # set the timeout alarm back to wherever it was at
+ alarm($previous_alarm);
+
+ };
+
+ if ( $@ ne '' ) {
+ $self->log(1, "WARNING!! SpamAssassin error on message $msgid: $@");
+ return 0;
+ }
+
+ } else {
+
+ $self->log(2, "skipped large message (". $size / 1024 ."KB)");
+
+ }
+
+ return 1;
+
+}
+
+sub process_request {
+ my $self = shift;
+ my $msg;
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Child server process timed out!\n" };
+ my $timeout = $self->{spampd}->{childtimeout};
+
+ # start a timeout alarm
+ alarm($timeout);
+
+ # start an smtp server
+ my $smtp_server = SpamPD::Server->new($self->{server}->{client});
+ unless ( defined $smtp_server ) {
+ die "Failed to create listening Server: $!"; }
+
+ $self->{smtp_server} = $smtp_server;
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Server"); }
+
+ # start an smtp "client" (really a sending server)
+ my $client = SpamPD::Client->new(interface => $self->{spampd}->{relayhost},
+ port => $self->{spampd}->{relayport});
+ unless ( defined $client ) {
+ die "Failed to create sending Client: $!"; }
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Client"); }
+
+ # pass on initial client response
+ # $client->hear can handle multiline responses so no need to loop
+ $smtp_server->ok($client->hear)
+ or die "Error in initial server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # while loop over incoming data from the server
+ while ( my $what = $smtp_server->chat ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # until end of DATA is sent, just pass the commands on transparently
+ if ($what ne '.') {
+
+ $client->say($what)
+ or die "Failure in client->say(what): $!";
+
+ # but once the data is sent now we want to process it
+ } else {
+
+ # spam checking routine - message might be rewritten here
+ my $pmrescode = $self->process_message($smtp_server->{data});
+
+ # pass on the messsage if exit code <> 0 or die-on-sa-errors flag is off
+ if ( $pmrescode or !$self->{spampd}->{dose} ) {
+
+ # need to give the client a rewound file
+ $smtp_server->{data}->seek(0,0)
+ or die "Can't rewind mail file: $!";
+
+ # now send the data on through the client
+ $client->yammer($smtp_server->{data})
+ or die "Failure in client->yammer(smtp_server->{data}): $!";
+
+ } else {
+
+ $smtp_server->ok("450 Temporary failure processing message, please try again later");
+ last;
+ }
+
+ #close the temp file
+ $smtp_server->{data}->close
+ or $self->log(1, "WARNING!! Couldn't close smtp_server->{data} temp file: $!");
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Finished sending DATA"); }
+ }
+
+ # pass on whatever the relayhost said in response
+ # $client->hear can handle multiline responses so no need to loop
+ my $destresp = $client->hear;
+ $smtp_server->ok($destresp)
+ or die "Error in server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Destination response: '" . $destresp . "'"); }
+
+ # restart the timeout alarm
+ alarm($timeout);
+
+ } # server ends connection
+
+ # close connections
+ $client->{sock}->close
+ or die "Couldn't close client->{sock}: $!";
+ $smtp_server->{sock}->close
+ or die "Couldn't close smtp_server->{sock}: $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Closed connections"); }
+
+ }; # end eval block
+
+ alarm(0); # stop the timer
+ # check for error in eval block
+ if ($@ ne '') {
+ chomp($@);
+ $msg = "WARNING!! Error in process_request eval block: $@";
+ $self->log(0, $msg);
+ die ($msg . "\n");
+ }
+
+ $self->{spampd}->{instance}++;
+
+# if ( $self->{spampd}->{instance}++ > $self->{spampd}->{maxrequests} ) {
+
+# if ( $self->{spampd}->{debug} ) {
+# $self->log(2, "Exiting child process after handling ".
+# $self->{spampd}->{maxrequests} ." requests"); }
+
+# exit 0;
+
+# };
+
+}
+
+# Net::Server hook
+# about to exit child process
+sub child_finish_hook {
+ my($self) = shift;
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Exiting child process after handling ".
+ $self->{spampd}->{instance} ." requests"); }
+}
+
+
+my $relayhost = '127.0.0.1'; # relay to ip
+my $relayport = 25; # relay to port
+my $host = '127.0.0.1'; # listen on ip
+my $port = 10025; # listen on port
+my $children = 5; # number of child processes (servers) to spawn at start
+# my $maxchildren = $children; # max. number of child processes (servers) to spawn
+my $maxrequests = 20; # max requests handled by child b4 dying
+my $childtimeout = 6*60; # child process per-command timeout in seconds
+my $satimeout = 285; # SpamAssassin timeout in seconds (15s less than Postfix
+ # default for smtp_data_done_timeout)
+my $pidfile = '/var/run/spampd.pid'; # write pid to file
+my $user = 'mail'; # user to run as
+my $group = 'mail'; # group to run as
+my $tagall = 0; # mark-up all msgs with SA, not just spam
+my $maxsize = 64; # max. msg size to scan with SA, in KB.
+my $rh = 0; # log which rules were hit
+my $debug = 0; # debug flag
+my $dose = 0; # die-on-sa-errors flag
+my $addheader = 0; # add X-Spam-Checked-By header to all messages
+# hostname to use in X-Spam-Checked-By header:
+my $myhostname = ( length($ENV{HOSTNAME}) ) ? $ENV{HOSTNAME} : "localhost";
+
+# the following are deprecated as of v.2
+my $heloname = '';
+my $dead_letters = '';
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ relayport => \$relayport,
+ 'dead-letters' => \$dead_letters,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ heloname => \$heloname,
+ childtimeout => \$childtimeout,
+ satimeout => \$satimeout,
+ children => \$children,
+ # maxchildren => \$maxchildren,
+ hostname => \$myhostname
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'relayport=i',
+ 'children|c=i',
+ # 'maxchildren|mc=i',
+ 'maxrequests|mr=i',
+ 'childtimeout=i',
+ 'satimeout=i',
+ 'dead-letters=s',
+ 'user|u=s',
+ 'group|g=s',
+ 'pid|p=s',
+ 'maxsize=i',
+ 'heloname=s',
+ 'tagall|a',
+ 'auto-whitelist|aw',
+ 'stop-at-threshold',
+ 'debug|d',
+ 'help|h',
+ 'local-only|l',
+ 'log-rules-hit|rh',
+ 'dose',
+ 'add-sc-header|ash',
+ 'hostname=s'
+ );
+
+usage(0) if $options{help};
+
+if ( $options{tagall} ) { $tagall = 1; }
+if ( $options{'log-rules-hit'} ) { $rh = 1; }
+if ( $options{debug} ) { $debug = 1; }
+if ( $options{dose} ) { $dose = 1; }
+if ( $options{'add-sc-header'} ) { $addheader = 1; }
+# if ( !$options{maxchildren} or $maxchildren < $children ) { $maxchildren = $children; }
+
+if ( $children < 1 ) { print "Option --children must be greater than zero!\n"; exit shift; }
+
+# my $min_spare_servers = ($children == $maxchildren) ? 0 : 1;
+# my $max_spare_servers = ($min_spare_servers == 0) ? 0 : $maxchildren-1;
+
+my @tmp = split (/:/, $relayhost);
+$relayhost = $tmp[0];
+if ( $tmp[1] ) { $relayport = $tmp[1]; }
+
+@tmp = split (/:/, $host);
+$host = $tmp[0];
+if ( $tmp[1] ) { $port = $tmp[1]; }
+
+my $assassin = Mail::SpamAssassin->new({
+ 'dont_copy_prefs' => 1,
+ 'debug' => $debug,
+ 'local_tests_only' => $options{'local-only'} || 0 });
+
+# 'stop_at_threshold' => $options{'stop_at_threshold'} || 0,
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now();
+
+# thanks to Kurt Andersen for this fix for HPUX
+my $logsock = "unix";
+eval {
+ if (`uname -s` =~ 'HP-UX') { $logsock = "inet"; }
+};
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ syslog_logsock => $logsock,
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => 1,
+ # setsid => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ max_servers => $children,
+ max_requests => $maxrequests,
+ # min_servers => $children,
+ # max_servers => $maxchildren,
+ # min_spare_servers => $min_spare_servers,
+ # max_spare_servers => $max_spare_servers,
+ },
+ spampd => { relayhost => $relayhost,
+ relayport => $relayport,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ childtimeout => $childtimeout,
+ satimeout => $satimeout,
+ rh => $rh,
+ debug => $debug,
+ dose => $dose,
+ addheader => $addheader,
+ myhostname => $myhostname,
+ instance => 0,
+ },
+ }, 'SpamPD';
+
+# call Net::Server to start up the daemon inside
+$server->run;
+
+exit 1; # shouldn't get here
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --host=host[:port] Hostname/IP and optional port to listen on.
+ Default is 127.0.0.1 port 10025
+ --port=n Port to listen on (alternate syntax to above).
+ --relayhost=host[:port] Host to relay mail to.
+ Default is 127.0.0.1 port 25.
+ --relayport=n Port to relay to (alternate syntax to above).
+
+ --children=n Number of child processes (servers) to start and
+ keep running. Default is 5 (plus 1 parent proc).
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Default is 20.
+ --childtimeout=n Time out children after this many seconds during
+ transactions (each S/LMTP command including the
+ time it takes to send the data).
+ Default is 360 seconds (6min).
+ --satimeout=n Time out SpamAssassin after this many seconds.
+ Default is 285 seconds.
+
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --dose (d)ie (o)n (s)pamAssassin (e)rrors. If this is
+ specified and SA times out or throws an error,
+ the mail will be rejected with a 450 temporary
+ error message. Default is to pass through email
+ even in the event of an SA problem.
+ --tagall Tag all messages with SA headers, not just spam.
+ --log-rules-hit or --rh Log the name of each SA test which matched the
+ current message.
+ --add-sc-header or --ash Add a 'X-Spam-Checked-By: {hostname}' header to each
+ scanned message. By default no header is added.
+ --hostname=hostname Hostname to use in the X-Spam-Checked-By header.
+ By default the value of the environmen variable
+ HOSTNAME is used, or if undefined/blank then
+ 'localhost' is used as the hostname.
+
+ --auto-whitelist or --aw Use the SA global auto-whitelist feature.
+ --local-only or --L Turn off all SA network-based tests (RBL, Razor, etc).
+ --debug or --d Turn on SA debugging (sent to STDERR).
+
+ --help or --h This message
+
+Deprecated Options (still accepted for backwards compatibility):
+ --heloname=hostname No longer used in spampd v.2
+ --dead-letters=path No longer used in spampd v.2
+ --stop-at-threshold No longer implemented in SpamAssassin
+EOF
+
+# --maxchildren=n Maximum number of child processes (servers) to
+# run. Default is the value of --children.
+
+ exit shift;
+}
+
+__END__
+
+# Some commented-out documentation. POD doesn't have a way to comment
+# out sections!? This documents a feature which may be implemented later.
+#
+# =item B<--maxchildren=n> or B<--mc=n> C<(new in v2)>
+#
+# Maximum number of children to spawn if needed (where n >= --children). When
+# I<spampd> starts it will spawn a number of child servers as specified by
+# --children. If all those servers become busy, a new child is spawned up to the
+# number specified in --maxchildren. Default is to have --maxchildren equal to
+# --children so extra child processes aren't started. Also see the --children
+# option, above. You may want to set your origination mail server to limit the
+# number of concurrent connections to I<spampd> to match this setting (for
+# Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+# 'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+#
+# Note that extra servers after the initial --children will only spawn on very
+# busy systems. This is because the check to see if a new server is needed (ie.
+# all current ones are busy) is only done around once per minute (this is
+# controlled by the Net::Server::PreFork module, in case you want to
+# hack at it :). It can still be useful as an "overflow valve," and is
+# especially nice since the extra child servers will die off once they're not
+# needed.
+
+=pod
+
+=head1 NAME
+
+SpamPD - Spam Proxy Daemon (version 2.10)
+
+=head1 Synopsis
+
+B<spampd>
+[B<--host=host[:port]>]
+[B<--relayhost=hostname[:port]>]
+[B<--user|u=username>]
+[B<--group|g=groupname>]
+[B<--children|c=n>]
+#[B<--maxchildren|mc=n>]
+[B<--maxrequests=n>]
+[B<--childtimeout=n>]
+[B<--satimeout=n>]
+[B<--pid|p=filename>]
+[B<--maxsize=n>]
+[B<--dose>]
+[B<--tagall|a>]
+[B<--log-rules-hit|rh>]
+[B<--auto-whitelist|aw>]
+[B<--local-only|L>]
+[B<--debug|d>]
+
+B<spampd> B<--help>
+
+=head1 Description
+
+I<spampd> is an SMTP/LMTP proxy that marks (or tags) spam using
+SpamAssassin (http://www.SpamAssassin.org/). The proxy is designed
+to be transparent to the sending and receiving mail servers and at no point
+takes responsibility for the message itself. If a failure occurs within
+I<spampd> (or SpamAssassin) then the mail servers will disconnect and the
+sending server is still responsible for retrying the message for as long
+as it is configured to do so.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+The latest version can be found at
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>.
+
+=head1 Requires
+
+=over 5
+
+Perl modules:
+
+=item B<Mail::SpamAssassin>
+
+=item B<Net::Server::PreForkSimple>
+
+=item B<IO::File>
+
+=item B<IO::Socket>
+
+=item B<Time::HiRes> (not actually required but recommended)
+
+=back
+
+=head1 Operation
+
+I<spampd> is meant to operate as an S/LMTP mail proxy which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing! It is very easy to set up an open relay this
+way.
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+
+B<Running between firewall/gateway and internal mail server>
+
+=over 3
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+ Internet -> [ MX gateway (@inter.net.host:25) ->
+ spampd (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+=back
+
+B<Using Postfix advanced content filtering>
+
+=over 3
+
+Please see the F<FILTER_README> that came with the Postfix distribution. You
+need to have a version of Postfix which supports this (ideally v.2 and up).
+
+ Internet -> [ Postfix (@inter.net.host:25) ->
+ spampd (@localhost:10025) ->
+ Postfix (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+=head1 Upgrading
+
+Upgrading from version 1 simply involves replacing the F<spampd> program file
+with the latest one. Note that the I<dead-letters> folder is no longer being
+used and the --dead-letters option is no longer needed (though no errors are
+thrown if it's present). Check the L<"Options"> list below for a full list of new
+and deprecated options. Also be sure to check out the change log.
+
+=head1 Installation
+
+I<spampd> can be run directly from the command prompt if desired. This is
+useful for testing purposes, but for long term use you probably want to put
+it somewhere like /usr/bin or /usr/local/bin and execute it at system startup.
+For example on Red Hat-style Linux system one can use a script in
+/etc/rc.d/init.d to start I<spampd> (a sample script is available on the
+I<spampd> Web page @ http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm).
+
+The options all have reasonable defaults, especially for a Postfix-centric
+installation. You may want to specify the --children option if you have an
+especially beefy or weak server box because I<spampd> is a memory-hungry
+program. Check the L<"Options"> for details on this and all other parameters.
+
+Note that I<spampd> B<replaces> I<spamd> from the I<SpamAssassin> distribution
+in function. You do not need to run I<spamd> in order for I<spampd> to work.
+This has apparently been the source of some confusion, so now you know.
+
+=head2 Postfix-specific Notes
+
+Here is a typical setup for Postfix "advanced" content filtering as described
+in the F<FILTER_README> that came with the Postfix distribution (which you
+really need to read):
+
+F</etc/postfix/master.cf>:
+
+ smtp inet n - y - - smtpd
+ -o content_filter=smtp:localhost:10025
+ -o myhostname=mx.example.com
+
+ localhost:10026 inet n - n - 10 smtpd
+ -o content_filter=
+ -o myhostname=mx-int.example.com
+
+The first entry is the main public-facing MTA which uses localhost:10025
+as the content filter for all mail. The second entry receives mail from
+the content filter and does final delivery. Both smtpd instances use
+the same Postfix F<main.cf> file. I<spampd> is the process that listens on
+localhost:10025 and then connects to the Postfix listener on localhost:10026.
+Note that the C<myhostname> options must be different between the two instances,
+otherwise Postfix will think it's talking to itself and abort sending.
+
+For the above example you can simply start I<spampd> like this:
+
+ spampd --host=localhost:10025 --relayhost=localhost:10026
+
+F<FILTER_README> from the Postfix distro has more details and examples of
+various setups, including how to skip the content filter for outbound mail.
+
+Another tip for Postfix when considering what timeout values to use for
+--childtimout and --satimeout options is the following command:
+
+C<# postconf | grep timeout>
+
+This will return a list of useful timeout settings and their values. For
+explanations see the relevant C<man> page (smtp, smtpd, lmtp). By default
+I<spampd> is set up for the default Postfix timeout values.
+
+=head1 Options
+
+=over 5
+
+=item B<--host=ip[:port] or hostname[:port]> C<(changed in v2)>
+
+Specifies what hostname/IP and port I<spampd> listens on. By default, it listens
+on 127.0.0.1 (localhost) on port 10025.
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025. This is an alternate to using the above --host=ip:port notation.
+
+=item B<--relayhost=ip[:port] or hostname[:port]>
+
+Specifies the hostname/IP where I<spampd> will relay all
+messages. Defaults to 127.0.0.1 (localhost). If the port is not provided, that
+defaults to 25.
+
+=item B<--relayport=n> C<(new in v2)>
+
+Specifies what port I<spampd> will relay to. Default is 25. This is an
+alternate to using the above --relayhost=ip:port notation.
+
+=item B<--user=username> or B<--u=username>
+
+=item B<--group=groupname> or B<--g=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--children=n> or B<--c=n> C<(new in v2)>
+
+Number of child servers to start and maintain (where n > 0). Each child will
+process up to --maxrequests (below) before exiting and being replaced by
+another child. Keep this number low on systems w/out a lot of memory.
+Default is 5 (which seems OK on a 512MB lightly loaded system). Note that
+there is always a parent process running, so if you specify 5 children you
+will actually have 6 I<spampd> processes running.
+
+You may want to set your origination mail server to limit the
+number of concurrent connections to I<spampd> to match this setting (for
+Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--childtimeout=n> C<(new in v2)>
+
+This is the number of seconds to allow each child server before it times out
+a transaction. In an S/LMTP transaction the timer is reset for every command.
+This timeout includes time it would take to send the message data, so it should
+not be too short. Note that it's more likely the origination or destination
+mail servers will timeout first, which is fine. This is just a "sane" failsafe.
+Default is 360 seconds (6 minutes).
+
+=item B<--satimeout=n> C<(new in v2)>
+
+This is the number of seconds to allow for processing a message with
+SpamAssassin (including feeding it the message, analyzing it, and adding
+the headers/report if necessary).
+This should be less than your origination and destination servers' timeout
+settings for the DATA command. For Postfix the default is 300 seconds in both
+cases (smtp_data_done_timeout and smtpd_timeout). In the event of timeout
+while processing the message, the problem is logged and the message is passed
+on anyway (w/out spam tagging, obviously). To fail the message with a temp
+450 error, see the --dose (die-on-sa-errors) option, below.
+Default is 285 seconds.
+
+=item B<--pid=filename> or B<--p=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KBytes. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. The size includes headers and attachments (if any).
+
+=item B<--dose> C<(new in v2)>
+
+Acronym for (d)ie (o)n (s)pamAssassin (e)rrors. By default if I<spampd>
+encounters a problem with processing the message through Spam Assassin (timeout
+or other error), it will still pass the mail on to the destination server. If
+you specify this option however, the mail is instead rejected with a temporary
+error (code 450, which means the origination server should keep retrying to send
+it). See the related --satimeout option, above.
+
+=item B<--tagall> or B<--a>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings). Note that
+for this option to work as of SA-2.50, the I<always_add_report> and/or
+I<always_add_headers> settings in your SpamAssassin F<local.cf> need to be
+set to 1/true.
+
+=item B<--log-rules-hit> or B<--rh> C<(new in v2)>
+
+Logs the names of each SpamAssassin rule which matched the message being
+processed. This list is returned by SA.
+
+=item B<--add-sc-header> or B<--ash> C<(new in v2.1)>
+
+Add a 'X-Spam-Checked-By: {hostname}' header to each scanned message. By
+default no such header is added. This can be useful in tracking which server
+in a pool did the scanning. See below for how to specify a hostname.
+
+=item B<--hostname=hostname> C<(new in v2.1)>
+
+Hostname to use in the X-Spam-Checked-By header. By default the value of the
+environmental variable $HOSTNAME is used, or if that is undefined/blank then
+'localhost' is used as the hostname. Only relevant if the --add-sc-header
+option is specified.
+
+=item B<--auto-whitelist> or B<--aw>
+
+Turns on the SpamAssassin global whitelist feature. See the SA docs. Note
+that per-user whitelists are not available.
+
+=item B<--local-only> or B<--L> C<(new in v2)>
+
+Turn off all SA network-based tests (DNS, Razor, etc).
+
+=item B<--debug> or B<--d> C<(changed in v2)>
+
+Turns on SpamAssassin debug messages which print to STDERR (usually the
+console). Also turns on more verbose logging of what spampd is doing (new in
+v2).
+
+=item B<--help> or B<--h>
+
+Prints usage information.
+
+=back
+
+=head2 Deprecated Options
+
+=over 5
+
+The following options are no longer used but still accepted for backwards
+compatibility with I<spampd> v1:
+
+=item B<--dead-letters>
+
+=item B<--heloname>
+
+=item B<--stop-at-threshold>
+
+=back
+
+=head1 Examples
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 Credits
+
+I<spampd> is written and maintained by Maxim Paperno <MPaperno@WorldDesign.com>.
+See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info.
+
+I<spampd> v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+Stanley Dean Witter. These are distributed under the GNU GPL (see
+module code for more details). Both modules have been slightly modified
+from the originals and are included in this file under new names.
+
+Also thanks to Bennet Todd for the example smtpproxy script which helped create
+this version of I<spampd>. See http://bent.latency.net/smtpprox/ .
+
+I<spampd> v1 was based on code by Dave Carrigan named I<assassind>. Trace
+amounts of his code or documentation may still remain. Thanks to him for the
+original inspiration and code. See http://www.rudedog.org/assassind/ .
+
+Also thanks to I<spamd> (included with SpamAssassin) and
+I<amavisd-new> (http://www.ijs.si/software/amavisd/) for some tricks.
+
+=head1 Copyright, License, and Disclaimer
+
+I<spampd> is Copyright (c) 2002 by World Design Group and Maxim Paperno.
+
+Portions are Copyright (C) 2001 Morgan Stanley Dean Witter as mentioned above
+in the Credits section.
+
+ 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.
+
+ The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+
+
+=head1 Bugs
+
+None known. Please report any to MPaperno@WorldDesign.com.
+
+=head1 To Do
+
+Figure out how to use Net::Server::PreFork because it has cool potential for
+load management. I tried but either I'm missing something or PreFork is
+somewhat broken in how it works. If anyone has experience here, please let
+me know.
+
+Add configurable option for rejecting mail outright based on spam score.
+It would be nice to make this program safe enough to sit in front of a mail
+server such as Postfix and be able to reject mail before it enters our systems.
+The only real problem is that Postfix will see localhost as the connecting
+client, so that disables any client-based checks Postfix can do and creates a
+possible relay hole if localhost is trusted.
+
+Per-user preferences: The jury is still out on this one. I'm thinking more
+and more that most per-user prefs should be specified on the final mailbox
+server. Why? Because SMTP isn't designed with per-user preferences in mind.
+On a relay server, the same message body can go to multiple recipients who
+may have wildly different preferences when it comes to handilng junk mail. The
+exception here might be the use of LMTP protocol, which bears further
+investigation.
+
+=head1 See Also
+
+perl(1), Spam::Assassin(3), L<http://www.spamassassin.org/>,
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>
diff --git a/previous-versions/spampd-2.11.pl b/previous-versions/spampd-2.11.pl
new file mode 100644
index 0000000..10835cb
--- /dev/null
+++ b/previous-versions/spampd-2.11.pl
@@ -0,0 +1,1401 @@
+#! /usr/bin/perl -T
+
+######################
+# SpamPD - spam proxy daemon
+#
+# v2.11 - 15-Jul-03
+# v2.10 - 01-Jul-03
+# v2.00 - 10-Jun-03
+# v1.0.2 - 13-Apr-03
+# v1.0.1 - 03-Feb-03
+# v1.0.0 - May 2002
+#
+# spampd is Copyright (c) 2002 by World Design Group and Maxim Paperno
+# (see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# Written and maintained by Maxim Paperno (MPaperno@WorldDesign.com)
+#
+# 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.
+#
+# The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+#
+# spampd v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+# Stanley Dean Witter. These are also distributed under the GNU GPL (see
+# module code for more details). Both modules have been slightly modified
+# from the originals and are included in this file under new names.
+#
+# spampd v1 was based on code by Dave Carrigan named assassind. Trace amounts
+# of his code or documentation may still remain. Thanks to him for the
+# original inspiration and code. (see http://www.rudedog.org/assassind/)
+#
+######################
+
+
+################################################################################
+package SpamPD::Server;
+
+# Originally known as MSDW::SMTP::Server
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =item DESCRIPTION
+#
+# This server simply gathers the SMTP acquired information (envelope
+# sender and recipient, and data) into unparsed memory buffers (or a
+# file for the data), and returns control to the caller to explicitly
+# acknowlege each command or request. Since acknowlegement or failure
+# are driven explicitly from the caller, this module can be used to
+# create a robust SMTP content scanning proxy, transparent or not as
+# desired.
+#
+# =cut
+
+use strict;
+use IO::File;
+#use IO::Socket;
+
+# =item new(interface => $interface, port => $port);
+
+# The #interface and port to listen on must be specified. The interface
+# must be a valid numeric IP address (0.0.0.0 to listen on all
+# interfaces, as usual); the port must be numeric. If this call
+# succeeds, it returns a server structure with an open
+# IO::Socket::INET in it, ready to listen on. If it fails it dies, so
+# if you want anything other than an exit with an explanatory error
+# message, wrap the constructor call in an eval block and pull the
+# error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die.
+#
+# =cut
+
+sub new {
+
+# This now emulates Net::SMTP::Server::Client for use with Net::Server which
+# passes an already open socket.
+
+ my($this, $socket) = @_;
+
+ my $class = ref($this) || $this;
+ my $self = {};
+ $self->{sock} = $socket;
+
+ bless($self, $class);
+
+ die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+ $self->{state} = 'started';
+ return $self;
+
+# Original code, removed by MP for spampd use
+#
+# my ($this, @opts) = @_;
+# my $class = ref($this) || $this;
+# my $self = bless { @opts }, $class;
+# $self->{sock} = IO::Socket::INET->new(
+# LocalAddr => $self->{interface},
+# LocalPort => $self->{port},
+# Proto => 'tcp',
+# Type => SOCK_STREAM,
+# Listen => 65536,
+# Reuse => 1,
+# );
+# die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+# $self->{state} = 'just bound',
+# return $self;
+
+}
+
+# sub accept { }
+#
+# Removed by MP; not needed for spampd use
+#
+
+
+# =item chat;
+#
+# The chat method carries the SMTP dialogue up to the point where any
+# acknowlegement must be made. If chat returns true, then its return
+# value is the previous SMTP command. If the return value begins with
+# 'mail' (case insensitive), then the attribute 'from' has been filled
+# in, and may be checked; if the return value begins with 'rcpt' then
+# both from and to have been been filled in with scalars, and should
+# be checked, then either 'ok' or 'fail' should be called to accept
+# or reject the given sender/recipient pair. If the return value is
+# 'data', then the attributes from and to are populated; in this case,
+# the 'to' attribute is a reference to an anonymous array containing
+# all the recipients for this data. If the return value is '.', then
+# the 'data' attribute (which may be pre-populated in the "new" or
+# "accept" methods if desired) is a reference to a filehandle; if it's
+# created automatically by this module it will point to an unlinked
+# tmp file in /tmp. If chat returns false, the SMTP dialogue has been
+# completed and the socket closed; this server is ready to exit or to
+# accept again, as appropriate for the server style.
+#
+# The return value from chat is also remembered inside the server
+# structure in the "state" attribute.
+#
+# =cut
+
+sub chat {
+ my ($self) = @_;
+ local(*_);
+ if ($self->{state} !~ /^data/i) {
+ return 0 unless defined($_ = $self->_getline);
+ s/[\r\n]*$//;
+ $self->{state} = $_;
+ if (s/^.?he?lo\s+//i) { # mp: find helo|ehlo|lhlo
+ # mp: determine protocol (for future use)
+ if ( /^L/i ) {
+ $self->{proto} = "lmtp";
+ } elsif ( /^E/i ) {
+ $self->{proto} = "esmtp";
+ } else {
+ $self->{proto} = "smtp"; }
+ s/\s*$//;
+ s/\s+/ /g;
+ $self->{helo} = $_;
+ } elsif (s/^rset\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ } elsif (s/^mail\s+from:\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ s/\s*$//;
+ $self->{from} = $_;
+ } elsif (s/^rcpt\s+to:\s*//i) {
+ s/\s*$//; s/\s+/ /g;
+ $self->{to} = $_;
+ push @{$self->{recipients}}, $_;
+ } elsif (/^data/i) {
+ $self->{to} = $self->{recipients};
+ }
+ } else {
+ if (defined($self->{data})) {
+ $self->{data}->seek(0, 0);
+ $self->{data}->truncate(0);
+ } else {
+ $self->{data} = IO::File->new_tmpfile;
+ }
+ while (defined($_ = $self->_getline)) {
+ if ($_ eq ".\r\n") {
+ $self->{data}->seek(0,0);
+ return $self->{state} = '.';
+ }
+ s/^\.\./\./;
+ $self->{data}->print($_) or die "$0: write error saving data\n";
+ }
+ return(0);
+ }
+ return $self->{state};
+}
+
+# =item ok([message]);
+#
+# Approves of the data given to date, either the recipient or the
+# data, in the context of the sender [and, for data, recipients]
+# already given and available as attributes. If a message is given, it
+# will be sent instead of the internal default.
+#
+# =cut
+
+sub ok {
+ my ($self, @msg) = @_;
+ @msg = ("250 ok.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# =item fail([message]);
+#
+# Rejects the current info; if processing from, rejects the sender; if
+# processing 'to', rejects the current recipient; if processing data,
+# rejects the entire message. If a message is specified it means the
+# exact same thing as "ok" --- simply send that message to the sender.
+#
+# =cut
+
+sub fail {
+ my ($self, @msg) = @_;
+ @msg = ("550 no.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# utility functions
+
+sub _getline {
+ my ($self) = @_;
+ local ($/) = "\r\n";
+ my $tmp = $self->{sock}->getline;
+ if ( defined $self->{debug} ) {
+ $self->{debug}->print($tmp) if ($tmp);
+ }
+ return $tmp;
+}
+
+sub _print {
+ my ($self, @msg) = @_;
+ $self->{debug}->print(@msg) if defined $self->{debug};
+ $self->{sock}->print(@msg);
+}
+
+1;
+
+################################################################################
+package SpamPD::Client;
+
+# Originally known as MSDW::SMTP::Client
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =head1 DESCRIPTION
+#
+# MSDW::SMTP::Client provides a very lean SMTP client implementation;
+# the only protocol-specific knowlege it has is the structure of SMTP
+# multiline responses. All specifics lie in the hands of the calling
+# program; this makes it appropriate for a semi-transparent SMTP
+# proxy, passing commands between a talker and a listener.
+#
+# =cut
+
+use strict;
+use IO::Socket;
+
+# =item new(interface => $interface, port => $port[, timeout = 300]);
+#
+# The interface and port to talk to must be specified. The interface
+# must be a valid numeric IP address; the port must be numeric. If
+# this call succeeds, it returns a client structure with an open
+# IO::Socket::INET in it, ready to talk to. If it fails it dies,
+# so if you want anything other than an exit with an explanatory
+# error message, wrap the constructor call in an eval block and pull
+# the error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die. The timeout parameter is passed
+# on into the IO::Socket::INET constructor.
+#
+# =cut
+
+sub new {
+ my ($this, @opts) = @_;
+ my $class = ref($this) || $this;
+ my $self = bless { timeout => 300, @opts }, $class;
+ $self->{sock} = IO::Socket::INET->new(
+ PeerAddr => $self->{interface},
+ PeerPort => $self->{port},
+ Timeout => $self->{timeout},
+ Proto => 'tcp',
+ Type => SOCK_STREAM,
+ );
+ die "$0: socket connect failure: $!\n" unless defined $self->{sock};
+ return $self;
+}
+
+# =item hear
+#
+# hear collects a complete SMTP response and returns it with trailing
+# CRLF removed; for multi-line responses, intermediate CRLFs are left
+# intact. Returns undef if EOF is seen before a complete reply is
+# collected.
+#
+# =cut
+
+sub hear {
+ my ($self) = @_;
+ my ($tmp, $reply);
+ return undef unless $tmp = $self->{sock}->getline;
+ while ($tmp =~ /^\d{3}-/) {
+ $reply .= $tmp;
+ return undef unless $tmp = $self->{sock}->getline;
+ }
+ $reply .= $tmp;
+ $reply =~ s/\r\n$//;
+ return $reply;
+}
+
+# =item say("command text")
+#
+# say sends an SMTP command, appending CRLF.
+#
+# =cut
+
+sub say {
+ my ($self, @msg) = @_;
+ return unless @msg;
+ $self->{sock}->print("@msg", "\r\n") or die "$0: write error: $!";
+}
+
+# =item yammer(FILEHANDLE)
+#
+# yammer takes a filehandle (which should be positioned at the
+# beginning of the file, remember to $fh->seek(0,0) if you've just
+# written it) and sends its contents as the contents of DATA. This
+# should only be invoked after a $client->say("data") and a
+# $client->hear to collect the reply to the data command. It will send
+# the trailing "." as well. It will perform leading-dot-doubling in
+# accordance with the SMTP protocol spec, where "leading dot" is
+# defined in terms of CR-LF terminated lines --- i.e. the data should
+# contain CR-LF data without the leading-dot-quoting. The filehandle
+# will be left at EOF.
+#
+# =cut
+
+sub yammer {
+ my ($self, $fh) = (@_);
+ local (*_);
+ local ($/) = "\r\n";
+ while (<$fh>) {
+ s/^\./../;
+ $self->{sock}->print($_) or die "$0: write error: $!\n";
+ }
+ $self->{sock}->print(".\r\n") or die "$0: write error: $!\n";
+}
+
+1;
+
+
+################################################################################
+package SpamPD;
+
+use strict;
+use Net::Server::PreForkSimple;
+use IO::File;
+use Getopt::Long;
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::NoMailAudit;
+
+BEGIN {
+ # Load Time::HiRes if it's available
+ eval { require Time::HiRes };
+ Time::HiRes->import( qw(time) ) unless $@;
+
+ # use included modules
+ import SpamPD::Server;
+ import SpamPD::Client;
+}
+
+
+use vars qw(@ISA $VERSION);
+our @ISA = qw(Net::Server::PreForkSimple);
+our $VERSION = '2.11';
+
+sub process_message {
+ my ($self, $fh) = @_;
+
+ # output lists with a , delimeter by default
+ local ($") = ",";
+
+ # start a timer
+ my $start = time;
+ # use the assassin object created during startup
+ my $assassin = $self->{spampd}->{assassin};
+
+ # this gets info about the message temp file
+ (my $dev,my $ino,my $mode,my $nlink,my $uid,
+ my $gid,my $rdev,my $size,
+ my $atime,my $mtime,my $ctime,
+ my $blksize,my $blocks) = $fh->stat or die "Can't stat mail file: $!";
+
+ # Only process message under --maxsize KB
+ if ( $size < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ # read message into array of lines to feed to SA
+ # notes in the SA::NoMailAudit code indicate it should take a
+ # filehandle... but that doesn't seem to work :-/
+ my (@msglines, $msgid, $tmp);
+ my $inhdr=1;
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ while (<$fh>) {
+ push(@msglines, $_);
+ $inhdr = 0 if (/^\r?\n$/); # outside of msg header after first blank line
+ # find the Message-ID for logging (code is from spamd)
+ if ( $inhdr && /^Message-Id:\s+(.*?)\s*$/i ) {
+ $msgid = $1;
+ while($msgid =~ s/\([^\(\)]*\)//) {}; # remove comments and
+ $msgid =~ s/^\s+|\s+$//g; # leading and trailing spaces
+ $msgid =~ s/\s.*$//; # keep only the first token
+ }
+ }
+
+ my $recips = "@{$self->{smtp_server}->{to}}";
+ $msgid ||= "(unknown)";
+ $recips ||= "(unknown)";
+
+ $self->log(2, "processing message $msgid for ". $recips);
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Timed out!\n" };
+ # save previous timer and start new
+ my $previous_alarm = alarm($self->{spampd}->{satimeout});
+
+ # Audit the message
+ my $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines );
+
+ # Check spamminess
+ my $status = $assassin->check($mail);
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Returned from checking by SpamAssassin"); }
+
+ my $addingHeader = 0;
+ if ( $self->{spampd}->{addheader} && length($self->{spampd}->{myhostname}) ) {
+ $mail->put_header("X-Spam-Checked-By", $self->{spampd}->{myhostname});
+ $addingHeader = 1;
+ }
+
+ # Rewrite mail if high spam factor or options --tagall or --add-sc-header
+ if ( $status->is_spam || $self->{spampd}->{tagall} || $addingHeader ) {
+
+ # if spam or --tagall, have SA put in its report/headers.
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Rewriting mail using SpamAssassin"); }
+
+ $status->rewrite_mail;
+
+ }
+
+ my $msg_resp = join '', $mail->header, "\r\n", @{$mail->body};
+ my @resplines = split(/\r?\n/, $msg_resp);
+
+ # Build the new message to relay
+ # pause the timeout alarm while we do this (no point in timing
+ # out here and leaving a half-written file).
+ my $pause_alarm = alarm(0);
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ $fh->truncate(0) or die "Can't truncate message file: $!";
+ my $arraycont = @resplines;
+ for ( 0..$arraycont ) {
+ $fh->print($resplines[$_] . "\r\n")
+ or die "Can't print to message file: $!";
+ }
+ #restart the alarm
+ alarm($pause_alarm);
+
+ }
+
+ # Log what we did
+ my $was_it_spam = 'clean message';
+ if ($status->is_spam) { $was_it_spam = 'identified spam'; }
+ my $msg_score = sprintf("%.2f",$status->get_hits);
+ my $msg_threshold = sprintf("%.2f",$status->get_required_hits);
+ my $proc_time = sprintf("%.2f", time - $start);
+
+ $self->log(2, "$was_it_spam $msgid ($msg_score/$msg_threshold) for ".
+ "$recips in $proc_time seconds, $size bytes.");
+
+ # thanks to Kurt Andersen for this idea
+ if ( $self->{spampd}->{rh} ) {
+ $self->log(2, "rules hit for $msgid: " . $status->get_names_of_tests_hit); }
+
+ $status->finish();
+
+ # set the timeout alarm back to wherever it was at
+ alarm($previous_alarm);
+
+ };
+
+ if ( $@ ne '' ) {
+ $self->log(1, "WARNING!! SpamAssassin error on message $msgid: $@");
+ return 0;
+ }
+
+ } else {
+
+ $self->log(2, "skipped large message (". $size / 1024 ."KB)");
+
+ }
+
+ return 1;
+
+}
+
+sub process_request {
+ my $self = shift;
+ my $msg;
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Child server process timed out!\n" };
+ my $timeout = $self->{spampd}->{childtimeout};
+
+ # start a timeout alarm
+ alarm($timeout);
+
+ # start an smtp server
+ my $smtp_server = SpamPD::Server->new($self->{server}->{client});
+ unless ( defined $smtp_server ) {
+ die "Failed to create listening Server: $!"; }
+
+ $self->{smtp_server} = $smtp_server;
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Server"); }
+
+ # start an smtp "client" (really a sending server)
+ my $client = SpamPD::Client->new(interface => $self->{spampd}->{relayhost},
+ port => $self->{spampd}->{relayport});
+ unless ( defined $client ) {
+ die "Failed to create sending Client: $!"; }
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Client"); }
+
+ # pass on initial client response
+ # $client->hear can handle multiline responses so no need to loop
+ $smtp_server->ok($client->hear)
+ or die "Error in initial server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # while loop over incoming data from the server
+ while ( my $what = $smtp_server->chat ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # until end of DATA is sent, just pass the commands on transparently
+ if ($what ne '.') {
+
+ $client->say($what)
+ or die "Failure in client->say(what): $!";
+
+ # but once the data is sent now we want to process it
+ } else {
+
+ # spam checking routine - message might be rewritten here
+ my $pmrescode = $self->process_message($smtp_server->{data});
+
+ # pass on the messsage if exit code <> 0 or die-on-sa-errors flag is off
+ if ( $pmrescode or !$self->{spampd}->{dose} ) {
+
+ # need to give the client a rewound file
+ $smtp_server->{data}->seek(0,0)
+ or die "Can't rewind mail file: $!";
+
+ # now send the data on through the client
+ $client->yammer($smtp_server->{data})
+ or die "Failure in client->yammer(smtp_server->{data}): $!";
+
+ } else {
+
+ $smtp_server->ok("450 Temporary failure processing message, please try again later");
+ last;
+ }
+
+ #close the temp file
+ $smtp_server->{data}->close
+ or $self->log(1, "WARNING!! Couldn't close smtp_server->{data} temp file: $!");
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Finished sending DATA"); }
+ }
+
+ # pass on whatever the relayhost said in response
+ # $client->hear can handle multiline responses so no need to loop
+ my $destresp = $client->hear;
+ $smtp_server->ok($destresp)
+ or die "Error in server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Destination response: '" . $destresp . "'"); }
+
+ # restart the timeout alarm
+ alarm($timeout);
+
+ } # server ends connection
+
+ # close connections
+ $client->{sock}->close
+ or die "Couldn't close client->{sock}: $!";
+ $smtp_server->{sock}->close
+ or die "Couldn't close smtp_server->{sock}: $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Closed connections"); }
+
+ }; # end eval block
+
+ alarm(0); # stop the timer
+ # check for error in eval block
+ if ($@ ne '') {
+ chomp($@);
+ $msg = "WARNING!! Error in process_request eval block: $@";
+ $self->log(0, $msg);
+ die ($msg . "\n");
+ }
+
+ $self->{spampd}->{instance}++;
+
+# if ( $self->{spampd}->{instance}++ > $self->{spampd}->{maxrequests} ) {
+
+# if ( $self->{spampd}->{debug} ) {
+# $self->log(2, "Exiting child process after handling ".
+# $self->{spampd}->{maxrequests} ." requests"); }
+
+# exit 0;
+
+# };
+
+}
+
+# Net::Server hook
+# about to exit child process
+sub child_finish_hook {
+ my($self) = shift;
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Exiting child process after handling ".
+ $self->{spampd}->{instance} ." requests"); }
+}
+
+
+my $relayhost = '127.0.0.1'; # relay to ip
+my $relayport = 25; # relay to port
+my $host = '127.0.0.1'; # listen on ip
+my $port = 10025; # listen on port
+my $children = 5; # number of child processes (servers) to spawn at start
+# my $maxchildren = $children; # max. number of child processes (servers) to spawn
+my $maxrequests = 20; # max requests handled by child b4 dying
+my $childtimeout = 6*60; # child process per-command timeout in seconds
+my $satimeout = 285; # SpamAssassin timeout in seconds (15s less than Postfix
+ # default for smtp_data_done_timeout)
+my $pidfile = '/var/run/spampd.pid'; # write pid to file
+my $user = 'mail'; # user to run as
+my $group = 'mail'; # group to run as
+my $tagall = 0; # mark-up all msgs with SA, not just spam
+my $maxsize = 64; # max. msg size to scan with SA, in KB.
+my $rh = 0; # log which rules were hit
+my $debug = 0; # debug flag
+my $dose = 0; # die-on-sa-errors flag
+my $addheader = 0; # add X-Spam-Checked-By header to all messages
+# hostname to use in X-Spam-Checked-By header:
+my $myhostname = ( length($ENV{HOSTNAME}) ) ? $ENV{HOSTNAME} : "localhost";
+my $logsock = "unix"; # default log socket (some systems like 'inet')
+
+# the following are deprecated as of v.2
+my $heloname = '';
+my $dead_letters = '';
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ relayport => \$relayport,
+ 'dead-letters' => \$dead_letters,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ heloname => \$heloname,
+ childtimeout => \$childtimeout,
+ satimeout => \$satimeout,
+ children => \$children,
+ # maxchildren => \$maxchildren,
+ hostname => \$myhostname,
+ logsock => \$logsock
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'relayport=i',
+ 'children|c=i',
+ # 'maxchildren|mc=i',
+ 'maxrequests|mr=i',
+ 'childtimeout=i',
+ 'satimeout=i',
+ 'dead-letters=s',
+ 'user|u=s',
+ 'group|g=s',
+ 'pid|p=s',
+ 'maxsize=i',
+ 'heloname=s',
+ 'tagall|a',
+ 'auto-whitelist|aw',
+ 'stop-at-threshold',
+ 'debug|d',
+ 'help|h',
+ 'local-only|l',
+ 'log-rules-hit|rh',
+ 'dose',
+ 'add-sc-header|ash',
+ 'hostname=s',
+ 'logsock=s'
+ );
+
+usage(0) if $options{help};
+if ( $logsock !~ /^(unix|inet)$/ ) {
+ print "--logsock parameter needs to be either unix or inet\n\n";
+ usage(0);
+}
+
+if ( $options{tagall} ) { $tagall = 1; }
+if ( $options{'log-rules-hit'} ) { $rh = 1; }
+if ( $options{debug} ) { $debug = 1; }
+if ( $options{dose} ) { $dose = 1; }
+if ( $options{'add-sc-header'} ) { $addheader = 1; }
+# if ( !$options{maxchildren} or $maxchildren < $children ) { $maxchildren = $children; }
+
+if ( $children < 1 ) { print "Option --children must be greater than zero!\n"; exit shift; }
+
+# my $min_spare_servers = ($children == $maxchildren) ? 0 : 1;
+# my $max_spare_servers = ($min_spare_servers == 0) ? 0 : $maxchildren-1;
+
+my @tmp = split (/:/, $relayhost);
+$relayhost = $tmp[0];
+if ( $tmp[1] ) { $relayport = $tmp[1]; }
+
+@tmp = split (/:/, $host);
+$host = $tmp[0];
+if ( $tmp[1] ) { $port = $tmp[1]; }
+
+my $assassin = Mail::SpamAssassin->new({
+ 'dont_copy_prefs' => 1,
+ 'debug' => $debug,
+ 'local_tests_only' => $options{'local-only'} || 0 });
+
+# 'stop_at_threshold' => $options{'stop_at_threshold'} || 0,
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now();
+
+# thanks to Kurt Andersen for the 'uname -s' fix
+if ( !$options{logsock} ) {
+ eval {
+ my $osname = `uname -s`;
+ if (($osname =~ 'HP-UX') || ($osname =~ 'SunOS')) {
+ $logsock = "inet";
+ }
+ };
+}
+
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ syslog_logsock => $logsock,
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => 1,
+ # setsid => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ max_servers => $children,
+ max_requests => $maxrequests,
+ # min_servers => $children,
+ # max_servers => $maxchildren,
+ # min_spare_servers => $min_spare_servers,
+ # max_spare_servers => $max_spare_servers,
+ },
+ spampd => { relayhost => $relayhost,
+ relayport => $relayport,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ childtimeout => $childtimeout,
+ satimeout => $satimeout,
+ rh => $rh,
+ debug => $debug,
+ dose => $dose,
+ addheader => $addheader,
+ myhostname => $myhostname,
+ instance => 0,
+ },
+ }, 'SpamPD';
+
+# call Net::Server to start up the daemon inside
+$server->run;
+
+exit 1; # shouldn't get here
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --host=host[:port] Hostname/IP and optional port to listen on.
+ Default is 127.0.0.1 port 10025
+ --port=n Port to listen on (alternate syntax to above).
+ --relayhost=host[:port] Host to relay mail to.
+ Default is 127.0.0.1 port 25.
+ --relayport=n Port to relay to (alternate syntax to above).
+
+ --children=n Number of child processes (servers) to start and
+ keep running. Default is 5 (plus 1 parent proc).
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Default is 20.
+ --childtimeout=n Time out children after this many seconds during
+ transactions (each S/LMTP command including the
+ time it takes to send the data).
+ Default is 360 seconds (6min).
+ --satimeout=n Time out SpamAssassin after this many seconds.
+ Default is 285 seconds.
+
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+
+ --logsock=inet or unix Allows specifying the syslog socket type. Default is
+ 'unix' except on HPUX and SunOS which prefer 'inet'.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --dose (d)ie (o)n (s)pamAssassin (e)rrors. If this is
+ specified and SA times out or throws an error,
+ the mail will be rejected with a 450 temporary
+ error message. Default is to pass through email
+ even in the event of an SA problem.
+ --tagall Tag all messages with SA headers, not just spam.
+ --log-rules-hit or --rh Log the name of each SA test which matched the
+ current message.
+ --add-sc-header or --ash Add a 'X-Spam-Checked-By: {hostname}' header to each
+ scanned message. By default no header is added.
+ --hostname=hostname Hostname to use in the X-Spam-Checked-By header.
+ By default the value of the environmen variable
+ HOSTNAME is used, or if undefined/blank then
+ 'localhost' is used as the hostname.
+
+ --auto-whitelist or --aw Use the SA global auto-whitelist feature.
+ --local-only or --L Turn off all SA network-based tests (RBL, Razor, etc).
+ --debug or --d Turn on SA debugging (sent to STDERR).
+
+ --help or --h This message
+
+Deprecated Options (still accepted for backwards compatibility):
+ --heloname=hostname No longer used in spampd v.2
+ --dead-letters=path No longer used in spampd v.2
+ --stop-at-threshold No longer implemented in SpamAssassin
+EOF
+
+# --maxchildren=n Maximum number of child processes (servers) to
+# run. Default is the value of --children.
+
+ exit shift;
+}
+
+__END__
+
+# Some commented-out documentation. POD doesn't have a way to comment
+# out sections!? This documents a feature which may be implemented later.
+#
+# =item B<--maxchildren=n> or B<--mc=n> C<(new in v2)>
+#
+# Maximum number of children to spawn if needed (where n >= --children). When
+# I<spampd> starts it will spawn a number of child servers as specified by
+# --children. If all those servers become busy, a new child is spawned up to the
+# number specified in --maxchildren. Default is to have --maxchildren equal to
+# --children so extra child processes aren't started. Also see the --children
+# option, above. You may want to set your origination mail server to limit the
+# number of concurrent connections to I<spampd> to match this setting (for
+# Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+# 'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+#
+# Note that extra servers after the initial --children will only spawn on very
+# busy systems. This is because the check to see if a new server is needed (ie.
+# all current ones are busy) is only done around once per minute (this is
+# controlled by the Net::Server::PreFork module, in case you want to
+# hack at it :). It can still be useful as an "overflow valve," and is
+# especially nice since the extra child servers will die off once they're not
+# needed.
+
+=pod
+
+=head1 NAME
+
+SpamPD - Spam Proxy Daemon (version 2.11)
+
+=head1 Synopsis
+
+B<spampd>
+[B<--host=host[:port]>]
+[B<--relayhost=hostname[:port]>]
+[B<--user|u=username>]
+[B<--group|g=groupname>]
+[B<--children|c=n>]
+#[B<--maxchildren|mc=n>]
+[B<--maxrequests=n>]
+[B<--childtimeout=n>]
+[B<--satimeout=n>]
+[B<--pid|p=filename>]
+[B<--logsock=inet|unix>]
+[B<--maxsize=n>]
+[B<--dose>]
+[B<--tagall|a>]
+[B<--log-rules-hit|rh>]
+[B<--auto-whitelist|aw>]
+[B<--local-only|L>]
+[B<--debug|d>]
+
+B<spampd> B<--help>
+
+=head1 Description
+
+I<spampd> is an SMTP/LMTP proxy that marks (or tags) spam using
+SpamAssassin (http://www.SpamAssassin.org/). The proxy is designed
+to be transparent to the sending and receiving mail servers and at no point
+takes responsibility for the message itself. If a failure occurs within
+I<spampd> (or SpamAssassin) then the mail servers will disconnect and the
+sending server is still responsible for retrying the message for as long
+as it is configured to do so.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+The latest version can be found at
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>.
+
+=head1 Requires
+
+=over 5
+
+Perl modules:
+
+=item B<Mail::SpamAssassin>
+
+=item B<Net::Server::PreForkSimple>
+
+=item B<IO::File>
+
+=item B<IO::Socket>
+
+=item B<Time::HiRes> (not actually required but recommended)
+
+=back
+
+=head1 Operation
+
+I<spampd> is meant to operate as an S/LMTP mail proxy which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing! It is very easy to set up an open relay this
+way.
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+
+B<Running between firewall/gateway and internal mail server>
+
+=over 3
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+ Internet -> [ MX gateway (@inter.net.host:25) ->
+ spampd (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+=back
+
+B<Using Postfix advanced content filtering>
+
+=over 3
+
+Please see the F<FILTER_README> that came with the Postfix distribution. You
+need to have a version of Postfix which supports this (ideally v.2 and up).
+
+ Internet -> [ Postfix (@inter.net.host:25) ->
+ spampd (@localhost:10025) ->
+ Postfix (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+=head1 Upgrading
+
+Upgrading from version 1 simply involves replacing the F<spampd> program file
+with the latest one. Note that the I<dead-letters> folder is no longer being
+used and the --dead-letters option is no longer needed (though no errors are
+thrown if it's present). Check the L<"Options"> list below for a full list of new
+and deprecated options. Also be sure to check out the change log.
+
+=head1 Installation
+
+I<spampd> can be run directly from the command prompt if desired. This is
+useful for testing purposes, but for long term use you probably want to put
+it somewhere like /usr/bin or /usr/local/bin and execute it at system startup.
+For example on Red Hat-style Linux system one can use a script in
+/etc/rc.d/init.d to start I<spampd> (a sample script is available on the
+I<spampd> Web page @ http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm).
+
+The options all have reasonable defaults, especially for a Postfix-centric
+installation. You may want to specify the --children option if you have an
+especially beefy or weak server box because I<spampd> is a memory-hungry
+program. Check the L<"Options"> for details on this and all other parameters.
+
+Note that I<spampd> B<replaces> I<spamd> from the I<SpamAssassin> distribution
+in function. You do not need to run I<spamd> in order for I<spampd> to work.
+This has apparently been the source of some confusion, so now you know.
+
+=head2 Postfix-specific Notes
+
+Here is a typical setup for Postfix "advanced" content filtering as described
+in the F<FILTER_README> that came with the Postfix distribution (which you
+really need to read):
+
+F</etc/postfix/master.cf>:
+
+ smtp inet n - y - - smtpd
+ -o content_filter=smtp:localhost:10025
+ -o myhostname=mx.example.com
+
+ localhost:10026 inet n - n - 10 smtpd
+ -o content_filter=
+ -o myhostname=mx-int.example.com
+
+The first entry is the main public-facing MTA which uses localhost:10025
+as the content filter for all mail. The second entry receives mail from
+the content filter and does final delivery. Both smtpd instances use
+the same Postfix F<main.cf> file. I<spampd> is the process that listens on
+localhost:10025 and then connects to the Postfix listener on localhost:10026.
+Note that the C<myhostname> options must be different between the two instances,
+otherwise Postfix will think it's talking to itself and abort sending.
+
+For the above example you can simply start I<spampd> like this:
+
+ spampd --host=localhost:10025 --relayhost=localhost:10026
+
+F<FILTER_README> from the Postfix distro has more details and examples of
+various setups, including how to skip the content filter for outbound mail.
+
+Another tip for Postfix when considering what timeout values to use for
+--childtimout and --satimeout options is the following command:
+
+C<# postconf | grep timeout>
+
+This will return a list of useful timeout settings and their values. For
+explanations see the relevant C<man> page (smtp, smtpd, lmtp). By default
+I<spampd> is set up for the default Postfix timeout values.
+
+=head1 Options
+
+=over 5
+
+=item B<--host=ip[:port] or hostname[:port]> C<(changed in v2)>
+
+Specifies what hostname/IP and port I<spampd> listens on. By default, it listens
+on 127.0.0.1 (localhost) on port 10025.
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025. This is an alternate to using the above --host=ip:port notation.
+
+=item B<--relayhost=ip[:port] or hostname[:port]>
+
+Specifies the hostname/IP where I<spampd> will relay all
+messages. Defaults to 127.0.0.1 (localhost). If the port is not provided, that
+defaults to 25.
+
+=item B<--relayport=n> C<(new in v2)>
+
+Specifies what port I<spampd> will relay to. Default is 25. This is an
+alternate to using the above --relayhost=ip:port notation.
+
+=item B<--user=username> or B<--u=username>
+
+=item B<--group=groupname> or B<--g=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--children=n> or B<--c=n> C<(new in v2)>
+
+Number of child servers to start and maintain (where n > 0). Each child will
+process up to --maxrequests (below) before exiting and being replaced by
+another child. Keep this number low on systems w/out a lot of memory.
+Default is 5 (which seems OK on a 512MB lightly loaded system). Note that
+there is always a parent process running, so if you specify 5 children you
+will actually have 6 I<spampd> processes running.
+
+You may want to set your origination mail server to limit the
+number of concurrent connections to I<spampd> to match this setting (for
+Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--childtimeout=n> C<(new in v2)>
+
+This is the number of seconds to allow each child server before it times out
+a transaction. In an S/LMTP transaction the timer is reset for every command.
+This timeout includes time it would take to send the message data, so it should
+not be too short. Note that it's more likely the origination or destination
+mail servers will timeout first, which is fine. This is just a "sane" failsafe.
+Default is 360 seconds (6 minutes).
+
+=item B<--satimeout=n> C<(new in v2)>
+
+This is the number of seconds to allow for processing a message with
+SpamAssassin (including feeding it the message, analyzing it, and adding
+the headers/report if necessary).
+This should be less than your origination and destination servers' timeout
+settings for the DATA command. For Postfix the default is 300 seconds in both
+cases (smtp_data_done_timeout and smtpd_timeout). In the event of timeout
+while processing the message, the problem is logged and the message is passed
+on anyway (w/out spam tagging, obviously). To fail the message with a temp
+450 error, see the --dose (die-on-sa-errors) option, below.
+Default is 285 seconds.
+
+=item B<--pid=filename> or B<--p=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--logsock=unix or inet> C<(new in v2.2)>
+
+Syslog socket to use. May be either "unix" of "inet". Default is "unix"
+except on HP-UX and SunOS (Solaris) systems which seem to prefer "inet".
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KBytes. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. The size includes headers and attachments (if any).
+
+=item B<--dose> C<(new in v2)>
+
+Acronym for (d)ie (o)n (s)pamAssassin (e)rrors. By default if I<spampd>
+encounters a problem with processing the message through Spam Assassin (timeout
+or other error), it will still pass the mail on to the destination server. If
+you specify this option however, the mail is instead rejected with a temporary
+error (code 450, which means the origination server should keep retrying to send
+it). See the related --satimeout option, above.
+
+=item B<--tagall> or B<--a>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings). Note that
+for this option to work as of SA-2.50, the I<always_add_report> and/or
+I<always_add_headers> settings in your SpamAssassin F<local.cf> need to be
+set to 1/true.
+
+=item B<--log-rules-hit> or B<--rh> C<(new in v2)>
+
+Logs the names of each SpamAssassin rule which matched the message being
+processed. This list is returned by SA.
+
+=item B<--add-sc-header> or B<--ash> C<(new in v2.1)>
+
+Add a 'X-Spam-Checked-By: {hostname}' header to each scanned message. By
+default no such header is added. This can be useful in tracking which server
+in a pool did the scanning. See below for how to specify a hostname.
+
+=item B<--hostname=hostname> C<(new in v2.1)>
+
+Hostname to use in the X-Spam-Checked-By header. By default the value of the
+environmental variable $HOSTNAME is used, or if that is undefined/blank then
+'localhost' is used as the hostname. Only relevant if the --add-sc-header
+option is specified.
+
+=item B<--auto-whitelist> or B<--aw>
+
+Turns on the SpamAssassin global whitelist feature. See the SA docs. Note
+that per-user whitelists are not available.
+
+=item B<--local-only> or B<--L> C<(new in v2)>
+
+Turn off all SA network-based tests (DNS, Razor, etc).
+
+=item B<--debug> or B<--d> C<(changed in v2)>
+
+Turns on SpamAssassin debug messages which print to STDERR (usually the
+console). Also turns on more verbose logging of what spampd is doing (new in
+v2).
+
+=item B<--help> or B<--h>
+
+Prints usage information.
+
+=back
+
+=head2 Deprecated Options
+
+=over 5
+
+The following options are no longer used but still accepted for backwards
+compatibility with I<spampd> v1:
+
+=item B<--dead-letters>
+
+=item B<--heloname>
+
+=item B<--stop-at-threshold>
+
+=back
+
+=head1 Examples
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 Credits
+
+I<spampd> is written and maintained by Maxim Paperno <MPaperno@WorldDesign.com>.
+See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info.
+
+I<spampd> v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+Stanley Dean Witter. These are distributed under the GNU GPL (see
+module code for more details). Both modules have been slightly modified
+from the originals and are included in this file under new names.
+
+Also thanks to Bennett Todd for the example smtpproxy script which helped create
+this version of I<spampd>. See http://bent.latency.net/smtpprox/ .
+
+I<spampd> v1 was based on code by Dave Carrigan named I<assassind>. Trace
+amounts of his code or documentation may still remain. Thanks to him for the
+original inspiration and code. See http://www.rudedog.org/assassind/ .
+
+Also thanks to I<spamd> (included with SpamAssassin) and
+I<amavisd-new> (http://www.ijs.si/software/amavisd/) for some tricks.
+
+=head1 Copyright, License, and Disclaimer
+
+I<spampd> is Copyright (c) 2002 by World Design Group and Maxim Paperno.
+
+Portions are Copyright (C) 2001 Morgan Stanley Dean Witter as mentioned above
+in the Credits section.
+
+ 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.
+
+ The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+
+
+=head1 Bugs
+
+None known. Please report any to MPaperno@WorldDesign.com.
+
+=head1 To Do
+
+Figure out how to use Net::Server::PreFork because it has cool potential for
+load management. I tried but either I'm missing something or PreFork is
+somewhat broken in how it works. If anyone has experience here, please let
+me know.
+
+Add configurable option for rejecting mail outright based on spam score.
+It would be nice to make this program safe enough to sit in front of a mail
+server such as Postfix and be able to reject mail before it enters our systems.
+The only real problem is that Postfix will see localhost as the connecting
+client, so that disables any client-based checks Postfix can do and creates a
+possible relay hole if localhost is trusted.
+
+Per-user preferences: The jury is still out on this one. I'm thinking more
+and more that most per-user prefs should be specified on the final mailbox
+server. Why? Because SMTP isn't designed with per-user preferences in mind.
+On a relay server, the same message body can go to multiple recipients who
+may have wildly different preferences when it comes to handilng junk mail. The
+exception here might be the use of LMTP protocol, which bears further
+investigation.
+
+=head1 See Also
+
+perl(1), Spam::Assassin(3), L<http://www.spamassassin.org/>,
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>
diff --git a/previous-versions/spampd-2.12.pl b/previous-versions/spampd-2.12.pl
new file mode 100644
index 0000000..5b1bb6c
--- /dev/null
+++ b/previous-versions/spampd-2.12.pl
@@ -0,0 +1,1413 @@
+#! /usr/bin/perl -T
+
+######################
+# SpamPD - spam proxy daemon
+#
+# v2.12 - 15-Nov-03
+# v2.11 - 15-Jul-03
+# v2.10 - 01-Jul-03
+# v2.00 - 10-Jun-03
+# v1.0.2 - 13-Apr-03
+# v1.0.1 - 03-Feb-03
+# v1.0.0 - May 2002
+#
+# spampd is Copyright (c) 2002 by World Design Group and Maxim Paperno
+# (see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# Written and maintained by Maxim Paperno (MPaperno@WorldDesign.com)
+#
+# 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.
+#
+# The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+#
+# spampd v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+# Stanley Dean Witter. These are also distributed under the GNU GPL (see
+# module code for more details). Both modules have been slightly modified
+# from the originals and are included in this file under new names.
+#
+# spampd v1 was based on code by Dave Carrigan named assassind. Trace amounts
+# of his code or documentation may still remain. Thanks to him for the
+# original inspiration and code. (see http://www.rudedog.org/assassind/)
+#
+######################
+
+
+################################################################################
+package SpamPD::Server;
+
+# Originally known as MSDW::SMTP::Server
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =item DESCRIPTION
+#
+# This server simply gathers the SMTP acquired information (envelope
+# sender and recipient, and data) into unparsed memory buffers (or a
+# file for the data), and returns control to the caller to explicitly
+# acknowlege each command or request. Since acknowlegement or failure
+# are driven explicitly from the caller, this module can be used to
+# create a robust SMTP content scanning proxy, transparent or not as
+# desired.
+#
+# =cut
+
+use strict;
+use IO::File;
+#use IO::Socket;
+
+# =item new(interface => $interface, port => $port);
+
+# The #interface and port to listen on must be specified. The interface
+# must be a valid numeric IP address (0.0.0.0 to listen on all
+# interfaces, as usual); the port must be numeric. If this call
+# succeeds, it returns a server structure with an open
+# IO::Socket::INET in it, ready to listen on. If it fails it dies, so
+# if you want anything other than an exit with an explanatory error
+# message, wrap the constructor call in an eval block and pull the
+# error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die.
+#
+# =cut
+
+sub new {
+
+# This now emulates Net::SMTP::Server::Client for use with Net::Server which
+# passes an already open socket.
+
+ my($this, $socket) = @_;
+
+ my $class = ref($this) || $this;
+ my $self = {};
+ $self->{sock} = $socket;
+
+ bless($self, $class);
+
+ die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+ $self->{state} = 'started';
+ return $self;
+
+# Original code, removed by MP for spampd use
+#
+# my ($this, @opts) = @_;
+# my $class = ref($this) || $this;
+# my $self = bless { @opts }, $class;
+# $self->{sock} = IO::Socket::INET->new(
+# LocalAddr => $self->{interface},
+# LocalPort => $self->{port},
+# Proto => 'tcp',
+# Type => SOCK_STREAM,
+# Listen => 65536,
+# Reuse => 1,
+# );
+# die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+# $self->{state} = 'just bound',
+# return $self;
+
+}
+
+# sub accept { }
+#
+# Removed by MP; not needed for spampd use
+#
+
+
+# =item chat;
+#
+# The chat method carries the SMTP dialogue up to the point where any
+# acknowlegement must be made. If chat returns true, then its return
+# value is the previous SMTP command. If the return value begins with
+# 'mail' (case insensitive), then the attribute 'from' has been filled
+# in, and may be checked; if the return value begins with 'rcpt' then
+# both from and to have been been filled in with scalars, and should
+# be checked, then either 'ok' or 'fail' should be called to accept
+# or reject the given sender/recipient pair. If the return value is
+# 'data', then the attributes from and to are populated; in this case,
+# the 'to' attribute is a reference to an anonymous array containing
+# all the recipients for this data. If the return value is '.', then
+# the 'data' attribute (which may be pre-populated in the "new" or
+# "accept" methods if desired) is a reference to a filehandle; if it's
+# created automatically by this module it will point to an unlinked
+# tmp file in /tmp. If chat returns false, the SMTP dialogue has been
+# completed and the socket closed; this server is ready to exit or to
+# accept again, as appropriate for the server style.
+#
+# The return value from chat is also remembered inside the server
+# structure in the "state" attribute.
+#
+# =cut
+
+sub chat {
+ my ($self) = @_;
+ local(*_);
+ if ($self->{state} !~ /^data/i) {
+ return 0 unless defined($_ = $self->_getline);
+ s/[\r\n]*$//;
+ $self->{state} = $_;
+ if (s/^.?he?lo\s+//i) { # mp: find helo|ehlo|lhlo
+ # mp: determine protocol (for future use)
+ if ( /^L/i ) {
+ $self->{proto} = "lmtp";
+ } elsif ( /^E/i ) {
+ $self->{proto} = "esmtp";
+ } else {
+ $self->{proto} = "smtp"; }
+ s/\s*$//;
+ s/\s+/ /g;
+ $self->{helo} = $_;
+ } elsif (s/^rset\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ } elsif (s/^mail\s+from:\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ s/\s*$//;
+ $self->{from} = $_;
+ } elsif (s/^rcpt\s+to:\s*//i) {
+ s/\s*$//; s/\s+/ /g;
+ $self->{to} = $_;
+ push @{$self->{recipients}}, $_;
+ } elsif (/^data/i) {
+ $self->{to} = $self->{recipients};
+ }
+ } else {
+ if (defined($self->{data})) {
+ $self->{data}->seek(0, 0);
+ $self->{data}->truncate(0);
+ } else {
+ $self->{data} = IO::File->new_tmpfile;
+ }
+ while (defined($_ = $self->_getline)) {
+ if ($_ eq ".\r\n") {
+ $self->{data}->seek(0,0);
+ return $self->{state} = '.';
+ }
+ s/^\.\./\./;
+ $self->{data}->print($_) or die "$0: write error saving data\n";
+ }
+ return(0);
+ }
+ return $self->{state};
+}
+
+# =item ok([message]);
+#
+# Approves of the data given to date, either the recipient or the
+# data, in the context of the sender [and, for data, recipients]
+# already given and available as attributes. If a message is given, it
+# will be sent instead of the internal default.
+#
+# =cut
+
+sub ok {
+ my ($self, @msg) = @_;
+ @msg = ("250 ok.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# =item fail([message]);
+#
+# Rejects the current info; if processing from, rejects the sender; if
+# processing 'to', rejects the current recipient; if processing data,
+# rejects the entire message. If a message is specified it means the
+# exact same thing as "ok" --- simply send that message to the sender.
+#
+# =cut
+
+sub fail {
+ my ($self, @msg) = @_;
+ @msg = ("550 no.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# utility functions
+
+sub _getline {
+ my ($self) = @_;
+ local ($/) = "\r\n";
+ my $tmp = $self->{sock}->getline;
+ if ( defined $self->{debug} ) {
+ $self->{debug}->print($tmp) if ($tmp);
+ }
+ return $tmp;
+}
+
+sub _print {
+ my ($self, @msg) = @_;
+ $self->{debug}->print(@msg) if defined $self->{debug};
+ $self->{sock}->print(@msg);
+}
+
+1;
+
+################################################################################
+package SpamPD::Client;
+
+# Originally known as MSDW::SMTP::Client
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =head1 DESCRIPTION
+#
+# MSDW::SMTP::Client provides a very lean SMTP client implementation;
+# the only protocol-specific knowlege it has is the structure of SMTP
+# multiline responses. All specifics lie in the hands of the calling
+# program; this makes it appropriate for a semi-transparent SMTP
+# proxy, passing commands between a talker and a listener.
+#
+# =cut
+
+use strict;
+use IO::Socket;
+
+# =item new(interface => $interface, port => $port[, timeout = 300]);
+#
+# The interface and port to talk to must be specified. The interface
+# must be a valid numeric IP address; the port must be numeric. If
+# this call succeeds, it returns a client structure with an open
+# IO::Socket::INET in it, ready to talk to. If it fails it dies,
+# so if you want anything other than an exit with an explanatory
+# error message, wrap the constructor call in an eval block and pull
+# the error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die. The timeout parameter is passed
+# on into the IO::Socket::INET constructor.
+#
+# =cut
+
+sub new {
+ my ($this, @opts) = @_;
+ my $class = ref($this) || $this;
+ my $self = bless { timeout => 300, @opts }, $class;
+ $self->{sock} = IO::Socket::INET->new(
+ PeerAddr => $self->{interface},
+ PeerPort => $self->{port},
+ Timeout => $self->{timeout},
+ Proto => 'tcp',
+ Type => SOCK_STREAM,
+ );
+ die "$0: socket connect failure: $!\n" unless defined $self->{sock};
+ return $self;
+}
+
+# =item hear
+#
+# hear collects a complete SMTP response and returns it with trailing
+# CRLF removed; for multi-line responses, intermediate CRLFs are left
+# intact. Returns undef if EOF is seen before a complete reply is
+# collected.
+#
+# =cut
+
+sub hear {
+ my ($self) = @_;
+ my ($tmp, $reply);
+ return undef unless $tmp = $self->{sock}->getline;
+ while ($tmp =~ /^\d{3}-/) {
+ $reply .= $tmp;
+ return undef unless $tmp = $self->{sock}->getline;
+ }
+ $reply .= $tmp;
+ $reply =~ s/\r\n$//;
+ return $reply;
+}
+
+# =item say("command text")
+#
+# say sends an SMTP command, appending CRLF.
+#
+# =cut
+
+sub say {
+ my ($self, @msg) = @_;
+ return unless @msg;
+ $self->{sock}->print("@msg", "\r\n") or die "$0: write error: $!";
+}
+
+# =item yammer(FILEHANDLE)
+#
+# yammer takes a filehandle (which should be positioned at the
+# beginning of the file, remember to $fh->seek(0,0) if you've just
+# written it) and sends its contents as the contents of DATA. This
+# should only be invoked after a $client->say("data") and a
+# $client->hear to collect the reply to the data command. It will send
+# the trailing "." as well. It will perform leading-dot-doubling in
+# accordance with the SMTP protocol spec, where "leading dot" is
+# defined in terms of CR-LF terminated lines --- i.e. the data should
+# contain CR-LF data without the leading-dot-quoting. The filehandle
+# will be left at EOF.
+#
+# =cut
+
+sub yammer {
+ my ($self, $fh) = (@_);
+ local (*_);
+ local ($/) = "\r\n";
+ $self->{sock}->autoflush(0); # use less writes (thx to Sam Horrocks for the tip)
+ while (<$fh>) {
+ s/^\./../;
+ $self->{sock}->print($_) or die "$0: write error: $!\n";
+ }
+ $self->{sock}->autoflush(1); # restore unbuffered socket operation
+ $self->{sock}->print(".\r\n") or die "$0: write error: $!\n";
+}
+
+1;
+
+
+################################################################################
+package SpamPD;
+
+use strict;
+use Net::Server::PreForkSimple;
+use IO::File;
+use Getopt::Long;
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::NoMailAudit;
+
+BEGIN {
+ # Load Time::HiRes if it's available
+ eval { require Time::HiRes };
+ Time::HiRes->import( qw(time) ) unless $@;
+
+ # use included modules
+ import SpamPD::Server;
+ import SpamPD::Client;
+}
+
+
+use vars qw(@ISA $VERSION);
+our @ISA = qw(Net::Server::PreForkSimple);
+our $VERSION = '2.12';
+
+sub process_message {
+ my ($self, $fh) = @_;
+
+ # output lists with a , delimeter by default
+ local ($") = ",";
+
+ # start a timer
+ my $start = time;
+ # use the assassin object created during startup
+ my $assassin = $self->{spampd}->{assassin};
+
+ # this gets info about the message temp file
+ (my $dev,my $ino,my $mode,my $nlink,my $uid,
+ my $gid,my $rdev,my $size,
+ my $atime,my $mtime,my $ctime,
+ my $blksize,my $blocks) = $fh->stat or die "Can't stat mail file: $!";
+
+ # Only process message under --maxsize KB
+ if ( $size < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ # read message into array of lines to feed to SA
+ # notes in the SA::NoMailAudit code indicate it should take a
+ # filehandle... but that doesn't seem to work :-/
+ my (@msglines, $msgid, $tmp);
+ my $inhdr=1;
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ while (<$fh>) {
+ push(@msglines, $_);
+ $inhdr = 0 if (/^\r?\n$/); # outside of msg header after first blank line
+ # find the Message-ID for logging (code is from spamd)
+ if ( $inhdr && /^Message-Id:\s+(.*?)\s*$/i ) {
+ $msgid = $1;
+ while($msgid =~ s/\([^\(\)]*\)//) {}; # remove comments and
+ $msgid =~ s/^\s+|\s+$//g; # leading and trailing spaces
+ $msgid =~ s/\s.*$//; # keep only the first token
+ $msgid =~ s/%/%%/g; # escape % because Sys::Syslog uses sprintf()
+ }
+ }
+
+ my $recips = "@{$self->{smtp_server}->{to}}";
+ $msgid ||= "(unknown)";
+ $recips ||= "(unknown)";
+
+ $self->log(2, "processing message $msgid for ". $recips);
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Timed out!\n" };
+ # save previous timer and start new
+ my $previous_alarm = alarm($self->{spampd}->{satimeout});
+
+ # Audit the message
+ my $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines );
+
+ # Check spamminess
+ my $status = $assassin->check($mail);
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Returned from checking by SpamAssassin"); }
+
+ my $addingHeader = 0;
+ if ( $self->{spampd}->{addheader} && length($self->{spampd}->{myhostname}) ) {
+ $mail->put_header("X-Spam-Checked-By", $self->{spampd}->{myhostname});
+ $addingHeader = 1;
+ }
+
+ # Rewrite mail if high spam factor or options --tagall or --add-sc-header
+ if ( $status->is_spam || $self->{spampd}->{tagall} || $addingHeader ) {
+
+ # if spam or --tagall, have SA put in its report/headers.
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Rewriting mail using SpamAssassin"); }
+
+ $status->rewrite_mail;
+
+ }
+
+ my $msg_resp = join '', $mail->header, "\r\n", @{$mail->body};
+ my @resplines = split(/\r?\n/, $msg_resp);
+
+ # Build the new message to relay
+ # pause the timeout alarm while we do this (no point in timing
+ # out here and leaving a half-written file).
+ my $pause_alarm = alarm(0);
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ $fh->truncate(0) or die "Can't truncate message file: $!";
+ my $arraycont = @resplines;
+ for ( 0..$arraycont ) {
+ $fh->print($resplines[$_] . "\r\n")
+ or die "Can't print to message file: $!";
+ }
+ #restart the alarm
+ alarm($pause_alarm);
+
+ }
+
+ # Log what we did
+ my $was_it_spam = 'clean message';
+ if ($status->is_spam) { $was_it_spam = 'identified spam'; }
+ my $msg_score = sprintf("%.2f",$status->get_hits);
+ my $msg_threshold = sprintf("%.2f",$status->get_required_hits);
+ my $proc_time = sprintf("%.2f", time - $start);
+
+ $self->log(2, "$was_it_spam $msgid ($msg_score/$msg_threshold) for ".
+ "$recips in $proc_time seconds, $size bytes.");
+
+ # thanks to Kurt Andersen for this idea
+ if ( $self->{spampd}->{rh} ) {
+ $self->log(2, "rules hit for $msgid: " . $status->get_names_of_tests_hit); }
+
+ $status->finish();
+
+ # set the timeout alarm back to wherever it was at
+ alarm($previous_alarm);
+
+ };
+
+ if ( $@ ne '' ) {
+ $self->log(1, "WARNING!! SpamAssassin error on message $msgid: $@");
+ return 0;
+ }
+
+ } else {
+
+ $self->log(2, "skipped large message (". $size / 1024 ."KB)");
+
+ }
+
+ return 1;
+
+}
+
+sub process_request {
+ my $self = shift;
+ my $msg;
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Child server process timed out!\n" };
+ my $timeout = $self->{spampd}->{childtimeout};
+
+ # start a timeout alarm
+ alarm($timeout);
+
+ # start an smtp server
+ my $smtp_server = SpamPD::Server->new($self->{server}->{client});
+ unless ( defined $smtp_server ) {
+ die "Failed to create listening Server: $!"; }
+
+ $self->{smtp_server} = $smtp_server;
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Server"); }
+
+ # start an smtp "client" (really a sending server)
+ my $client = SpamPD::Client->new(interface => $self->{spampd}->{relayhost},
+ port => $self->{spampd}->{relayport});
+ unless ( defined $client ) {
+ die "Failed to create sending Client: $!"; }
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Client"); }
+
+ # pass on initial client response
+ # $client->hear can handle multiline responses so no need to loop
+ $smtp_server->ok($client->hear)
+ or die "Error in initial server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # while loop over incoming data from the server
+ while ( my $what = $smtp_server->chat ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # until end of DATA is sent, just pass the commands on transparently
+ if ($what ne '.') {
+
+ $client->say($what)
+ or die "Failure in client->say(what): $!";
+
+ # but once the data is sent now we want to process it
+ } else {
+
+ # spam checking routine - message might be rewritten here
+ my $pmrescode = $self->process_message($smtp_server->{data});
+
+ # pass on the messsage if exit code <> 0 or die-on-sa-errors flag is off
+ if ( $pmrescode or !$self->{spampd}->{dose} ) {
+
+ # need to give the client a rewound file
+ $smtp_server->{data}->seek(0,0)
+ or die "Can't rewind mail file: $!";
+
+ # now send the data on through the client
+ $client->yammer($smtp_server->{data})
+ or die "Failure in client->yammer(smtp_server->{data}): $!";
+
+ } else {
+
+ $smtp_server->ok("450 Temporary failure processing message, please try again later");
+ last;
+ }
+
+ #close the temp file
+ $smtp_server->{data}->close
+ or $self->log(1, "WARNING!! Couldn't close smtp_server->{data} temp file: $!");
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Finished sending DATA"); }
+ }
+
+ # pass on whatever the relayhost said in response
+ # $client->hear can handle multiline responses so no need to loop
+ my $destresp = $client->hear;
+ $smtp_server->ok($destresp)
+ or die "Error in server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Destination response: '" . $destresp . "'"); }
+
+ # if we're in data state but the response is an error, exit data state.
+ # Shold not normally occur, but can happen. Thanks to Rodrigo Ventura for bug reports.
+ if ( $smtp_server->{state} =~ /^data/i and $destresp =~ /^[45]\d{2} / ) {
+ $smtp_server->{state} = "err_after_data";
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Destination response indicates error after DATA command"); }
+ }
+
+ # restart the timeout alarm
+ alarm($timeout);
+
+ } # server ends connection
+
+ # close connections
+ $client->{sock}->close
+ or die "Couldn't close client->{sock}: $!";
+ $smtp_server->{sock}->close
+ or die "Couldn't close smtp_server->{sock}: $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Closed connections"); }
+
+ }; # end eval block
+
+ alarm(0); # stop the timer
+ # check for error in eval block
+ if ($@ ne '') {
+ chomp($@);
+ $msg = "WARNING!! Error in process_request eval block: $@";
+ $self->log(0, $msg);
+ die ($msg . "\n");
+ }
+
+ $self->{spampd}->{instance}++;
+
+# if ( $self->{spampd}->{instance}++ > $self->{spampd}->{maxrequests} ) {
+
+# if ( $self->{spampd}->{debug} ) {
+# $self->log(2, "Exiting child process after handling ".
+# $self->{spampd}->{maxrequests} ." requests"); }
+
+# exit 0;
+
+# };
+
+}
+
+# Net::Server hook
+# about to exit child process
+sub child_finish_hook {
+ my($self) = shift;
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Exiting child process after handling ".
+ $self->{spampd}->{instance} ." requests"); }
+}
+
+
+my $relayhost = '127.0.0.1'; # relay to ip
+my $relayport = 25; # relay to port
+my $host = '127.0.0.1'; # listen on ip
+my $port = 10025; # listen on port
+my $children = 5; # number of child processes (servers) to spawn at start
+# my $maxchildren = $children; # max. number of child processes (servers) to spawn
+my $maxrequests = 20; # max requests handled by child b4 dying
+my $childtimeout = 6*60; # child process per-command timeout in seconds
+my $satimeout = 285; # SpamAssassin timeout in seconds (15s less than Postfix
+ # default for smtp_data_done_timeout)
+my $pidfile = '/var/run/spampd.pid'; # write pid to file
+my $user = 'mail'; # user to run as
+my $group = 'mail'; # group to run as
+my $tagall = 0; # mark-up all msgs with SA, not just spam
+my $maxsize = 64; # max. msg size to scan with SA, in KB.
+my $rh = 0; # log which rules were hit
+my $debug = 0; # debug flag
+my $dose = 0; # die-on-sa-errors flag
+my $addheader = 0; # add X-Spam-Checked-By header to all messages
+# hostname to use in X-Spam-Checked-By header:
+my $myhostname = ( length($ENV{HOSTNAME}) ) ? $ENV{HOSTNAME} : "localhost";
+my $logsock = "unix"; # default log socket (some systems like 'inet')
+
+# the following are deprecated as of v.2
+my $heloname = '';
+my $dead_letters = '';
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ relayport => \$relayport,
+ 'dead-letters' => \$dead_letters,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ heloname => \$heloname,
+ childtimeout => \$childtimeout,
+ satimeout => \$satimeout,
+ children => \$children,
+ # maxchildren => \$maxchildren,
+ hostname => \$myhostname,
+ logsock => \$logsock
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'relayport=i',
+ 'children|c=i',
+ # 'maxchildren|mc=i',
+ 'maxrequests|mr=i',
+ 'childtimeout=i',
+ 'satimeout=i',
+ 'dead-letters=s',
+ 'user|u=s',
+ 'group|g=s',
+ 'pid|p=s',
+ 'maxsize=i',
+ 'heloname=s',
+ 'tagall|a',
+ 'auto-whitelist|aw',
+ 'stop-at-threshold',
+ 'debug|d',
+ 'help|h',
+ 'local-only|l',
+ 'log-rules-hit|rh',
+ 'dose',
+ 'add-sc-header|ash',
+ 'hostname=s',
+ 'logsock=s'
+ );
+
+usage(0) if $options{help};
+if ( $logsock !~ /^(unix|inet)$/ ) {
+ print "--logsock parameter needs to be either unix or inet\n\n";
+ usage(0);
+}
+
+if ( $options{tagall} ) { $tagall = 1; }
+if ( $options{'log-rules-hit'} ) { $rh = 1; }
+if ( $options{debug} ) { $debug = 1; }
+if ( $options{dose} ) { $dose = 1; }
+if ( $options{'add-sc-header'} ) { $addheader = 1; }
+# if ( !$options{maxchildren} or $maxchildren < $children ) { $maxchildren = $children; }
+
+if ( $children < 1 ) { print "Option --children must be greater than zero!\n"; exit shift; }
+
+# my $min_spare_servers = ($children == $maxchildren) ? 0 : 1;
+# my $max_spare_servers = ($min_spare_servers == 0) ? 0 : $maxchildren-1;
+
+my @tmp = split (/:/, $relayhost);
+$relayhost = $tmp[0];
+if ( $tmp[1] ) { $relayport = $tmp[1]; }
+
+@tmp = split (/:/, $host);
+$host = $tmp[0];
+if ( $tmp[1] ) { $port = $tmp[1]; }
+
+my $assassin = Mail::SpamAssassin->new({
+ 'dont_copy_prefs' => 1,
+ 'debug' => $debug,
+ 'local_tests_only' => $options{'local-only'} || 0 });
+
+# 'stop_at_threshold' => $options{'stop_at_threshold'} || 0,
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now();
+
+# thanks to Kurt Andersen for the 'uname -s' fix
+if ( !$options{logsock} ) {
+ eval {
+ my $osname = `uname -s`;
+ if (($osname =~ 'HP-UX') || ($osname =~ 'SunOS')) {
+ $logsock = "inet";
+ }
+ };
+}
+
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ syslog_logsock => $logsock,
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => 1,
+ # setsid => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ max_servers => $children,
+ max_requests => $maxrequests,
+ # min_servers => $children,
+ # max_servers => $maxchildren,
+ # min_spare_servers => $min_spare_servers,
+ # max_spare_servers => $max_spare_servers,
+ },
+ spampd => { relayhost => $relayhost,
+ relayport => $relayport,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ childtimeout => $childtimeout,
+ satimeout => $satimeout,
+ rh => $rh,
+ debug => $debug,
+ dose => $dose,
+ addheader => $addheader,
+ myhostname => $myhostname,
+ instance => 0,
+ },
+ }, 'SpamPD';
+
+# call Net::Server to start up the daemon inside
+$server->run;
+
+exit 1; # shouldn't get here
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --host=host[:port] Hostname/IP and optional port to listen on.
+ Default is 127.0.0.1 port 10025
+ --port=n Port to listen on (alternate syntax to above).
+ --relayhost=host[:port] Host to relay mail to.
+ Default is 127.0.0.1 port 25.
+ --relayport=n Port to relay to (alternate syntax to above).
+
+ --children=n Number of child processes (servers) to start and
+ keep running. Default is 5 (plus 1 parent proc).
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Default is 20.
+ --childtimeout=n Time out children after this many seconds during
+ transactions (each S/LMTP command including the
+ time it takes to send the data).
+ Default is 360 seconds (6min).
+ --satimeout=n Time out SpamAssassin after this many seconds.
+ Default is 285 seconds.
+
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+
+ --logsock=inet or unix Allows specifying the syslog socket type. Default is
+ 'unix' except on HPUX and SunOS which prefer 'inet'.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --dose (d)ie (o)n (s)pamAssassin (e)rrors. If this is
+ specified and SA times out or throws an error,
+ the mail will be rejected with a 450 temporary
+ error message. Default is to pass through email
+ even in the event of an SA problem.
+ --tagall Tag all messages with SA headers, not just spam.
+ --log-rules-hit or --rh Log the name of each SA test which matched the
+ current message.
+ --add-sc-header or --ash Add a 'X-Spam-Checked-By: {hostname}' header to each
+ scanned message. By default no header is added.
+ --hostname=hostname Hostname to use in the X-Spam-Checked-By header.
+ By default the value of the environmen variable
+ HOSTNAME is used, or if undefined/blank then
+ 'localhost' is used as the hostname.
+
+ --auto-whitelist or --aw Use the SA global auto-whitelist feature.
+ --local-only or --L Turn off all SA network-based tests (RBL, Razor, etc).
+ --debug or --d Turn on SA debugging (sent to STDERR).
+
+ --help or --h This message
+
+Deprecated Options (still accepted for backwards compatibility):
+ --heloname=hostname No longer used in spampd v.2
+ --dead-letters=path No longer used in spampd v.2
+ --stop-at-threshold No longer implemented in SpamAssassin
+EOF
+
+# --maxchildren=n Maximum number of child processes (servers) to
+# run. Default is the value of --children.
+
+ exit shift;
+}
+
+__END__
+
+# Some commented-out documentation. POD doesn't have a way to comment
+# out sections!? This documents a feature which may be implemented later.
+#
+# =item B<--maxchildren=n> or B<--mc=n> C<(new in v2)>
+#
+# Maximum number of children to spawn if needed (where n >= --children). When
+# I<spampd> starts it will spawn a number of child servers as specified by
+# --children. If all those servers become busy, a new child is spawned up to the
+# number specified in --maxchildren. Default is to have --maxchildren equal to
+# --children so extra child processes aren't started. Also see the --children
+# option, above. You may want to set your origination mail server to limit the
+# number of concurrent connections to I<spampd> to match this setting (for
+# Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+# 'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+#
+# Note that extra servers after the initial --children will only spawn on very
+# busy systems. This is because the check to see if a new server is needed (ie.
+# all current ones are busy) is only done around once per minute (this is
+# controlled by the Net::Server::PreFork module, in case you want to
+# hack at it :). It can still be useful as an "overflow valve," and is
+# especially nice since the extra child servers will die off once they're not
+# needed.
+
+=pod
+
+=head1 NAME
+
+SpamPD - Spam Proxy Daemon (version 2.11)
+
+=head1 Synopsis
+
+B<spampd>
+[B<--host=host[:port]>]
+[B<--relayhost=hostname[:port]>]
+[B<--user|u=username>]
+[B<--group|g=groupname>]
+[B<--children|c=n>]
+#[B<--maxchildren|mc=n>]
+[B<--maxrequests=n>]
+[B<--childtimeout=n>]
+[B<--satimeout=n>]
+[B<--pid|p=filename>]
+[B<--logsock=inet|unix>]
+[B<--maxsize=n>]
+[B<--dose>]
+[B<--tagall|a>]
+[B<--log-rules-hit|rh>]
+[B<--auto-whitelist|aw>]
+[B<--local-only|L>]
+[B<--debug|d>]
+
+B<spampd> B<--help>
+
+=head1 Description
+
+I<spampd> is an SMTP/LMTP proxy that marks (or tags) spam using
+SpamAssassin (http://www.SpamAssassin.org/). The proxy is designed
+to be transparent to the sending and receiving mail servers and at no point
+takes responsibility for the message itself. If a failure occurs within
+I<spampd> (or SpamAssassin) then the mail servers will disconnect and the
+sending server is still responsible for retrying the message for as long
+as it is configured to do so.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+The latest version can be found at
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>.
+
+=head1 Requires
+
+=over 5
+
+Perl modules:
+
+=item B<Mail::SpamAssassin>
+
+=item B<Net::Server::PreForkSimple>
+
+=item B<IO::File>
+
+=item B<IO::Socket>
+
+=item B<Time::HiRes> (not actually required but recommended)
+
+=back
+
+=head1 Operation
+
+I<spampd> is meant to operate as an S/LMTP mail proxy which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing! It is very easy to set up an open relay this
+way.
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+
+B<Running between firewall/gateway and internal mail server>
+
+=over 3
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+ Internet -> [ MX gateway (@inter.net.host:25) ->
+ spampd (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+=back
+
+B<Using Postfix advanced content filtering>
+
+=over 3
+
+Please see the F<FILTER_README> that came with the Postfix distribution. You
+need to have a version of Postfix which supports this (ideally v.2 and up).
+
+ Internet -> [ Postfix (@inter.net.host:25) ->
+ spampd (@localhost:10025) ->
+ Postfix (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+=head1 Upgrading
+
+Upgrading from version 1 simply involves replacing the F<spampd> program file
+with the latest one. Note that the I<dead-letters> folder is no longer being
+used and the --dead-letters option is no longer needed (though no errors are
+thrown if it's present). Check the L<"Options"> list below for a full list of new
+and deprecated options. Also be sure to check out the change log.
+
+=head1 Installation
+
+I<spampd> can be run directly from the command prompt if desired. This is
+useful for testing purposes, but for long term use you probably want to put
+it somewhere like /usr/bin or /usr/local/bin and execute it at system startup.
+For example on Red Hat-style Linux system one can use a script in
+/etc/rc.d/init.d to start I<spampd> (a sample script is available on the
+I<spampd> Web page @ http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm).
+
+The options all have reasonable defaults, especially for a Postfix-centric
+installation. You may want to specify the --children option if you have an
+especially beefy or weak server box because I<spampd> is a memory-hungry
+program. Check the L<"Options"> for details on this and all other parameters.
+
+Note that I<spampd> B<replaces> I<spamd> from the I<SpamAssassin> distribution
+in function. You do not need to run I<spamd> in order for I<spampd> to work.
+This has apparently been the source of some confusion, so now you know.
+
+=head2 Postfix-specific Notes
+
+Here is a typical setup for Postfix "advanced" content filtering as described
+in the F<FILTER_README> that came with the Postfix distribution (which you
+really need to read):
+
+F</etc/postfix/master.cf>:
+
+ smtp inet n - y - - smtpd
+ -o content_filter=smtp:localhost:10025
+ -o myhostname=mx.example.com
+
+ localhost:10026 inet n - n - 10 smtpd
+ -o content_filter=
+ -o myhostname=mx-int.example.com
+
+The first entry is the main public-facing MTA which uses localhost:10025
+as the content filter for all mail. The second entry receives mail from
+the content filter and does final delivery. Both smtpd instances use
+the same Postfix F<main.cf> file. I<spampd> is the process that listens on
+localhost:10025 and then connects to the Postfix listener on localhost:10026.
+Note that the C<myhostname> options must be different between the two instances,
+otherwise Postfix will think it's talking to itself and abort sending.
+
+For the above example you can simply start I<spampd> like this:
+
+ spampd --host=localhost:10025 --relayhost=localhost:10026
+
+F<FILTER_README> from the Postfix distro has more details and examples of
+various setups, including how to skip the content filter for outbound mail.
+
+Another tip for Postfix when considering what timeout values to use for
+--childtimout and --satimeout options is the following command:
+
+C<# postconf | grep timeout>
+
+This will return a list of useful timeout settings and their values. For
+explanations see the relevant C<man> page (smtp, smtpd, lmtp). By default
+I<spampd> is set up for the default Postfix timeout values.
+
+=head1 Options
+
+=over 5
+
+=item B<--host=ip[:port] or hostname[:port]> C<(changed in v2)>
+
+Specifies what hostname/IP and port I<spampd> listens on. By default, it listens
+on 127.0.0.1 (localhost) on port 10025.
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025. This is an alternate to using the above --host=ip:port notation.
+
+=item B<--relayhost=ip[:port] or hostname[:port]>
+
+Specifies the hostname/IP where I<spampd> will relay all
+messages. Defaults to 127.0.0.1 (localhost). If the port is not provided, that
+defaults to 25.
+
+=item B<--relayport=n> C<(new in v2)>
+
+Specifies what port I<spampd> will relay to. Default is 25. This is an
+alternate to using the above --relayhost=ip:port notation.
+
+=item B<--user=username> or B<--u=username>
+
+=item B<--group=groupname> or B<--g=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--children=n> or B<--c=n> C<(new in v2)>
+
+Number of child servers to start and maintain (where n > 0). Each child will
+process up to --maxrequests (below) before exiting and being replaced by
+another child. Keep this number low on systems w/out a lot of memory.
+Default is 5 (which seems OK on a 512MB lightly loaded system). Note that
+there is always a parent process running, so if you specify 5 children you
+will actually have 6 I<spampd> processes running.
+
+You may want to set your origination mail server to limit the
+number of concurrent connections to I<spampd> to match this setting (for
+Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--childtimeout=n> C<(new in v2)>
+
+This is the number of seconds to allow each child server before it times out
+a transaction. In an S/LMTP transaction the timer is reset for every command.
+This timeout includes time it would take to send the message data, so it should
+not be too short. Note that it's more likely the origination or destination
+mail servers will timeout first, which is fine. This is just a "sane" failsafe.
+Default is 360 seconds (6 minutes).
+
+=item B<--satimeout=n> C<(new in v2)>
+
+This is the number of seconds to allow for processing a message with
+SpamAssassin (including feeding it the message, analyzing it, and adding
+the headers/report if necessary).
+This should be less than your origination and destination servers' timeout
+settings for the DATA command. For Postfix the default is 300 seconds in both
+cases (smtp_data_done_timeout and smtpd_timeout). In the event of timeout
+while processing the message, the problem is logged and the message is passed
+on anyway (w/out spam tagging, obviously). To fail the message with a temp
+450 error, see the --dose (die-on-sa-errors) option, below.
+Default is 285 seconds.
+
+=item B<--pid=filename> or B<--p=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--logsock=unix or inet> C<(new in v2.2)>
+
+Syslog socket to use. May be either "unix" of "inet". Default is "unix"
+except on HP-UX and SunOS (Solaris) systems which seem to prefer "inet".
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KBytes. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. The size includes headers and attachments (if any).
+
+=item B<--dose> C<(new in v2)>
+
+Acronym for (d)ie (o)n (s)pamAssassin (e)rrors. By default if I<spampd>
+encounters a problem with processing the message through Spam Assassin (timeout
+or other error), it will still pass the mail on to the destination server. If
+you specify this option however, the mail is instead rejected with a temporary
+error (code 450, which means the origination server should keep retrying to send
+it). See the related --satimeout option, above.
+
+=item B<--tagall> or B<--a>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings). Note that
+for this option to work as of SA-2.50, the I<always_add_report> and/or
+I<always_add_headers> settings in your SpamAssassin F<local.cf> need to be
+set to 1/true.
+
+=item B<--log-rules-hit> or B<--rh> C<(new in v2)>
+
+Logs the names of each SpamAssassin rule which matched the message being
+processed. This list is returned by SA.
+
+=item B<--add-sc-header> or B<--ash> C<(new in v2.1)>
+
+Add a 'X-Spam-Checked-By: {hostname}' header to each scanned message. By
+default no such header is added. This can be useful in tracking which server
+in a pool did the scanning. See below for how to specify a hostname.
+
+=item B<--hostname=hostname> C<(new in v2.1)>
+
+Hostname to use in the X-Spam-Checked-By header. By default the value of the
+environmental variable $HOSTNAME is used, or if that is undefined/blank then
+'localhost' is used as the hostname. Only relevant if the --add-sc-header
+option is specified.
+
+=item B<--auto-whitelist> or B<--aw>
+
+Turns on the SpamAssassin global whitelist feature. See the SA docs. Note
+that per-user whitelists are not available.
+
+=item B<--local-only> or B<--L> C<(new in v2)>
+
+Turn off all SA network-based tests (DNS, Razor, etc).
+
+=item B<--debug> or B<--d> C<(changed in v2)>
+
+Turns on SpamAssassin debug messages which print to STDERR (usually the
+console). Also turns on more verbose logging of what spampd is doing (new in
+v2).
+
+=item B<--help> or B<--h>
+
+Prints usage information.
+
+=back
+
+=head2 Deprecated Options
+
+=over 5
+
+The following options are no longer used but still accepted for backwards
+compatibility with I<spampd> v1:
+
+=item B<--dead-letters>
+
+=item B<--heloname>
+
+=item B<--stop-at-threshold>
+
+=back
+
+=head1 Examples
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 Credits
+
+I<spampd> is written and maintained by Maxim Paperno <MPaperno@WorldDesign.com>.
+See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info.
+
+I<spampd> v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+Stanley Dean Witter. These are distributed under the GNU GPL (see
+module code for more details). Both modules have been slightly modified
+from the originals and are included in this file under new names.
+
+Also thanks to Bennett Todd for the example smtpproxy script which helped create
+this version of I<spampd>. See http://bent.latency.net/smtpprox/ .
+
+I<spampd> v1 was based on code by Dave Carrigan named I<assassind>. Trace
+amounts of his code or documentation may still remain. Thanks to him for the
+original inspiration and code. See http://www.rudedog.org/assassind/ .
+
+Also thanks to I<spamd> (included with SpamAssassin) and
+I<amavisd-new> (http://www.ijs.si/software/amavisd/) for some tricks.
+
+=head1 Copyright, License, and Disclaimer
+
+I<spampd> is Copyright (c) 2002 by World Design Group and Maxim Paperno.
+
+Portions are Copyright (C) 2001 Morgan Stanley Dean Witter as mentioned above
+in the Credits section.
+
+ 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.
+
+ The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+
+
+=head1 Bugs
+
+None known. Please report any to MPaperno@WorldDesign.com.
+
+=head1 To Do
+
+Figure out how to use Net::Server::PreFork because it has cool potential for
+load management. I tried but either I'm missing something or PreFork is
+somewhat broken in how it works. If anyone has experience here, please let
+me know.
+
+Add configurable option for rejecting mail outright based on spam score.
+It would be nice to make this program safe enough to sit in front of a mail
+server such as Postfix and be able to reject mail before it enters our systems.
+The only real problem is that Postfix will see localhost as the connecting
+client, so that disables any client-based checks Postfix can do and creates a
+possible relay hole if localhost is trusted.
+
+Per-user preferences: The jury is still out on this one. I'm thinking more
+and more that most per-user prefs should be specified on the final mailbox
+server. Why? Because SMTP isn't designed with per-user preferences in mind.
+On a relay server, the same message body can go to multiple recipients who
+may have wildly different preferences when it comes to handilng junk mail. The
+exception here might be the use of LMTP protocol, which bears further
+investigation.
+
+=head1 See Also
+
+perl(1), Spam::Assassin(3), L<http://www.spamassassin.org/>,
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>
diff --git a/previous-versions/spampd-2.13.pl b/previous-versions/spampd-2.13.pl
new file mode 100644
index 0000000..0656e28
--- /dev/null
+++ b/previous-versions/spampd-2.13.pl
@@ -0,0 +1,1417 @@
+#! /usr/bin/perl -T
+
+######################
+# SpamPD - spam proxy daemon
+#
+# v2.13 - 24-Nov-03
+# v2.12 - 15-Nov-03
+# v2.11 - 15-Jul-03
+# v2.10 - 01-Jul-03
+# v2.00 - 10-Jun-03
+# v1.0.2 - 13-Apr-03
+# v1.0.1 - 03-Feb-03
+# v1.0.0 - May 2002
+#
+# spampd is Copyright (c) 2002 by World Design Group and Maxim Paperno
+# (see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# Written and maintained by Maxim Paperno (MPaperno@WorldDesign.com)
+#
+# 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.
+#
+# The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+#
+# spampd v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+# Stanley Dean Witter. These are also distributed under the GNU GPL (see
+# module code for more details). Both modules have been slightly modified
+# from the originals and are included in this file under new names.
+#
+# spampd v1 was based on code by Dave Carrigan named assassind. Trace amounts
+# of his code or documentation may still remain. Thanks to him for the
+# original inspiration and code. (see http://www.rudedog.org/assassind/)
+#
+######################
+
+
+################################################################################
+package SpamPD::Server;
+
+# Originally known as MSDW::SMTP::Server
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =item DESCRIPTION
+#
+# This server simply gathers the SMTP acquired information (envelope
+# sender and recipient, and data) into unparsed memory buffers (or a
+# file for the data), and returns control to the caller to explicitly
+# acknowlege each command or request. Since acknowlegement or failure
+# are driven explicitly from the caller, this module can be used to
+# create a robust SMTP content scanning proxy, transparent or not as
+# desired.
+#
+# =cut
+
+use strict;
+use IO::File;
+#use IO::Socket;
+
+# =item new(interface => $interface, port => $port);
+
+# The #interface and port to listen on must be specified. The interface
+# must be a valid numeric IP address (0.0.0.0 to listen on all
+# interfaces, as usual); the port must be numeric. If this call
+# succeeds, it returns a server structure with an open
+# IO::Socket::INET in it, ready to listen on. If it fails it dies, so
+# if you want anything other than an exit with an explanatory error
+# message, wrap the constructor call in an eval block and pull the
+# error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die.
+#
+# =cut
+
+sub new {
+
+# This now emulates Net::SMTP::Server::Client for use with Net::Server which
+# passes an already open socket.
+
+ my($this, $socket) = @_;
+
+ my $class = ref($this) || $this;
+ my $self = {};
+ $self->{sock} = $socket;
+
+ bless($self, $class);
+
+ die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+ $self->{state} = 'started';
+ return $self;
+
+# Original code, removed by MP for spampd use
+#
+# my ($this, @opts) = @_;
+# my $class = ref($this) || $this;
+# my $self = bless { @opts }, $class;
+# $self->{sock} = IO::Socket::INET->new(
+# LocalAddr => $self->{interface},
+# LocalPort => $self->{port},
+# Proto => 'tcp',
+# Type => SOCK_STREAM,
+# Listen => 65536,
+# Reuse => 1,
+# );
+# die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+# $self->{state} = 'just bound',
+# return $self;
+
+}
+
+# sub accept { }
+#
+# Removed by MP; not needed for spampd use
+#
+
+
+# =item chat;
+#
+# The chat method carries the SMTP dialogue up to the point where any
+# acknowlegement must be made. If chat returns true, then its return
+# value is the previous SMTP command. If the return value begins with
+# 'mail' (case insensitive), then the attribute 'from' has been filled
+# in, and may be checked; if the return value begins with 'rcpt' then
+# both from and to have been been filled in with scalars, and should
+# be checked, then either 'ok' or 'fail' should be called to accept
+# or reject the given sender/recipient pair. If the return value is
+# 'data', then the attributes from and to are populated; in this case,
+# the 'to' attribute is a reference to an anonymous array containing
+# all the recipients for this data. If the return value is '.', then
+# the 'data' attribute (which may be pre-populated in the "new" or
+# "accept" methods if desired) is a reference to a filehandle; if it's
+# created automatically by this module it will point to an unlinked
+# tmp file in /tmp. If chat returns false, the SMTP dialogue has been
+# completed and the socket closed; this server is ready to exit or to
+# accept again, as appropriate for the server style.
+#
+# The return value from chat is also remembered inside the server
+# structure in the "state" attribute.
+#
+# =cut
+
+sub chat {
+ my ($self) = @_;
+ local(*_);
+ if ($self->{state} !~ /^data/i) {
+ return 0 unless defined($_ = $self->_getline);
+ s/[\r\n]*$//;
+ $self->{state} = $_;
+ if (s/^.?he?lo\s+//i) { # mp: find helo|ehlo|lhlo
+ # mp: determine protocol (for future use)
+ if ( /^L/i ) {
+ $self->{proto} = "lmtp";
+ } elsif ( /^E/i ) {
+ $self->{proto} = "esmtp";
+ } else {
+ $self->{proto} = "smtp"; }
+ s/\s*$//;
+ s/\s+/ /g;
+ $self->{helo} = $_;
+ } elsif (s/^rset\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ } elsif (s/^mail\s+from:\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ s/\s*$//;
+ $self->{from} = $_;
+ } elsif (s/^rcpt\s+to:\s*//i) {
+ s/\s*$//; s/\s+/ /g;
+ $self->{to} = $_;
+ push @{$self->{recipients}}, $_;
+ } elsif (/^data/i) {
+ $self->{to} = $self->{recipients};
+ }
+ } else {
+ if (defined($self->{data})) {
+ $self->{data}->seek(0, 0);
+ $self->{data}->truncate(0);
+ } else {
+ $self->{data} = IO::File->new_tmpfile;
+ }
+ while (defined($_ = $self->_getline)) {
+ if ($_ eq ".\r\n") {
+ $self->{data}->seek(0,0);
+ return $self->{state} = '.';
+ }
+ s/^\.\./\./;
+ $self->{data}->print($_) or die "$0: write error saving data\n";
+ }
+ return(0);
+ }
+ return $self->{state};
+}
+
+# =item ok([message]);
+#
+# Approves of the data given to date, either the recipient or the
+# data, in the context of the sender [and, for data, recipients]
+# already given and available as attributes. If a message is given, it
+# will be sent instead of the internal default.
+#
+# =cut
+
+sub ok {
+ my ($self, @msg) = @_;
+ @msg = ("250 ok.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# =item fail([message]);
+#
+# Rejects the current info; if processing from, rejects the sender; if
+# processing 'to', rejects the current recipient; if processing data,
+# rejects the entire message. If a message is specified it means the
+# exact same thing as "ok" --- simply send that message to the sender.
+#
+# =cut
+
+sub fail {
+ my ($self, @msg) = @_;
+ @msg = ("550 no.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# utility functions
+
+sub _getline {
+ my ($self) = @_;
+ local ($/) = "\r\n";
+ my $tmp = $self->{sock}->getline;
+ if ( defined $self->{debug} ) {
+ $self->{debug}->print($tmp) if ($tmp);
+ }
+ return $tmp;
+}
+
+sub _print {
+ my ($self, @msg) = @_;
+ $self->{debug}->print(@msg) if defined $self->{debug};
+ $self->{sock}->print(@msg);
+}
+
+1;
+
+################################################################################
+package SpamPD::Client;
+
+# Originally known as MSDW::SMTP::Client
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =head1 DESCRIPTION
+#
+# MSDW::SMTP::Client provides a very lean SMTP client implementation;
+# the only protocol-specific knowlege it has is the structure of SMTP
+# multiline responses. All specifics lie in the hands of the calling
+# program; this makes it appropriate for a semi-transparent SMTP
+# proxy, passing commands between a talker and a listener.
+#
+# =cut
+
+use strict;
+use IO::Socket;
+
+# =item new(interface => $interface, port => $port[, timeout = 300]);
+#
+# The interface and port to talk to must be specified. The interface
+# must be a valid numeric IP address; the port must be numeric. If
+# this call succeeds, it returns a client structure with an open
+# IO::Socket::INET in it, ready to talk to. If it fails it dies,
+# so if you want anything other than an exit with an explanatory
+# error message, wrap the constructor call in an eval block and pull
+# the error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die. The timeout parameter is passed
+# on into the IO::Socket::INET constructor.
+#
+# =cut
+
+sub new {
+ my ($this, @opts) = @_;
+ my $class = ref($this) || $this;
+ my $self = bless { timeout => 300, @opts }, $class;
+ $self->{sock} = IO::Socket::INET->new(
+ PeerAddr => $self->{interface},
+ PeerPort => $self->{port},
+ Timeout => $self->{timeout},
+ Proto => 'tcp',
+ Type => SOCK_STREAM,
+ );
+ die "$0: socket connect failure: $!\n" unless defined $self->{sock};
+ return $self;
+}
+
+# =item hear
+#
+# hear collects a complete SMTP response and returns it with trailing
+# CRLF removed; for multi-line responses, intermediate CRLFs are left
+# intact. Returns undef if EOF is seen before a complete reply is
+# collected.
+#
+# =cut
+
+sub hear {
+ my ($self) = @_;
+ my ($tmp, $reply);
+ return undef unless $tmp = $self->{sock}->getline;
+ while ($tmp =~ /^\d{3}-/) {
+ $reply .= $tmp;
+ return undef unless $tmp = $self->{sock}->getline;
+ }
+ $reply .= $tmp;
+ $reply =~ s/\r\n$//;
+ return $reply;
+}
+
+# =item say("command text")
+#
+# say sends an SMTP command, appending CRLF.
+#
+# =cut
+
+sub say {
+ my ($self, @msg) = @_;
+ return unless @msg;
+ $self->{sock}->print("@msg", "\r\n") or die "$0: write error: $!";
+}
+
+# =item yammer(FILEHANDLE)
+#
+# yammer takes a filehandle (which should be positioned at the
+# beginning of the file, remember to $fh->seek(0,0) if you've just
+# written it) and sends its contents as the contents of DATA. This
+# should only be invoked after a $client->say("data") and a
+# $client->hear to collect the reply to the data command. It will send
+# the trailing "." as well. It will perform leading-dot-doubling in
+# accordance with the SMTP protocol spec, where "leading dot" is
+# defined in terms of CR-LF terminated lines --- i.e. the data should
+# contain CR-LF data without the leading-dot-quoting. The filehandle
+# will be left at EOF.
+#
+# =cut
+
+sub yammer {
+ my ($self, $fh) = (@_);
+ local (*_);
+ local ($/) = "\r\n";
+ $self->{sock}->autoflush(0); # use less writes (thx to Sam Horrocks for the tip)
+ while (<$fh>) {
+ s/^\./../;
+ $self->{sock}->print($_) or die "$0: write error: $!\n";
+ }
+ $self->{sock}->autoflush(1); # restore unbuffered socket operation
+ $self->{sock}->print(".\r\n") or die "$0: write error: $!\n";
+}
+
+1;
+
+
+################################################################################
+package SpamPD;
+
+use strict;
+use Net::Server::PreForkSimple;
+use IO::File;
+use Getopt::Long;
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::NoMailAudit;
+
+BEGIN {
+ # Load Time::HiRes if it's available
+ eval { require Time::HiRes };
+ Time::HiRes->import( qw(time) ) unless $@;
+
+ # use included modules
+ import SpamPD::Server;
+ import SpamPD::Client;
+}
+
+
+use vars qw(@ISA $VERSION);
+our @ISA = qw(Net::Server::PreForkSimple);
+our $VERSION = '2.12';
+
+sub process_message {
+ my ($self, $fh) = @_;
+
+ # output lists with a , delimeter by default
+ local ($") = ",";
+
+ # start a timer
+ my $start = time;
+ # use the assassin object created during startup
+ my $assassin = $self->{spampd}->{assassin};
+
+ # this gets info about the message temp file
+ (my $dev,my $ino,my $mode,my $nlink,my $uid,
+ my $gid,my $rdev,my $size,
+ my $atime,my $mtime,my $ctime,
+ my $blksize,my $blocks) = $fh->stat or die "Can't stat mail file: $!";
+
+ # Only process message under --maxsize KB
+ if ( $size < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ # read message into array of lines to feed to SA
+ # notes in the SA::NoMailAudit code indicate it should take a
+ # filehandle... but that doesn't seem to work :-/
+ my (@msglines, $msgid, $tmp);
+ my $inhdr=1;
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ while (<$fh>) {
+ push(@msglines, $_);
+ $inhdr = 0 if (/^\r?\n$/); # outside of msg header after first blank line
+ # find the Message-ID for logging (code is from spamd)
+ if ( $inhdr && /^Message-Id:\s+(.*?)\s*$/i ) {
+ $msgid = $1;
+ while($msgid =~ s/\([^\(\)]*\)//) {}; # remove comments and
+ $msgid =~ s/^\s+|\s+$//g; # leading and trailing spaces
+ $msgid =~ s/\s.*$//; # keep only the first token
+ $msgid =~ s/%/%%/g; # escape % because Sys::Syslog uses sprintf()
+ }
+ }
+
+ my $recips = "@{$self->{smtp_server}->{to}}";
+ $msgid ||= "(unknown)";
+ $recips ||= "(unknown)";
+
+ $self->log(2, "processing message $msgid for ". $recips);
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Timed out!\n" };
+ # save previous timer and start new
+ my $previous_alarm = alarm($self->{spampd}->{satimeout});
+
+ # Audit the message
+ my $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines );
+
+ # Check spamminess
+ my $status = $assassin->check($mail);
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Returned from checking by SpamAssassin"); }
+
+ my $addingHeader = 0;
+ if ( $self->{spampd}->{addheader} && length($self->{spampd}->{myhostname}) ) {
+ $mail->put_header("X-Spam-Checked-By", $self->{spampd}->{myhostname});
+ $addingHeader = 1;
+ }
+
+ # Rewrite mail if high spam factor or options --tagall or --add-sc-header
+ if ( $status->is_spam || $self->{spampd}->{tagall} || $addingHeader ) {
+
+ # if spam or --tagall, have SA put in its report/headers.
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Rewriting mail using SpamAssassin"); }
+
+ $status->rewrite_mail;
+
+ }
+
+ my $msg_resp = join '', $mail->header, "\r\n", @{$mail->body};
+ my @resplines = split(/\r?\n/, $msg_resp);
+
+ # Build the new message to relay
+ # pause the timeout alarm while we do this (no point in timing
+ # out here and leaving a half-written file).
+ my $pause_alarm = alarm(0);
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ $fh->truncate(0) or die "Can't truncate message file: $!";
+ my $arraycont = @resplines;
+ for ( 0..$arraycont ) {
+ $fh->print($resplines[$_] . "\r\n")
+ or die "Can't print to message file: $!";
+ }
+ #restart the alarm
+ alarm($pause_alarm);
+
+ }
+
+ # Log what we did
+ my $was_it_spam = 'clean message';
+ if ($status->is_spam) { $was_it_spam = 'identified spam'; }
+ my $msg_score = sprintf("%.2f",$status->get_hits);
+ my $msg_threshold = sprintf("%.2f",$status->get_required_hits);
+ my $proc_time = sprintf("%.2f", time - $start);
+
+ $self->log(2, "$was_it_spam $msgid ($msg_score/$msg_threshold) for ".
+ "$recips in $proc_time seconds, $size bytes.");
+
+ # thanks to Kurt Andersen for this idea
+ if ( $self->{spampd}->{rh} ) {
+ $self->log(2, "rules hit for $msgid: " . $status->get_names_of_tests_hit); }
+
+ $status->finish();
+
+ # set the timeout alarm back to wherever it was at
+ alarm($previous_alarm);
+
+ };
+
+ if ( $@ ne '' ) {
+ $self->log(1, "WARNING!! SpamAssassin error on message $msgid: $@");
+ return 0;
+ }
+
+ } else {
+
+ $self->log(2, "skipped large message (". $size / 1024 ."KB)");
+
+ }
+
+ return 1;
+
+}
+
+sub process_request {
+ my $self = shift;
+ my $msg;
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Child server process timed out!\n" };
+ my $timeout = $self->{spampd}->{childtimeout};
+
+ # start a timeout alarm
+ alarm($timeout);
+
+ # start an smtp server
+ my $smtp_server = SpamPD::Server->new($self->{server}->{client});
+ unless ( defined $smtp_server ) {
+ die "Failed to create listening Server: $!"; }
+
+ $self->{smtp_server} = $smtp_server;
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Server"); }
+
+ # start an smtp "client" (really a sending server)
+ my $client = SpamPD::Client->new(interface => $self->{spampd}->{relayhost},
+ port => $self->{spampd}->{relayport});
+ unless ( defined $client ) {
+ die "Failed to create sending Client: $!"; }
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Client"); }
+
+ # pass on initial client response
+ # $client->hear can handle multiline responses so no need to loop
+ $smtp_server->ok($client->hear)
+ or die "Error in initial server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # while loop over incoming data from the server
+ while ( my $what = $smtp_server->chat ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # until end of DATA is sent, just pass the commands on transparently
+ if ($what ne '.') {
+
+ $client->say($what)
+ or die "Failure in client->say(what): $!";
+
+ # but once the data is sent now we want to process it
+ } else {
+
+ # spam checking routine - message might be rewritten here
+ my $pmrescode = $self->process_message($smtp_server->{data});
+
+ # pass on the messsage if exit code <> 0 or die-on-sa-errors flag is off
+ if ( $pmrescode or !$self->{spampd}->{dose} ) {
+
+ # need to give the client a rewound file
+ $smtp_server->{data}->seek(0,0)
+ or die "Can't rewind mail file: $!";
+
+ # now send the data on through the client
+ $client->yammer($smtp_server->{data})
+ or die "Failure in client->yammer(smtp_server->{data}): $!";
+
+ } else {
+
+ $smtp_server->ok("450 Temporary failure processing message, please try again later");
+ last;
+ }
+
+ #close the temp file
+ $smtp_server->{data}->close
+ or $self->log(1, "WARNING!! Couldn't close smtp_server->{data} temp file: $!");
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Finished sending DATA"); }
+ }
+
+ # pass on whatever the relayhost said in response
+ # $client->hear can handle multiline responses so no need to loop
+ my $destresp = $client->hear;
+ $smtp_server->ok($destresp)
+ or die "Error in server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Destination response: '" . $destresp . "'"); }
+
+ # if we're in data state but the response is an error, exit data state.
+ # Shold not normally occur, but can happen. Thanks to Rodrigo Ventura for bug reports.
+ if ( $smtp_server->{state} =~ /^data/i and $destresp =~ /^[45]\d{2} / ) {
+ $smtp_server->{state} = "err_after_data";
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Destination response indicates error after DATA command"); }
+ }
+
+ # restart the timeout alarm
+ alarm($timeout);
+
+ } # server ends connection
+
+ # close connections
+ $client->{sock}->close
+ or die "Couldn't close client->{sock}: $!";
+ $smtp_server->{sock}->close
+ or die "Couldn't close smtp_server->{sock}: $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Closed connections"); }
+
+ }; # end eval block
+
+ alarm(0); # stop the timer
+ # check for error in eval block
+ if ($@ ne '') {
+ chomp($@);
+ $msg = "WARNING!! Error in process_request eval block: $@";
+ $self->log(0, $msg);
+ die ($msg . "\n");
+ }
+
+ $self->{spampd}->{instance}++;
+
+# if ( $self->{spampd}->{instance}++ > $self->{spampd}->{maxrequests} ) {
+
+# if ( $self->{spampd}->{debug} ) {
+# $self->log(2, "Exiting child process after handling ".
+# $self->{spampd}->{maxrequests} ." requests"); }
+
+# exit 0;
+
+# };
+
+}
+
+# Net::Server hook
+# about to exit child process
+sub child_finish_hook {
+ my($self) = shift;
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Exiting child process after handling ".
+ $self->{spampd}->{instance} ." requests"); }
+}
+
+
+my $relayhost = '127.0.0.1'; # relay to ip
+my $relayport = 25; # relay to port
+my $host = '127.0.0.1'; # listen on ip
+my $port = 10025; # listen on port
+my $children = 5; # number of child processes (servers) to spawn at start
+# my $maxchildren = $children; # max. number of child processes (servers) to spawn
+my $maxrequests = 20; # max requests handled by child b4 dying
+my $childtimeout = 6*60; # child process per-command timeout in seconds
+my $satimeout = 285; # SpamAssassin timeout in seconds (15s less than Postfix
+ # default for smtp_data_done_timeout)
+my $pidfile = '/var/run/spampd.pid'; # write pid to file
+my $user = 'mail'; # user to run as
+my $group = 'mail'; # group to run as
+my $tagall = 0; # mark-up all msgs with SA, not just spam
+my $maxsize = 64; # max. msg size to scan with SA, in KB.
+my $rh = 0; # log which rules were hit
+my $debug = 0; # debug flag
+my $dose = 0; # die-on-sa-errors flag
+my $addheader = 0; # add X-Spam-Checked-By header to all messages
+# hostname to use in X-Spam-Checked-By header:
+my $myhostname = ( length($ENV{HOSTNAME}) ) ? $ENV{HOSTNAME} : "localhost";
+my $logsock = "unix"; # default log socket (some systems like 'inet')
+
+# the following are deprecated as of v.2
+my $heloname = '';
+my $dead_letters = '';
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ relayport => \$relayport,
+ 'dead-letters' => \$dead_letters,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ heloname => \$heloname,
+ childtimeout => \$childtimeout,
+ satimeout => \$satimeout,
+ children => \$children,
+ # maxchildren => \$maxchildren,
+ hostname => \$myhostname,
+ logsock => \$logsock
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'relayport=i',
+ 'children|c=i',
+ # 'maxchildren|mc=i',
+ 'maxrequests|mr=i',
+ 'childtimeout=i',
+ 'satimeout=i',
+ 'dead-letters=s',
+ 'user|u=s',
+ 'group|g=s',
+ 'pid|p=s',
+ 'maxsize=i',
+ 'heloname=s',
+ 'tagall|a',
+ 'auto-whitelist|aw',
+ 'stop-at-threshold',
+ 'debug|d',
+ 'help|h',
+ 'local-only|l',
+ 'log-rules-hit|rh',
+ 'dose',
+ 'add-sc-header|ash',
+ 'hostname=s',
+ 'logsock=s'
+ );
+
+usage(0) if $options{help};
+if ( $logsock !~ /^(unix|inet)$/ ) {
+ print "--logsock parameter needs to be either unix or inet\n\n";
+ usage(0);
+}
+
+if ( $options{tagall} ) { $tagall = 1; }
+if ( $options{'log-rules-hit'} ) { $rh = 1; }
+if ( $options{debug} ) { $debug = 1; }
+if ( $options{dose} ) { $dose = 1; }
+if ( $options{'add-sc-header'} ) { $addheader = 1; }
+# if ( !$options{maxchildren} or $maxchildren < $children ) { $maxchildren = $children; }
+
+if ( $children < 1 ) { print "Option --children must be greater than zero!\n"; exit shift; }
+
+# my $min_spare_servers = ($children == $maxchildren) ? 0 : 1;
+# my $max_spare_servers = ($min_spare_servers == 0) ? 0 : $maxchildren-1;
+
+my @tmp = split (/:/, $relayhost);
+$relayhost = $tmp[0];
+if ( $tmp[1] ) { $relayport = $tmp[1]; }
+
+@tmp = split (/:/, $host);
+$host = $tmp[0];
+if ( $tmp[1] ) { $port = $tmp[1]; }
+
+my $assassin = Mail::SpamAssassin->new({
+ 'dont_copy_prefs' => 1,
+ 'debug' => $debug,
+ 'local_tests_only' => $options{'local-only'} || 0 });
+
+# 'stop_at_threshold' => $options{'stop_at_threshold'} || 0,
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now();
+
+# thanks to Kurt Andersen for the 'uname -s' fix
+if ( !$options{logsock} ) {
+ eval {
+ my $osname = `uname -s`;
+ if (($osname =~ 'HP-UX') || ($osname =~ 'SunOS')) {
+ $logsock = "inet";
+ }
+ };
+}
+
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ syslog_logsock => $logsock,
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => 1,
+ # setsid => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ max_servers => $children,
+ max_requests => $maxrequests,
+ # min_servers => $children,
+ # max_servers => $maxchildren,
+ # min_spare_servers => $min_spare_servers,
+ # max_spare_servers => $max_spare_servers,
+ },
+ spampd => { relayhost => $relayhost,
+ relayport => $relayport,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ childtimeout => $childtimeout,
+ satimeout => $satimeout,
+ rh => $rh,
+ debug => $debug,
+ dose => $dose,
+ addheader => $addheader,
+ myhostname => $myhostname,
+ instance => 0,
+ },
+ }, 'SpamPD';
+
+# Redirect all warnings to Server::log
+$SIG{__WARN__} = sub { $server->log (2, $_[0]); };
+
+# call Net::Server to start up the daemon inside
+$server->run;
+
+exit 1; # shouldn't get here
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --host=host[:port] Hostname/IP and optional port to listen on.
+ Default is 127.0.0.1 port 10025
+ --port=n Port to listen on (alternate syntax to above).
+ --relayhost=host[:port] Host to relay mail to.
+ Default is 127.0.0.1 port 25.
+ --relayport=n Port to relay to (alternate syntax to above).
+
+ --children=n Number of child processes (servers) to start and
+ keep running. Default is 5 (plus 1 parent proc).
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Default is 20.
+ --childtimeout=n Time out children after this many seconds during
+ transactions (each S/LMTP command including the
+ time it takes to send the data).
+ Default is 360 seconds (6min).
+ --satimeout=n Time out SpamAssassin after this many seconds.
+ Default is 285 seconds.
+
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+
+ --logsock=inet or unix Allows specifying the syslog socket type. Default is
+ 'unix' except on HPUX and SunOS which prefer 'inet'.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --dose (d)ie (o)n (s)pamAssassin (e)rrors. If this is
+ specified and SA times out or throws an error,
+ the mail will be rejected with a 450 temporary
+ error message. Default is to pass through email
+ even in the event of an SA problem.
+ --tagall Tag all messages with SA headers, not just spam.
+ --log-rules-hit or --rh Log the name of each SA test which matched the
+ current message.
+ --add-sc-header or --ash Add a 'X-Spam-Checked-By: {hostname}' header to each
+ scanned message. By default no header is added.
+ --hostname=hostname Hostname to use in the X-Spam-Checked-By header.
+ By default the value of the environmen variable
+ HOSTNAME is used, or if undefined/blank then
+ 'localhost' is used as the hostname.
+
+ --auto-whitelist or --aw Use the SA global auto-whitelist feature.
+ --local-only or --L Turn off all SA network-based tests (RBL, Razor, etc).
+ --debug or --d Turn on SA debugging (sent to STDERR).
+
+ --help or --h This message
+
+Deprecated Options (still accepted for backwards compatibility):
+ --heloname=hostname No longer used in spampd v.2
+ --dead-letters=path No longer used in spampd v.2
+ --stop-at-threshold No longer implemented in SpamAssassin
+EOF
+
+# --maxchildren=n Maximum number of child processes (servers) to
+# run. Default is the value of --children.
+
+ exit shift;
+}
+
+__END__
+
+# Some commented-out documentation. POD doesn't have a way to comment
+# out sections!? This documents a feature which may be implemented later.
+#
+# =item B<--maxchildren=n> or B<--mc=n> C<(new in v2)>
+#
+# Maximum number of children to spawn if needed (where n >= --children). When
+# I<spampd> starts it will spawn a number of child servers as specified by
+# --children. If all those servers become busy, a new child is spawned up to the
+# number specified in --maxchildren. Default is to have --maxchildren equal to
+# --children so extra child processes aren't started. Also see the --children
+# option, above. You may want to set your origination mail server to limit the
+# number of concurrent connections to I<spampd> to match this setting (for
+# Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+# 'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+#
+# Note that extra servers after the initial --children will only spawn on very
+# busy systems. This is because the check to see if a new server is needed (ie.
+# all current ones are busy) is only done around once per minute (this is
+# controlled by the Net::Server::PreFork module, in case you want to
+# hack at it :). It can still be useful as an "overflow valve," and is
+# especially nice since the extra child servers will die off once they're not
+# needed.
+
+=pod
+
+=head1 NAME
+
+SpamPD - Spam Proxy Daemon (version 2.11)
+
+=head1 Synopsis
+
+B<spampd>
+[B<--host=host[:port]>]
+[B<--relayhost=hostname[:port]>]
+[B<--user|u=username>]
+[B<--group|g=groupname>]
+[B<--children|c=n>]
+#[B<--maxchildren|mc=n>]
+[B<--maxrequests=n>]
+[B<--childtimeout=n>]
+[B<--satimeout=n>]
+[B<--pid|p=filename>]
+[B<--logsock=inet|unix>]
+[B<--maxsize=n>]
+[B<--dose>]
+[B<--tagall|a>]
+[B<--log-rules-hit|rh>]
+[B<--auto-whitelist|aw>]
+[B<--local-only|L>]
+[B<--debug|d>]
+
+B<spampd> B<--help>
+
+=head1 Description
+
+I<spampd> is an SMTP/LMTP proxy that marks (or tags) spam using
+SpamAssassin (http://www.SpamAssassin.org/). The proxy is designed
+to be transparent to the sending and receiving mail servers and at no point
+takes responsibility for the message itself. If a failure occurs within
+I<spampd> (or SpamAssassin) then the mail servers will disconnect and the
+sending server is still responsible for retrying the message for as long
+as it is configured to do so.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+The latest version can be found at
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>.
+
+=head1 Requires
+
+=over 5
+
+Perl modules:
+
+=item B<Mail::SpamAssassin>
+
+=item B<Net::Server::PreForkSimple>
+
+=item B<IO::File>
+
+=item B<IO::Socket>
+
+=item B<Time::HiRes> (not actually required but recommended)
+
+=back
+
+=head1 Operation
+
+I<spampd> is meant to operate as an S/LMTP mail proxy which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing! It is very easy to set up an open relay this
+way.
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+
+B<Running between firewall/gateway and internal mail server>
+
+=over 3
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+ Internet -> [ MX gateway (@inter.net.host:25) ->
+ spampd (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+=back
+
+B<Using Postfix advanced content filtering>
+
+=over 3
+
+Please see the F<FILTER_README> that came with the Postfix distribution. You
+need to have a version of Postfix which supports this (ideally v.2 and up).
+
+ Internet -> [ Postfix (@inter.net.host:25) ->
+ spampd (@localhost:10025) ->
+ Postfix (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+=head1 Upgrading
+
+Upgrading from version 1 simply involves replacing the F<spampd> program file
+with the latest one. Note that the I<dead-letters> folder is no longer being
+used and the --dead-letters option is no longer needed (though no errors are
+thrown if it's present). Check the L<"Options"> list below for a full list of new
+and deprecated options. Also be sure to check out the change log.
+
+=head1 Installation
+
+I<spampd> can be run directly from the command prompt if desired. This is
+useful for testing purposes, but for long term use you probably want to put
+it somewhere like /usr/bin or /usr/local/bin and execute it at system startup.
+For example on Red Hat-style Linux system one can use a script in
+/etc/rc.d/init.d to start I<spampd> (a sample script is available on the
+I<spampd> Web page @ http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm).
+
+The options all have reasonable defaults, especially for a Postfix-centric
+installation. You may want to specify the --children option if you have an
+especially beefy or weak server box because I<spampd> is a memory-hungry
+program. Check the L<"Options"> for details on this and all other parameters.
+
+Note that I<spampd> B<replaces> I<spamd> from the I<SpamAssassin> distribution
+in function. You do not need to run I<spamd> in order for I<spampd> to work.
+This has apparently been the source of some confusion, so now you know.
+
+=head2 Postfix-specific Notes
+
+Here is a typical setup for Postfix "advanced" content filtering as described
+in the F<FILTER_README> that came with the Postfix distribution (which you
+really need to read):
+
+F</etc/postfix/master.cf>:
+
+ smtp inet n - y - - smtpd
+ -o content_filter=smtp:localhost:10025
+ -o myhostname=mx.example.com
+
+ localhost:10026 inet n - n - 10 smtpd
+ -o content_filter=
+ -o myhostname=mx-int.example.com
+
+The first entry is the main public-facing MTA which uses localhost:10025
+as the content filter for all mail. The second entry receives mail from
+the content filter and does final delivery. Both smtpd instances use
+the same Postfix F<main.cf> file. I<spampd> is the process that listens on
+localhost:10025 and then connects to the Postfix listener on localhost:10026.
+Note that the C<myhostname> options must be different between the two instances,
+otherwise Postfix will think it's talking to itself and abort sending.
+
+For the above example you can simply start I<spampd> like this:
+
+ spampd --host=localhost:10025 --relayhost=localhost:10026
+
+F<FILTER_README> from the Postfix distro has more details and examples of
+various setups, including how to skip the content filter for outbound mail.
+
+Another tip for Postfix when considering what timeout values to use for
+--childtimout and --satimeout options is the following command:
+
+C<# postconf | grep timeout>
+
+This will return a list of useful timeout settings and their values. For
+explanations see the relevant C<man> page (smtp, smtpd, lmtp). By default
+I<spampd> is set up for the default Postfix timeout values.
+
+=head1 Options
+
+=over 5
+
+=item B<--host=ip[:port] or hostname[:port]> C<(changed in v2)>
+
+Specifies what hostname/IP and port I<spampd> listens on. By default, it listens
+on 127.0.0.1 (localhost) on port 10025.
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025. This is an alternate to using the above --host=ip:port notation.
+
+=item B<--relayhost=ip[:port] or hostname[:port]>
+
+Specifies the hostname/IP where I<spampd> will relay all
+messages. Defaults to 127.0.0.1 (localhost). If the port is not provided, that
+defaults to 25.
+
+=item B<--relayport=n> C<(new in v2)>
+
+Specifies what port I<spampd> will relay to. Default is 25. This is an
+alternate to using the above --relayhost=ip:port notation.
+
+=item B<--user=username> or B<--u=username>
+
+=item B<--group=groupname> or B<--g=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--children=n> or B<--c=n> C<(new in v2)>
+
+Number of child servers to start and maintain (where n > 0). Each child will
+process up to --maxrequests (below) before exiting and being replaced by
+another child. Keep this number low on systems w/out a lot of memory.
+Default is 5 (which seems OK on a 512MB lightly loaded system). Note that
+there is always a parent process running, so if you specify 5 children you
+will actually have 6 I<spampd> processes running.
+
+You may want to set your origination mail server to limit the
+number of concurrent connections to I<spampd> to match this setting (for
+Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--childtimeout=n> C<(new in v2)>
+
+This is the number of seconds to allow each child server before it times out
+a transaction. In an S/LMTP transaction the timer is reset for every command.
+This timeout includes time it would take to send the message data, so it should
+not be too short. Note that it's more likely the origination or destination
+mail servers will timeout first, which is fine. This is just a "sane" failsafe.
+Default is 360 seconds (6 minutes).
+
+=item B<--satimeout=n> C<(new in v2)>
+
+This is the number of seconds to allow for processing a message with
+SpamAssassin (including feeding it the message, analyzing it, and adding
+the headers/report if necessary).
+This should be less than your origination and destination servers' timeout
+settings for the DATA command. For Postfix the default is 300 seconds in both
+cases (smtp_data_done_timeout and smtpd_timeout). In the event of timeout
+while processing the message, the problem is logged and the message is passed
+on anyway (w/out spam tagging, obviously). To fail the message with a temp
+450 error, see the --dose (die-on-sa-errors) option, below.
+Default is 285 seconds.
+
+=item B<--pid=filename> or B<--p=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--logsock=unix or inet> C<(new in v2.2)>
+
+Syslog socket to use. May be either "unix" of "inet". Default is "unix"
+except on HP-UX and SunOS (Solaris) systems which seem to prefer "inet".
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KBytes. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. The size includes headers and attachments (if any).
+
+=item B<--dose> C<(new in v2)>
+
+Acronym for (d)ie (o)n (s)pamAssassin (e)rrors. By default if I<spampd>
+encounters a problem with processing the message through Spam Assassin (timeout
+or other error), it will still pass the mail on to the destination server. If
+you specify this option however, the mail is instead rejected with a temporary
+error (code 450, which means the origination server should keep retrying to send
+it). See the related --satimeout option, above.
+
+=item B<--tagall> or B<--a>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings). Note that
+for this option to work as of SA-2.50, the I<always_add_report> and/or
+I<always_add_headers> settings in your SpamAssassin F<local.cf> need to be
+set to 1/true.
+
+=item B<--log-rules-hit> or B<--rh> C<(new in v2)>
+
+Logs the names of each SpamAssassin rule which matched the message being
+processed. This list is returned by SA.
+
+=item B<--add-sc-header> or B<--ash> C<(new in v2.1)>
+
+Add a 'X-Spam-Checked-By: {hostname}' header to each scanned message. By
+default no such header is added. This can be useful in tracking which server
+in a pool did the scanning. See below for how to specify a hostname.
+
+=item B<--hostname=hostname> C<(new in v2.1)>
+
+Hostname to use in the X-Spam-Checked-By header. By default the value of the
+environmental variable $HOSTNAME is used, or if that is undefined/blank then
+'localhost' is used as the hostname. Only relevant if the --add-sc-header
+option is specified.
+
+=item B<--auto-whitelist> or B<--aw>
+
+Turns on the SpamAssassin global whitelist feature. See the SA docs. Note
+that per-user whitelists are not available.
+
+=item B<--local-only> or B<--L> C<(new in v2)>
+
+Turn off all SA network-based tests (DNS, Razor, etc).
+
+=item B<--debug> or B<--d> C<(changed in v2)>
+
+Turns on SpamAssassin debug messages which print to STDERR (usually the
+console). Also turns on more verbose logging of what spampd is doing (new in
+v2).
+
+=item B<--help> or B<--h>
+
+Prints usage information.
+
+=back
+
+=head2 Deprecated Options
+
+=over 5
+
+The following options are no longer used but still accepted for backwards
+compatibility with I<spampd> v1:
+
+=item B<--dead-letters>
+
+=item B<--heloname>
+
+=item B<--stop-at-threshold>
+
+=back
+
+=head1 Examples
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 Credits
+
+I<spampd> is written and maintained by Maxim Paperno <MPaperno@WorldDesign.com>.
+See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info.
+
+I<spampd> v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+Stanley Dean Witter. These are distributed under the GNU GPL (see
+module code for more details). Both modules have been slightly modified
+from the originals and are included in this file under new names.
+
+Also thanks to Bennett Todd for the example smtpproxy script which helped create
+this version of I<spampd>. See http://bent.latency.net/smtpprox/ .
+
+I<spampd> v1 was based on code by Dave Carrigan named I<assassind>. Trace
+amounts of his code or documentation may still remain. Thanks to him for the
+original inspiration and code. See http://www.rudedog.org/assassind/ .
+
+Also thanks to I<spamd> (included with SpamAssassin) and
+I<amavisd-new> (http://www.ijs.si/software/amavisd/) for some tricks.
+
+=head1 Copyright, License, and Disclaimer
+
+I<spampd> is Copyright (c) 2002 by World Design Group and Maxim Paperno.
+
+Portions are Copyright (C) 2001 Morgan Stanley Dean Witter as mentioned above
+in the Credits section.
+
+ 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.
+
+ The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+
+
+=head1 Bugs
+
+None known. Please report any to MPaperno@WorldDesign.com.
+
+=head1 To Do
+
+Figure out how to use Net::Server::PreFork because it has cool potential for
+load management. I tried but either I'm missing something or PreFork is
+somewhat broken in how it works. If anyone has experience here, please let
+me know.
+
+Add configurable option for rejecting mail outright based on spam score.
+It would be nice to make this program safe enough to sit in front of a mail
+server such as Postfix and be able to reject mail before it enters our systems.
+The only real problem is that Postfix will see localhost as the connecting
+client, so that disables any client-based checks Postfix can do and creates a
+possible relay hole if localhost is trusted.
+
+Per-user preferences: The jury is still out on this one. I'm thinking more
+and more that most per-user prefs should be specified on the final mailbox
+server. Why? Because SMTP isn't designed with per-user preferences in mind.
+On a relay server, the same message body can go to multiple recipients who
+may have wildly different preferences when it comes to handilng junk mail. The
+exception here might be the use of LMTP protocol, which bears further
+investigation.
+
+=head1 See Also
+
+perl(1), Spam::Assassin(3), L<http://www.spamassassin.org/>,
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>
diff --git a/previous-versions/spampd-2.2.pl b/previous-versions/spampd-2.2.pl
new file mode 100644
index 0000000..f2c09a0
--- /dev/null
+++ b/previous-versions/spampd-2.2.pl
@@ -0,0 +1,1418 @@
+#! /usr/bin/perl -T
+
+######################
+# SpamPD - spam proxy daemon
+#
+# v2.20 - 05-Oct-04
+# v2.13 - 24-Nov-03
+# v2.12 - 15-Nov-03
+# v2.11 - 15-Jul-03
+# v2.10 - 01-Jul-03
+# v2.00 - 10-Jun-03
+# v1.0.2 - 13-Apr-03
+# v1.0.1 - 03-Feb-03
+# v1.0.0 - May 2002
+#
+# spampd is Copyright (c) 2002 by World Design Group and Maxim Paperno
+# (see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# Written and maintained by Maxim Paperno (MPaperno@WorldDesign.com)
+#
+# 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.
+#
+# The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+#
+# spampd v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+# Stanley Dean Witter. These are also distributed under the GNU GPL (see
+# module code for more details). Both modules have been slightly modified
+# from the originals and are included in this file under new names.
+#
+# spampd v1 was based on code by Dave Carrigan named assassind. Trace amounts
+# of his code or documentation may still remain. Thanks to him for the
+# original inspiration and code. (see http://www.rudedog.org/assassind/)
+#
+######################
+
+
+################################################################################
+package SpamPD::Server;
+
+# Originally known as MSDW::SMTP::Server
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =item DESCRIPTION
+#
+# This server simply gathers the SMTP acquired information (envelope
+# sender and recipient, and data) into unparsed memory buffers (or a
+# file for the data), and returns control to the caller to explicitly
+# acknowlege each command or request. Since acknowlegement or failure
+# are driven explicitly from the caller, this module can be used to
+# create a robust SMTP content scanning proxy, transparent or not as
+# desired.
+#
+# =cut
+
+use strict;
+use IO::File;
+#use IO::Socket;
+
+# =item new(interface => $interface, port => $port);
+
+# The #interface and port to listen on must be specified. The interface
+# must be a valid numeric IP address (0.0.0.0 to listen on all
+# interfaces, as usual); the port must be numeric. If this call
+# succeeds, it returns a server structure with an open
+# IO::Socket::INET in it, ready to listen on. If it fails it dies, so
+# if you want anything other than an exit with an explanatory error
+# message, wrap the constructor call in an eval block and pull the
+# error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die.
+#
+# =cut
+
+sub new {
+
+# This now emulates Net::SMTP::Server::Client for use with Net::Server which
+# passes an already open socket.
+
+ my($this, $socket) = @_;
+
+ my $class = ref($this) || $this;
+ my $self = {};
+ $self->{sock} = $socket;
+
+ bless($self, $class);
+
+ die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+ $self->{state} = 'started';
+ return $self;
+
+# Original code, removed by MP for spampd use
+#
+# my ($this, @opts) = @_;
+# my $class = ref($this) || $this;
+# my $self = bless { @opts }, $class;
+# $self->{sock} = IO::Socket::INET->new(
+# LocalAddr => $self->{interface},
+# LocalPort => $self->{port},
+# Proto => 'tcp',
+# Type => SOCK_STREAM,
+# Listen => 65536,
+# Reuse => 1,
+# );
+# die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+# $self->{state} = 'just bound',
+# return $self;
+
+}
+
+# sub accept { }
+#
+# Removed by MP; not needed for spampd use
+#
+
+
+# =item chat;
+#
+# The chat method carries the SMTP dialogue up to the point where any
+# acknowlegement must be made. If chat returns true, then its return
+# value is the previous SMTP command. If the return value begins with
+# 'mail' (case insensitive), then the attribute 'from' has been filled
+# in, and may be checked; if the return value begins with 'rcpt' then
+# both from and to have been been filled in with scalars, and should
+# be checked, then either 'ok' or 'fail' should be called to accept
+# or reject the given sender/recipient pair. If the return value is
+# 'data', then the attributes from and to are populated; in this case,
+# the 'to' attribute is a reference to an anonymous array containing
+# all the recipients for this data. If the return value is '.', then
+# the 'data' attribute (which may be pre-populated in the "new" or
+# "accept" methods if desired) is a reference to a filehandle; if it's
+# created automatically by this module it will point to an unlinked
+# tmp file in /tmp. If chat returns false, the SMTP dialogue has been
+# completed and the socket closed; this server is ready to exit or to
+# accept again, as appropriate for the server style.
+#
+# The return value from chat is also remembered inside the server
+# structure in the "state" attribute.
+#
+# =cut
+
+sub chat {
+ my ($self) = @_;
+ local(*_);
+ if ($self->{state} !~ /^data/i) {
+ return 0 unless defined($_ = $self->_getline);
+ s/[\r\n]*$//;
+ $self->{state} = $_;
+ if (s/^.?he?lo\s+//i) { # mp: find helo|ehlo|lhlo
+ # mp: determine protocol (for future use)
+ if ( /^L/i ) {
+ $self->{proto} = "lmtp";
+ } elsif ( /^E/i ) {
+ $self->{proto} = "esmtp";
+ } else {
+ $self->{proto} = "smtp"; }
+ s/\s*$//;
+ s/\s+/ /g;
+ $self->{helo} = $_;
+ } elsif (s/^rset\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ } elsif (s/^mail\s+from:\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ s/\s*$//;
+ $self->{from} = $_;
+ } elsif (s/^rcpt\s+to:\s*//i) {
+ s/\s*$//; s/\s+/ /g;
+ $self->{to} = $_;
+ push @{$self->{recipients}}, $_;
+ } elsif (/^data/i) {
+ $self->{to} = $self->{recipients};
+ }
+ } else {
+ if (defined($self->{data})) {
+ $self->{data}->seek(0, 0);
+ $self->{data}->truncate(0);
+ } else {
+ $self->{data} = IO::File->new_tmpfile;
+ }
+ while (defined($_ = $self->_getline)) {
+ if ($_ eq ".\r\n") {
+ $self->{data}->seek(0,0);
+ return $self->{state} = '.';
+ }
+ s/^\.\./\./;
+ $self->{data}->print($_) or die "$0: write error saving data\n";
+ }
+ return(0);
+ }
+ return $self->{state};
+}
+
+# =item ok([message]);
+#
+# Approves of the data given to date, either the recipient or the
+# data, in the context of the sender [and, for data, recipients]
+# already given and available as attributes. If a message is given, it
+# will be sent instead of the internal default.
+#
+# =cut
+
+sub ok {
+ my ($self, @msg) = @_;
+ @msg = ("250 ok.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# =item fail([message]);
+#
+# Rejects the current info; if processing from, rejects the sender; if
+# processing 'to', rejects the current recipient; if processing data,
+# rejects the entire message. If a message is specified it means the
+# exact same thing as "ok" --- simply send that message to the sender.
+#
+# =cut
+
+sub fail {
+ my ($self, @msg) = @_;
+ @msg = ("550 no.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# utility functions
+
+sub _getline {
+ my ($self) = @_;
+ local ($/) = "\r\n";
+ my $tmp = $self->{sock}->getline;
+ if ( defined $self->{debug} ) {
+ $self->{debug}->print($tmp) if ($tmp);
+ }
+ return $tmp;
+}
+
+sub _print {
+ my ($self, @msg) = @_;
+ $self->{debug}->print(@msg) if defined $self->{debug};
+ $self->{sock}->print(@msg);
+}
+
+1;
+
+################################################################################
+package SpamPD::Client;
+
+# Originally known as MSDW::SMTP::Client
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =head1 DESCRIPTION
+#
+# MSDW::SMTP::Client provides a very lean SMTP client implementation;
+# the only protocol-specific knowlege it has is the structure of SMTP
+# multiline responses. All specifics lie in the hands of the calling
+# program; this makes it appropriate for a semi-transparent SMTP
+# proxy, passing commands between a talker and a listener.
+#
+# =cut
+
+use strict;
+use IO::Socket;
+
+# =item new(interface => $interface, port => $port[, timeout = 300]);
+#
+# The interface and port to talk to must be specified. The interface
+# must be a valid numeric IP address; the port must be numeric. If
+# this call succeeds, it returns a client structure with an open
+# IO::Socket::INET in it, ready to talk to. If it fails it dies,
+# so if you want anything other than an exit with an explanatory
+# error message, wrap the constructor call in an eval block and pull
+# the error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die. The timeout parameter is passed
+# on into the IO::Socket::INET constructor.
+#
+# =cut
+
+sub new {
+ my ($this, @opts) = @_;
+ my $class = ref($this) || $this;
+ my $self = bless { timeout => 300, @opts }, $class;
+ $self->{sock} = IO::Socket::INET->new(
+ PeerAddr => $self->{interface},
+ PeerPort => $self->{port},
+ Timeout => $self->{timeout},
+ Proto => 'tcp',
+ Type => SOCK_STREAM,
+ );
+ die "$0: socket connect failure: $!\n" unless defined $self->{sock};
+ return $self;
+}
+
+# =item hear
+#
+# hear collects a complete SMTP response and returns it with trailing
+# CRLF removed; for multi-line responses, intermediate CRLFs are left
+# intact. Returns undef if EOF is seen before a complete reply is
+# collected.
+#
+# =cut
+
+sub hear {
+ my ($self) = @_;
+ my ($tmp, $reply);
+ return undef unless $tmp = $self->{sock}->getline;
+ while ($tmp =~ /^\d{3}-/) {
+ $reply .= $tmp;
+ return undef unless $tmp = $self->{sock}->getline;
+ }
+ $reply .= $tmp;
+ $reply =~ s/\r\n$//;
+ return $reply;
+}
+
+# =item say("command text")
+#
+# say sends an SMTP command, appending CRLF.
+#
+# =cut
+
+sub say {
+ my ($self, @msg) = @_;
+ return unless @msg;
+ $self->{sock}->print("@msg", "\r\n") or die "$0: write error: $!";
+}
+
+# =item yammer(FILEHANDLE)
+#
+# yammer takes a filehandle (which should be positioned at the
+# beginning of the file, remember to $fh->seek(0,0) if you've just
+# written it) and sends its contents as the contents of DATA. This
+# should only be invoked after a $client->say("data") and a
+# $client->hear to collect the reply to the data command. It will send
+# the trailing "." as well. It will perform leading-dot-doubling in
+# accordance with the SMTP protocol spec, where "leading dot" is
+# defined in terms of CR-LF terminated lines --- i.e. the data should
+# contain CR-LF data without the leading-dot-quoting. The filehandle
+# will be left at EOF.
+#
+# =cut
+
+sub yammer {
+ my ($self, $fh) = (@_);
+ local (*_);
+ local ($/) = "\r\n";
+ $self->{sock}->autoflush(0); # use less writes (thx to Sam Horrocks for the tip)
+ while (<$fh>) {
+ s/^\./../;
+ $self->{sock}->print($_) or die "$0: write error: $!\n";
+ }
+ $self->{sock}->autoflush(1); # restore unbuffered socket operation
+ $self->{sock}->print(".\r\n") or die "$0: write error: $!\n";
+}
+
+1;
+
+
+################################################################################
+package SpamPD;
+
+use strict;
+use Net::Server::PreForkSimple;
+use IO::File;
+use Getopt::Long;
+use Mail::SpamAssassin;
+
+BEGIN {
+ # Load Time::HiRes if it's available
+ eval { require Time::HiRes };
+ Time::HiRes->import( qw(time) ) unless $@;
+
+ # use included modules
+ import SpamPD::Server;
+ import SpamPD::Client;
+}
+
+
+use vars qw(@ISA $VERSION);
+our @ISA = qw(Net::Server::PreForkSimple);
+our $VERSION = '2.2';
+
+sub process_message {
+ my ($self, $fh) = @_;
+
+ # output lists with a , delimeter by default
+ local ($") = ",";
+
+ # start a timer
+ my $start = time;
+ # use the assassin object created during startup
+ my $assassin = $self->{spampd}->{assassin};
+ my $sa_version = Mail::SpamAssassin::Version();
+
+ # this gets info about the message temp file
+ (my $dev,my $ino,my $mode,my $nlink,my $uid,
+ my $gid,my $rdev,my $size,
+ my $atime,my $mtime,my $ctime,
+ my $blksize,my $blocks) = $fh->stat or die "Can't stat mail file: $!";
+
+ # Only process message under --maxsize KB
+ if ( $size < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ my (@msglines, $msgid, $sender, $recips, $tmp, $mail, $msg_resp);
+
+ ## read message into array of lines to feed to SA
+ my $inhdr=1;
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ # loop over message file content
+ while (<$fh>) {
+ $inhdr = 0 if (/^\r?\n$/); # outside of msg header after first blank line
+ push(@msglines, $_);
+ # find the Message-ID for logging (code is mostly from spamd)
+ if ( $inhdr && /^Message-Id:\s+(.*?)\s*$/i ) {
+ $msgid = $1;
+ while ( $msgid =~ s/\([^\(\)]*\)// ) { } # remove comments and
+ $msgid =~ s/^\s+|\s+$//g; # leading and trailing spaces
+ $msgid =~ s/\s+/ /g; # collapse whitespaces
+ $msgid =~ s/^.*?<(.*?)>.*$/$1/; # keep only the id itself
+ $msgid =~ s/[^\x21-\x7e]/?/g; # replace all weird chars
+ $msgid =~ s/[<>]/?/g; # plus all dangling angle brackets
+ $msgid =~ s/%/%%/g; # escape % because Sys::Syslog uses sprintf()
+ $msgid =~ s/^(.+)$/<$1>/; # re-bracket the id (if not empty)
+ }
+ }
+
+ $recips = "@{$self->{smtp_server}->{to}}";
+ if ("$self->{smtp_server}->{from}" =~ /(\<.*?\>)/ ) {$sender = $1;}
+ $msgid ||= "(unknown)";
+ $recips ||= "(unknown)";
+ $sender ||= "(unknown)";
+
+ $self->log(2, "processing message $msgid for ". $recips);
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Timed out!\n" };
+ # save previous timer and start new
+ my $previous_alarm = alarm($self->{spampd}->{satimeout});
+
+ # Audit the message
+ if ($sa_version >= 3) {
+ $mail = $assassin->parse(\@msglines, 0);
+ undef @msglines; #clear some memory-- this screws up SA < v3
+ } elsif ($sa_version >= 2.70) {
+ $mail = Mail::SpamAssassin::MsgParser->parse(\@msglines);
+ } else {
+ $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines );
+ }
+
+
+ # Check spamminess (returns Mail::SpamAssassin:PerMsgStatus object)
+ my $status = $assassin->check($mail);
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Returned from checking by SpamAssassin"); }
+
+ # Rewrite mail if high spam factor or options --tagall
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Rewriting mail using SpamAssassin"); }
+
+ # use Mail::SpamAssassin:PerMsgStatus object to rewrite message
+ if ( $sa_version >= 3 ) {
+ $msg_resp = $status->rewrite_mail;
+ } else {
+ # SA versions prior to 3 need to get the response in a different manner
+ $status->rewrite_mail;
+ $msg_resp = join '', $mail->header, "\r\n", @{$mail->body};
+ }
+
+ # Build the new message to relay
+ # pause the timeout alarm while we do this (no point in timing
+ # out here and leaving a half-written file).
+ my @resplines = split(/\r?\n/, $msg_resp);
+ my $pause_alarm = alarm(0);
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ $fh->truncate(0) or die "Can't truncate message file: $!";
+ my $arraycont = @resplines;
+ for ( 0..$arraycont ) {
+ $fh->print($resplines[$_] . "\r\n")
+ or die "Can't print to message file: $!";
+ }
+ #restart the alarm
+ alarm($pause_alarm);
+
+ }
+
+ # Log what we did
+ my $was_it_spam = 'clean message';
+ if ($status->is_spam) { $was_it_spam = 'identified spam'; }
+ my $msg_score = sprintf("%.2f",$status->get_hits);
+ my $msg_threshold = sprintf("%.2f",$status->get_required_hits);
+ my $proc_time = sprintf("%.2f", time - $start);
+
+ $self->log(2, "$was_it_spam $msgid ($msg_score/$msg_threshold) from $sender for ".
+ "$recips in ". $proc_time . "s, $size bytes.");
+
+ # thanks to Kurt Andersen for this idea
+ if ( $self->{spampd}->{rh} ) {
+ $self->log(2, "rules hit for $msgid: " . $status->get_names_of_tests_hit); }
+
+ $status->finish();
+
+ # set the timeout alarm back to wherever it was at
+ alarm($previous_alarm);
+
+ };
+
+ if ( $@ ne '' ) {
+ $self->log(1, "WARNING!! SpamAssassin error on message $msgid: $@");
+ return 0;
+ }
+
+ } else {
+
+ $self->log(2, "skipped large message (". $size / 1024 ."KB)");
+
+ }
+
+ return 1;
+
+}
+
+sub process_request {
+ my $self = shift;
+ my $msg;
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Child server process timed out!\n" };
+ my $timeout = $self->{spampd}->{childtimeout};
+
+ # start a timeout alarm
+ alarm($timeout);
+
+ # start an smtp server
+ my $smtp_server = SpamPD::Server->new($self->{server}->{client});
+ unless ( defined $smtp_server ) {
+ die "Failed to create listening Server: $!"; }
+
+ $self->{smtp_server} = $smtp_server;
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Server"); }
+
+ # start an smtp "client" (really a sending server)
+ my $client = SpamPD::Client->new(interface => $self->{spampd}->{relayhost},
+ port => $self->{spampd}->{relayport});
+ unless ( defined $client ) {
+ die "Failed to create sending Client: $!"; }
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Client"); }
+
+ # pass on initial client response
+ # $client->hear can handle multiline responses so no need to loop
+ $smtp_server->ok($client->hear)
+ or die "Error in initial server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # while loop over incoming data from the server
+ while ( my $what = $smtp_server->chat ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # until end of DATA is sent, just pass the commands on transparently
+ if ($what ne '.') {
+
+ $client->say($what)
+ or die "Failure in client->say(what): $!";
+
+ # but once the data is sent now we want to process it
+ } else {
+
+ # spam checking routine - message might be rewritten here
+ my $pmrescode = $self->process_message($smtp_server->{data});
+
+ # pass on the messsage if exit code <> 0 or die-on-sa-errors flag is off
+ if ( $pmrescode or !$self->{spampd}->{dose} ) {
+
+ # need to give the client a rewound file
+ $smtp_server->{data}->seek(0,0)
+ or die "Can't rewind mail file: $!";
+
+ # now send the data on through the client
+ $client->yammer($smtp_server->{data})
+ or die "Failure in client->yammer(smtp_server->{data}): $!";
+
+ } else {
+
+ $smtp_server->ok("450 Temporary failure processing message, please try again later");
+ last;
+ }
+
+ #close the temp file
+ $smtp_server->{data}->close
+ or $self->log(1, "WARNING!! Couldn't close smtp_server->{data} temp file: $!");
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Finished sending DATA"); }
+ }
+
+ # pass on whatever the relayhost said in response
+ # $client->hear can handle multiline responses so no need to loop
+ my $destresp = $client->hear;
+ $smtp_server->ok($destresp)
+ or die "Error in server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Destination response: '" . $destresp . "'"); }
+
+ # if we're in data state but the response is an error, exit data state.
+ # Shold not normally occur, but can happen. Thanks to Rodrigo Ventura for bug reports.
+ if ( $smtp_server->{state} =~ /^data/i and $destresp =~ /^[45]\d{2} / ) {
+ $smtp_server->{state} = "err_after_data";
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Destination response indicates error after DATA command"); }
+ }
+
+ # restart the timeout alarm
+ alarm($timeout);
+
+ } # server ends connection
+
+ # close connections
+ $client->{sock}->close
+ or die "Couldn't close client->{sock}: $!";
+ $smtp_server->{sock}->close
+ or die "Couldn't close smtp_server->{sock}: $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Closed connections"); }
+
+ }; # end eval block
+
+ alarm(0); # stop the timer
+ # check for error in eval block
+ if ($@ ne '') {
+ chomp($@);
+ $msg = "WARNING!! Error in process_request eval block: $@";
+ $self->log(0, $msg);
+ die ($msg . "\n");
+ }
+
+ $self->{spampd}->{instance}++;
+
+}
+
+# Net::Server hook
+# about to exit child process
+sub child_finish_hook {
+ my($self) = shift;
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Exiting child process after handling ".
+ $self->{spampd}->{instance} ." requests"); }
+}
+
+
+my $relayhost = '127.0.0.1'; # relay to ip
+my $relayport = 25; # relay to port
+my $host = '127.0.0.1'; # listen on ip
+my $port = 10025; # listen on port
+my $children = 5; # number of child processes (servers) to spawn at start
+# my $maxchildren = $children; # max. number of child processes (servers) to spawn
+my $maxrequests = 20; # max requests handled by child b4 dying
+my $childtimeout = 6*60; # child process per-command timeout in seconds
+my $satimeout = 285; # SpamAssassin timeout in seconds (15s less than Postfix
+ # default for smtp_data_done_timeout)
+my $pidfile = '/var/run/spampd.pid'; # write pid to file
+my $user = 'mail'; # user to run as
+my $group = 'mail'; # group to run as
+my $tagall = 0; # mark-up all msgs with SA, not just spam
+my $maxsize = 64; # max. msg size to scan with SA, in KB.
+my $rh = 0; # log which rules were hit
+my $debug = 0; # debug flag
+my $dose = 0; # die-on-sa-errors flag
+my $logsock = "unix"; # default log socket (some systems like 'inet')
+my $nsloglevel = 2; # default log level for Net::Server (in the range 0-4)
+my $background = 1; # specifies whether to 'daemonize' and fork into background;
+ # apparently useful under Win32/cygwin to disable this via --nodetach option;
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ relayport => \$relayport,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ childtimeout => \$childtimeout,
+ satimeout => \$satimeout,
+ children => \$children,
+ # maxchildren => \$maxchildren,
+ logsock => \$logsock,
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'relayport=i',
+ 'children|c=i',
+ # 'maxchildren|mc=i',
+ 'maxrequests|mr=i',
+ 'childtimeout=i',
+ 'satimeout=i',
+ 'dead-letters=s', # deprecated
+ 'user|u=s',
+ 'group|g=s',
+ 'pid|p=s',
+ 'maxsize=i',
+ 'heloname=s', # deprecated
+ 'tagall|a',
+ 'auto-whitelist|aw',
+ 'stop-at-threshold', # deprecated
+ 'debug|d',
+ 'help|h',
+ 'local-only|l',
+ 'log-rules-hit|rh',
+ 'dose',
+ 'add-sc-header|ash', # deprecated
+ 'hostname=s', # deprecated
+ 'logsock=s',
+ 'nodetach'
+ );
+
+usage(0) if $options{help};
+if ( $logsock !~ /^(unix|inet)$/ ) {
+ print "--logsock parameter needs to be either unix or inet\n\n";
+ usage(0);
+}
+
+if ( $options{tagall} ) { $tagall = 1; }
+if ( $options{'log-rules-hit'} ) { $rh = 1; }
+if ( $options{debug} ) { $debug = 1; $nsloglevel = 4; }
+if ( $options{dose} ) { $dose = 1; }
+if ( $options{'nodetach'} ) { $background = undef; }
+# if ( !$options{maxchildren} or $maxchildren < $children ) { $maxchildren = $children; }
+
+if ( $children < 1 ) { print "Option --children must be greater than zero!\n"; exit shift; }
+
+# my $min_spare_servers = ($children == $maxchildren) ? 0 : 1;
+# my $max_spare_servers = ($min_spare_servers == 0) ? 0 : $maxchildren-1;
+
+my @tmp = split (/:/, $relayhost);
+$relayhost = $tmp[0];
+if ( $tmp[1] ) { $relayport = $tmp[1]; }
+
+@tmp = split (/:/, $host);
+$host = $tmp[0];
+if ( $tmp[1] ) { $port = $tmp[1]; }
+
+my $assassin = Mail::SpamAssassin->new({
+ 'dont_copy_prefs' => 1,
+ 'debug' => $debug,
+ 'local_tests_only' => $options{'local-only'} || 0 });
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now();
+
+# thanks to Kurt Andersen for the 'uname -s' fix
+if ( !$options{logsock} ) {
+ eval {
+ my $osname = `uname -s`;
+ if (($osname =~ 'HP-UX') || ($osname =~ 'SunOS')) {
+ $logsock = "inet";
+ }
+ };
+}
+
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ log_level => $nsloglevel,
+ syslog_logsock => $logsock,
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => $background,
+ # setsid => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ max_servers => $children,
+ max_requests => $maxrequests,
+ # min_servers => $children,
+ # max_servers => $maxchildren,
+ # min_spare_servers => $min_spare_servers,
+ # max_spare_servers => $max_spare_servers,
+ },
+ spampd => { relayhost => $relayhost,
+ relayport => $relayport,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ childtimeout => $childtimeout,
+ satimeout => $satimeout,
+ rh => $rh,
+ debug => $debug,
+ dose => $dose,
+ instance => 0,
+ },
+ }, 'SpamPD';
+
+# Redirect all warnings to Server::log
+$SIG{__WARN__} = sub { $server->log (2, $_[0]); };
+
+# call Net::Server to start up the daemon inside
+$server->run;
+
+exit 1; # shouldn't get here
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --host=host[:port] Hostname/IP and optional port to listen on.
+ Default is 127.0.0.1 port 10025
+ --port=n Port to listen on (alternate syntax to above).
+ --relayhost=host[:port] Host to relay mail to.
+ Default is 127.0.0.1 port 25.
+ --relayport=n Port to relay to (alternate syntax to above).
+
+ --children=n Number of child processes (servers) to start and
+ keep running. Default is 5 (plus 1 parent proc).
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Default is 20.
+ --childtimeout=n Time out children after this many seconds during
+ transactions (each S/LMTP command including the
+ time it takes to send the data).
+ Default is 360 seconds (6min).
+ --satimeout=n Time out SpamAssassin after this many seconds.
+ Default is 285 seconds.
+
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+ --nodetach Don't detach from the console and fork into
+ background. Useful for some daemon control
+ tools or when running as a win32 service
+ under cygwin.
+
+ --logsock=inet or unix Allows specifying the syslog socket type. Default is
+ 'unix' except on HPUX and SunOS which prefer 'inet'.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --dose (d)ie (o)n (s)pamAssassin (e)rrors. If this is
+ specified and SA times out or throws an error,
+ the mail will be rejected with a 450 temporary
+ error message. Default is to pass through email
+ even in the event of an SA problem.
+ --tagall Tag all messages with SA headers, not just spam.
+ --log-rules-hit or --rh Log the name of each SA test which matched the
+ current message.
+
+ --auto-whitelist or --aw Use the SA global auto-whitelist feature
+ (SA versions => 3.0 now control this via local.cf).
+ --local-only or -L Turn off all SA network-based tests (RBL, Razor, etc).
+ --debug or -d Turn on SA debugging (sent to log file).
+
+ --help or -h This message
+
+Deprecated Options (still accepted for backwards compatibility):
+ --heloname=hostname No longer used in spampd v.2
+ --dead-letters=path No longer used in spampd v.2
+ --stop-at-threshold No longer implemented in SpamAssassin
+EOF
+
+# --maxchildren=n Maximum number of child processes (servers) to
+# run. Default is the value of --children.
+
+ exit shift;
+}
+
+__END__
+
+# Some commented-out documentation. POD doesn't have a way to comment
+# out sections!? This documents a feature which may be implemented later.
+#
+# =item B<--maxchildren=n> or B<--mc=n> C<(new in v2)>
+#
+# Maximum number of children to spawn if needed (where n >= --children). When
+# I<spampd> starts it will spawn a number of child servers as specified by
+# --children. If all those servers become busy, a new child is spawned up to the
+# number specified in --maxchildren. Default is to have --maxchildren equal to
+# --children so extra child processes aren't started. Also see the --children
+# option, above. You may want to set your origination mail server to limit the
+# number of concurrent connections to I<spampd> to match this setting (for
+# Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+# 'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+#
+# Note that extra servers after the initial --children will only spawn on very
+# busy systems. This is because the check to see if a new server is needed (ie.
+# all current ones are busy) is only done around once per minute (this is
+# controlled by the Net::Server::PreFork module, in case you want to
+# hack at it :). It can still be useful as an "overflow valve," and is
+# especially nice since the extra child servers will die off once they're not
+# needed.
+
+=pod
+
+=head1 NAME
+
+SpamPD - Spam Proxy Daemon (version 2.2)
+
+=head1 Synopsis
+
+B<spampd>
+[B<--host=host[:port]>]
+[B<--relayhost=hostname[:port]>]
+[B<--user|u=username>]
+[B<--group|g=groupname>]
+[B<--children|c=n>]
+#[B<--maxchildren|mc=n>]
+[B<--maxrequests=n>]
+[B<--childtimeout=n>]
+[B<--satimeout=n>]
+[B<--pid|p=filename>]
+[B<--nodetach>]
+[B<--logsock=inet|unix>]
+[B<--maxsize=n>]
+[B<--dose>]
+[B<--tagall|a>]
+[B<--log-rules-hit|rh>]
+[B<--auto-whitelist|aw>]
+[B<--local-only|L>]
+[B<--debug|d>]
+
+B<spampd> B<--help>
+
+=head1 Description
+
+I<spampd> is an SMTP/LMTP proxy that marks (or tags) spam using
+SpamAssassin (http://www.SpamAssassin.org/). The proxy is designed
+to be transparent to the sending and receiving mail servers and at no point
+takes responsibility for the message itself. If a failure occurs within
+I<spampd> (or SpamAssassin) then the mail servers will disconnect and the
+sending server is still responsible for retrying the message for as long
+as it is configured to do so.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+The latest version can be found at
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>.
+
+=head1 Requires
+
+=over 5
+
+Perl modules:
+
+=item B<Mail::SpamAssassin>
+
+=item B<Net::Server::PreForkSimple>
+
+=item B<IO::File>
+
+=item B<IO::Socket>
+
+=item B<Time::HiRes> (not actually required but recommended)
+
+=back
+
+=head1 Operation
+
+I<spampd> is meant to operate as an S/LMTP mail proxy which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing! It is very easy to set up an open relay this
+way.
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+
+B<Running between firewall/gateway and internal mail server>
+
+=over 3
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+ Internet -> [ MX gateway (@inter.net.host:25) ->
+ spampd (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+=back
+
+B<Using Postfix advanced content filtering>
+
+=over 3
+
+Please see the F<FILTER_README> that came with the Postfix distribution. You
+need to have a version of Postfix which supports this (ideally v.2 and up).
+
+ Internet -> [ Postfix (@inter.net.host:25) ->
+ spampd (@localhost:10025) ->
+ Postfix (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+=head1 Upgrading
+
+If upgrading from a version prior to 2.2, please note that the --add-sc-header
+option is no longer supported. Use SAs built-in header manipulation features
+instead (as of SA v2.6).
+
+Upgrading from version 1 simply involves replacing the F<spampd> program file
+with the latest one. Note that the I<dead-letters> folder is no longer being
+used and the --dead-letters option is no longer needed (though no errors are
+thrown if it's present). Check the L<"Options"> list below for a full list of new
+and deprecated options. Also be sure to check out the change log.
+
+=head1 Installation
+
+I<spampd> can be run directly from the command prompt if desired. This is
+useful for testing purposes, but for long term use you probably want to put
+it somewhere like /usr/bin or /usr/local/bin and execute it at system startup.
+For example on Red Hat-style Linux system one can use a script in
+/etc/rc.d/init.d to start I<spampd> (a sample script is available on the
+I<spampd> Web page @ http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm).
+
+The options all have reasonable defaults, especially for a Postfix-centric
+installation. You may want to specify the --children option if you have an
+especially beefy or weak server box because I<spampd> is a memory-hungry
+program. Check the L<"Options"> for details on this and all other parameters.
+
+Note that I<spampd> B<replaces> I<spamd> from the I<SpamAssassin> distribution
+in function. You do not need to run I<spamd> in order for I<spampd> to work.
+This has apparently been the source of some confusion, so now you know.
+
+=head2 Postfix-specific Notes
+
+Here is a typical setup for Postfix "advanced" content filtering as described
+in the F<FILTER_README> that came with the Postfix distribution (which you
+really need to read):
+
+F</etc/postfix/master.cf>:
+
+ smtp inet n - y - - smtpd
+ -o content_filter=smtp:localhost:10025
+ -o myhostname=mx.example.com
+
+ localhost:10026 inet n - n - 10 smtpd
+ -o content_filter=
+ -o myhostname=mx-int.example.com
+
+The first entry is the main public-facing MTA which uses localhost:10025
+as the content filter for all mail. The second entry receives mail from
+the content filter and does final delivery. Both smtpd instances use
+the same Postfix F<main.cf> file. I<spampd> is the process that listens on
+localhost:10025 and then connects to the Postfix listener on localhost:10026.
+Note that the C<myhostname> options must be different between the two instances,
+otherwise Postfix will think it's talking to itself and abort sending.
+
+For the above example you can simply start I<spampd> like this:
+
+ spampd --host=localhost:10025 --relayhost=localhost:10026
+
+F<FILTER_README> from the Postfix distro has more details and examples of
+various setups, including how to skip the content filter for outbound mail.
+
+Another tip for Postfix when considering what timeout values to use for
+--childtimout and --satimeout options is the following command:
+
+C<# postconf | grep timeout>
+
+This will return a list of useful timeout settings and their values. For
+explanations see the relevant C<man> page (smtp, smtpd, lmtp). By default
+I<spampd> is set up for the default Postfix timeout values.
+
+=head1 Options
+
+=over 5
+
+=item B<--host=ip[:port] or hostname[:port]> C<(changed in v2)>
+
+Specifies what hostname/IP and port I<spampd> listens on. By default, it listens
+on 127.0.0.1 (localhost) on port 10025.
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025. This is an alternate to using the above --host=ip:port notation.
+
+=item B<--relayhost=ip[:port] or hostname[:port]>
+
+Specifies the hostname/IP where I<spampd> will relay all
+messages. Defaults to 127.0.0.1 (localhost). If the port is not provided, that
+defaults to 25.
+
+=item B<--relayport=n> C<(new in v2)>
+
+Specifies what port I<spampd> will relay to. Default is 25. This is an
+alternate to using the above --relayhost=ip:port notation.
+
+=item B<--user=username> or B<--u=username>
+
+=item B<--group=groupname> or B<--g=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--children=n> or B<--c=n> C<(new in v2)>
+
+Number of child servers to start and maintain (where n > 0). Each child will
+process up to --maxrequests (below) before exiting and being replaced by
+another child. Keep this number low on systems w/out a lot of memory.
+Default is 5 (which seems OK on a 512MB lightly loaded system). Note that
+there is always a parent process running, so if you specify 5 children you
+will actually have 6 I<spampd> processes running.
+
+You may want to set your origination mail server to limit the
+number of concurrent connections to I<spampd> to match this setting (for
+Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--childtimeout=n> C<(new in v2)>
+
+This is the number of seconds to allow each child server before it times out
+a transaction. In an S/LMTP transaction the timer is reset for every command.
+This timeout includes time it would take to send the message data, so it should
+not be too short. Note that it's more likely the origination or destination
+mail servers will timeout first, which is fine. This is just a "sane" failsafe.
+Default is 360 seconds (6 minutes).
+
+=item B<--satimeout=n> C<(new in v2)>
+
+This is the number of seconds to allow for processing a message with
+SpamAssassin (including feeding it the message, analyzing it, and adding
+the headers/report if necessary).
+This should be less than your origination and destination servers' timeout
+settings for the DATA command. For Postfix the default is 300 seconds in both
+cases (smtp_data_done_timeout and smtpd_timeout). In the event of timeout
+while processing the message, the problem is logged and the message is passed
+on anyway (w/out spam tagging, obviously). To fail the message with a temp
+450 error, see the --dose (die-on-sa-errors) option, below.
+Default is 285 seconds.
+
+=item B<--pid=filename> or B<--p=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--logsock=unix or inet> C<(new in v2.2)>
+
+Syslog socket to use. May be either "unix" of "inet". Default is "unix"
+except on HP-UX and SunOS (Solaris) systems which seem to prefer "inet".
+
+=item B<--nodetach> C<(new in v2.2)>
+
+If this option is given spampd won't detach from the console and fork into the
+background. This can be useful for running under control of some daemon
+management tools or when configured as a win32 service under cygrunsrv's
+control.
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KBytes. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. The size includes headers and attachments (if any).
+
+=item B<--dose> C<(new in v2)>
+
+Acronym for (d)ie (o)n (s)pamAssassin (e)rrors. By default if I<spampd>
+encounters a problem with processing the message through Spam Assassin (timeout
+or other error), it will still pass the mail on to the destination server. If
+you specify this option however, the mail is instead rejected with a temporary
+error (code 450, which means the origination server should keep retrying to send
+it). See the related --satimeout option, above.
+
+=item B<--tagall> or B<--a>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings). Note that
+for this option to work as of SA-2.50, the I<always_add_report> and/or
+I<always_add_headers> settings in your SpamAssassin F<local.cf> need to be
+set to 1/true.
+
+=item B<--log-rules-hit> or B<--rh> C<(new in v2)>
+
+Logs the names of each SpamAssassin rule which matched the message being
+processed. This list is returned by SA.
+
+=item B<--auto-whitelist> or B<--aw>
+
+This option is no longer relevant with SA version 3.0 and above, which
+controls auto whitelist use via local.cf settings.
+
+For SA version < 3.0, turns on the SpamAssassin global whitelist feature.
+See the SA docs. Note that per-user whitelists are not available.
+
+=item B<--local-only> or B<--L> C<(new in v2)>
+
+Turn off all SA network-based tests (DNS, Razor, etc).
+
+=item B<--debug> or B<--d> C<(changed in v2)>
+
+Turns on SpamAssassin debug messages which print to the system mail log
+(same log as spampd will log to). Also turns on more verbose logging of
+what spampd is doing (new in v2). Also increases log level of Net::Server
+to 4 (debug), adding more yet info (but not too much) (new in v2.2).
+
+=item B<--help> or B<--h>
+
+Prints usage information.
+
+=back
+
+=head2 Deprecated Options
+
+=over 5
+
+The following options are no longer used but still accepted for backwards
+compatibility with prevoius I<spampd> versions:
+
+=item B<--dead-letters>
+
+=item B<--heloname>
+
+=item B<--stop-at-threshold>
+
+=item B<--add-sc-header>
+
+=item B<--hostname>
+
+=back
+
+=head1 Examples
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 Credits
+
+I<spampd> is written and maintained by Maxim Paperno <MPaperno@WorldDesign.com>.
+See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info.
+
+I<spampd> v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+Stanley Dean Witter. These are distributed under the GNU GPL (see
+module code for more details). Both modules have been slightly modified
+from the originals and are included in this file under new names.
+
+Also thanks to Bennett Todd for the example smtpproxy script which helped create
+this version of I<spampd>. See http://bent.latency.net/smtpprox/ .
+
+I<spampd> v1 was based on code by Dave Carrigan named I<assassind>. Trace
+amounts of his code or documentation may still remain. Thanks to him for the
+original inspiration and code. See http://www.rudedog.org/assassind/ .
+
+Also thanks to I<spamd> (included with SpamAssassin) and
+I<amavisd-new> (http://www.ijs.si/software/amavisd/) for some tricks.
+
+Various people have contributed patches, bug reports, and ideas, all of whom
+I would like to thank. I have tried to include credits in code comments and
+in the change log, as appropriate.
+
+=head1 Copyright, License, and Disclaimer
+
+I<spampd> is Copyright (c) 2002 by World Design Group and Maxim Paperno.
+
+Portions are Copyright (C) 2001 Morgan Stanley Dean Witter as mentioned above
+in the Credits section.
+
+ 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.
+
+ The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+
+
+=head1 Bugs
+
+None known. Please report any to MPaperno@WorldDesign.com.
+
+=head1 To Do
+
+Figure out how to use Net::Server::PreFork because it has cool potential for
+load management. I tried but either I'm missing something or PreFork is
+somewhat broken in how it works. If anyone has experience here, please let
+me know.
+
+Add configurable option for rejecting mail outright based on spam score.
+It would be nice to make this program safe enough to sit in front of a mail
+server such as Postfix and be able to reject mail before it enters our systems.
+The only real problem is that Postfix will see localhost as the connecting
+client, so that disables any client-based checks Postfix can do and creates a
+possible relay hole if localhost is trusted.
+
+Per-user preferences: The jury is still out on this one. I'm thinking more
+and more that most per-user prefs should be specified on the final mailbox
+server. Why? Because SMTP isn't designed with per-user preferences in mind.
+On a relay server, the same message body can go to multiple recipients who
+may have wildly different preferences when it comes to handilng junk mail. The
+exception here might be the use of LMTP protocol, which bears further
+investigation.
+
+=head1 See Also
+
+perl(1), Spam::Assassin(3), L<http://www.spamassassin.org/>,
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>
diff --git a/previous-versions/spampd-2.30.pl b/previous-versions/spampd-2.30.pl
new file mode 100644
index 0000000..5ac393d
--- /dev/null
+++ b/previous-versions/spampd-2.30.pl
@@ -0,0 +1,1506 @@
+#!/usr/bin/perl -T
+
+######################
+# SpamPD - spam proxy daemon
+#
+# v2.30 - 31-Oct-05
+# v2.21 - 23-Oct-05
+# v2.20 - 05-Oct-04
+# v2.13 - 24-Nov-03
+# v2.12 - 15-Nov-03
+# v2.11 - 15-Jul-03
+# v2.10 - 01-Jul-03
+# v2.00 - 10-Jun-03
+# v1.0.2 - 13-Apr-03
+# v1.0.1 - 03-Feb-03
+# v1.0.0 - May 2002
+#
+# spampd is Copyright (c) 2002 by World Design Group and Maxim Paperno
+# (see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# Written and maintained by Maxim Paperno (MPaperno@WorldDesign.com)
+#
+# 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.
+#
+# The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+#
+# spampd v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+# Stanley Dean Witter. These are also distributed under the GNU GPL (see
+# module code for more details). Both modules have been slightly modified
+# from the originals and are included in this file under new names.
+#
+# spampd v1 was based on code by Dave Carrigan named assassind. Trace amounts
+# of his code or documentation may still remain. Thanks to him for the
+# original inspiration and code. (see http://www.rudedog.org/assassind/)
+#
+######################
+
+
+################################################################################
+package SpamPD::Server;
+
+# Originally known as MSDW::SMTP::Server
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =item DESCRIPTION
+#
+# This server simply gathers the SMTP acquired information (envelope
+# sender and recipient, and data) into unparsed memory buffers (or a
+# file for the data), and returns control to the caller to explicitly
+# acknowlege each command or request. Since acknowlegement or failure
+# are driven explicitly from the caller, this module can be used to
+# create a robust SMTP content scanning proxy, transparent or not as
+# desired.
+#
+# =cut
+
+use strict;
+use IO::File;
+#use IO::Socket;
+
+# =item new(interface => $interface, port => $port);
+
+# The #interface and port to listen on must be specified. The interface
+# must be a valid numeric IP address (0.0.0.0 to listen on all
+# interfaces, as usual); the port must be numeric. If this call
+# succeeds, it returns a server structure with an open
+# IO::Socket::INET in it, ready to listen on. If it fails it dies, so
+# if you want anything other than an exit with an explanatory error
+# message, wrap the constructor call in an eval block and pull the
+# error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die.
+#
+# =cut
+
+sub new {
+
+# This now emulates Net::SMTP::Server::Client for use with Net::Server which
+# passes an already open socket.
+
+ my($this, $socket) = @_;
+
+ my $class = ref($this) || $this;
+ my $self = {};
+ $self->{sock} = $socket;
+
+ bless($self, $class);
+
+ die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+ $self->{state} = 'started';
+ return $self;
+
+# Original code, removed by MP for spampd use
+#
+# my ($this, @opts) = @_;
+# my $class = ref($this) || $this;
+# my $self = bless { @opts }, $class;
+# $self->{sock} = IO::Socket::INET->new(
+# LocalAddr => $self->{interface},
+# LocalPort => $self->{port},
+# Proto => 'tcp',
+# Type => SOCK_STREAM,
+# Listen => 65536,
+# Reuse => 1,
+# );
+# die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+# $self->{state} = 'just bound',
+# return $self;
+
+}
+
+# sub accept { }
+#
+# Removed by MP; not needed for spampd use
+#
+
+
+# =item chat;
+#
+# The chat method carries the SMTP dialogue up to the point where any
+# acknowlegement must be made. If chat returns true, then its return
+# value is the previous SMTP command. If the return value begins with
+# 'mail' (case insensitive), then the attribute 'from' has been filled
+# in, and may be checked; if the return value begins with 'rcpt' then
+# both from and to have been been filled in with scalars, and should
+# be checked, then either 'ok' or 'fail' should be called to accept
+# or reject the given sender/recipient pair. If the return value is
+# 'data', then the attributes from and to are populated; in this case,
+# the 'to' attribute is a reference to an anonymous array containing
+# all the recipients for this data. If the return value is '.', then
+# the 'data' attribute (which may be pre-populated in the "new" or
+# "accept" methods if desired) is a reference to a filehandle; if it's
+# created automatically by this module it will point to an unlinked
+# tmp file in /tmp. If chat returns false, the SMTP dialogue has been
+# completed and the socket closed; this server is ready to exit or to
+# accept again, as appropriate for the server style.
+#
+# The return value from chat is also remembered inside the server
+# structure in the "state" attribute.
+#
+# =cut
+
+sub chat {
+ my ($self) = @_;
+ local(*_);
+ if ($self->{state} !~ /^data/i) {
+ return 0 unless defined($_ = $self->_getline);
+ s/[\r\n]*$//;
+ $self->{state} = $_;
+ if (s/^.?he?lo\s+//i) { # mp: find helo|ehlo|lhlo
+ # mp: determine protocol (for future use)
+ if ( /^L/i ) {
+ $self->{proto} = "lmtp";
+ } elsif ( /^E/i ) {
+ $self->{proto} = "esmtp";
+ } else {
+ $self->{proto} = "smtp"; }
+ s/\s*$//;
+ s/\s+/ /g;
+ $self->{helo} = $_;
+ } elsif (s/^rset\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ } elsif (s/^mail\s+from:\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ s/\s*$//;
+ $self->{from} = $_;
+ } elsif (s/^rcpt\s+to:\s*//i) {
+ s/\s*$//; s/\s+/ /g;
+ $self->{to} = $_;
+ push @{$self->{recipients}}, $_;
+ } elsif (/^data/i) {
+ $self->{to} = $self->{recipients};
+ }
+ } else {
+ if (defined($self->{data})) {
+ $self->{data}->seek(0, 0);
+ $self->{data}->truncate(0);
+ } else {
+ $self->{data} = IO::File->new_tmpfile;
+ }
+ while (defined($_ = $self->_getline)) {
+ if ($_ eq ".\r\n") {
+ $self->{data}->seek(0,0);
+ return $self->{state} = '.';
+ }
+ s/^\.\./\./;
+ $self->{data}->print($_) or die "$0: write error saving data\n";
+ }
+ return(0);
+ }
+ return $self->{state};
+}
+
+# =item ok([message]);
+#
+# Approves of the data given to date, either the recipient or the
+# data, in the context of the sender [and, for data, recipients]
+# already given and available as attributes. If a message is given, it
+# will be sent instead of the internal default.
+#
+# =cut
+
+sub ok {
+ my ($self, @msg) = @_;
+ @msg = ("250 ok.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# =item fail([message]);
+#
+# Rejects the current info; if processing from, rejects the sender; if
+# processing 'to', rejects the current recipient; if processing data,
+# rejects the entire message. If a message is specified it means the
+# exact same thing as "ok" --- simply send that message to the sender.
+#
+# =cut
+
+sub fail {
+ my ($self, @msg) = @_;
+ @msg = ("550 no.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# utility functions
+
+sub _getline {
+ my ($self) = @_;
+ local ($/) = "\r\n";
+ my $tmp = $self->{sock}->getline;
+ if ( defined $self->{debug} ) {
+ $self->{debug}->print($tmp) if ($tmp);
+ }
+ return $tmp;
+}
+
+sub _print {
+ my ($self, @msg) = @_;
+ $self->{debug}->print(@msg) if defined $self->{debug};
+ $self->{sock}->print(@msg);
+}
+
+1;
+
+################################################################################
+package SpamPD::Client;
+
+# Originally known as MSDW::SMTP::Client
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =head1 DESCRIPTION
+#
+# MSDW::SMTP::Client provides a very lean SMTP client implementation;
+# the only protocol-specific knowlege it has is the structure of SMTP
+# multiline responses. All specifics lie in the hands of the calling
+# program; this makes it appropriate for a semi-transparent SMTP
+# proxy, passing commands between a talker and a listener.
+#
+# =cut
+
+use strict;
+use IO::Socket;
+
+# =item new(interface => $interface, port => $port[, timeout = 300]);
+#
+# The interface and port to talk to must be specified. The interface
+# must be a valid numeric IP address; the port must be numeric. If
+# this call succeeds, it returns a client structure with an open
+# IO::Socket::INET in it, ready to talk to. If it fails it dies,
+# so if you want anything other than an exit with an explanatory
+# error message, wrap the constructor call in an eval block and pull
+# the error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die. The timeout parameter is passed
+# on into the IO::Socket::INET constructor.
+#
+# =cut
+
+sub new {
+ my ($this, @opts) = @_;
+ my $class = ref($this) || $this;
+ my $self = bless { timeout => 300, @opts }, $class;
+ $self->{sock} = IO::Socket::INET->new(
+ PeerAddr => $self->{interface},
+ PeerPort => $self->{port},
+ Timeout => $self->{timeout},
+ Proto => 'tcp',
+ Type => SOCK_STREAM,
+ );
+ die "$0: socket connect failure: $!\n" unless defined $self->{sock};
+ return $self;
+}
+
+# =item hear
+#
+# hear collects a complete SMTP response and returns it with trailing
+# CRLF removed; for multi-line responses, intermediate CRLFs are left
+# intact. Returns undef if EOF is seen before a complete reply is
+# collected.
+#
+# =cut
+
+sub hear {
+ my ($self) = @_;
+ my ($tmp, $reply);
+ return undef unless $tmp = $self->{sock}->getline;
+ while ($tmp =~ /^\d{3}-/) {
+ $reply .= $tmp;
+ return undef unless $tmp = $self->{sock}->getline;
+ }
+ $reply .= $tmp;
+ $reply =~ s/\r\n$//;
+ return $reply;
+}
+
+# =item say("command text")
+#
+# say sends an SMTP command, appending CRLF.
+#
+# =cut
+
+sub say {
+ my ($self, @msg) = @_;
+ return unless @msg;
+ $self->{sock}->print("@msg", "\r\n") or die "$0: write error: $!";
+}
+
+# =item yammer(FILEHANDLE)
+#
+# yammer takes a filehandle (which should be positioned at the
+# beginning of the file, remember to $fh->seek(0,0) if you've just
+# written it) and sends its contents as the contents of DATA. This
+# should only be invoked after a $client->say("data") and a
+# $client->hear to collect the reply to the data command. It will send
+# the trailing "." as well. It will perform leading-dot-doubling in
+# accordance with the SMTP protocol spec, where "leading dot" is
+# defined in terms of CR-LF terminated lines --- i.e. the data should
+# contain CR-LF data without the leading-dot-quoting. The filehandle
+# will be left at EOF.
+#
+# =cut
+
+sub yammer {
+ my ($self, $fh) = (@_);
+ local (*_);
+ local ($/) = "\r\n";
+ $self->{sock}->autoflush(0); # use less writes (thx to Sam Horrocks for the tip)
+ while (<$fh>) {
+ s/^\./../;
+ $self->{sock}->print($_) or die "$0: write error: $!\n";
+ }
+ $self->{sock}->autoflush(1); # restore unbuffered socket operation
+ $self->{sock}->print(".\r\n") or die "$0: write error: $!\n";
+}
+
+1;
+
+
+################################################################################
+package SpamPD;
+
+use strict;
+use Net::Server::PreForkSimple;
+use IO::File;
+use Getopt::Long;
+use Mail::SpamAssassin;
+
+BEGIN {
+ # Load Time::HiRes if it's available
+ eval { require Time::HiRes };
+ Time::HiRes->import( qw(time) ) unless $@;
+
+ # use included modules
+ import SpamPD::Server;
+ import SpamPD::Client;
+}
+
+
+use vars qw(@ISA $VERSION);
+our @ISA = qw(Net::Server::PreForkSimple);
+our $VERSION = '2.30';
+
+sub process_message {
+ my ($self, $fh) = @_;
+
+ # output lists with a , delimeter by default
+ local ($") = ",";
+
+ # start a timer
+ my $start = time;
+ # use the assassin object created during startup
+ my $assassin = $self->{spampd}->{assassin};
+ my $sa_version = Mail::SpamAssassin::Version();
+ # $sa_version can have a non-numeric value if version_tag is
+ # set in local.cf. Only take first numeric value
+ $sa_version =~ s/([0-9]*\.[0-9]*).*/$1/;
+
+ # this gets info about the message temp file
+ (my $dev,my $ino,my $mode,my $nlink,my $uid,
+ my $gid,my $rdev,my $size,
+ my $atime,my $mtime,my $ctime,
+ my $blksize,my $blocks) = $fh->stat or die "Can't stat mail file: $!";
+
+ # Only process message under --maxsize KB
+ if ( $size < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ my (@msglines, $msgid, $sender, $recips, $tmp, $mail, $msg_resp);
+
+ my $inhdr = 1;
+ my $envfrom = 0;
+ my $envto = 0;
+ my $addedenvto = 0;
+
+ $recips = "@{$self->{smtp_server}->{to}}";
+ if ("$self->{smtp_server}->{from}" =~ /(\<.*?\>)/ ) {$sender = $1;}
+ $recips ||= "(unknown)";
+ $sender ||= "(unknown)";
+
+ ## read message into array of lines to feed to SA
+
+ # loop over message file content
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ while (<$fh>) {
+ $envto = 1 if (/^(?:X-)?Envelope-To: /);
+ $envfrom = 1 if (/^(?:X-)?Envelope-From: /);
+ if ( (/^\r?\n$/) && ($inhdr ==1) ) {
+ $inhdr = 0; # outside of msg header after first blank line
+ if ( ( $self->{spampd}->{envelopeheaders} ||
+ $self->{spampd}->{setenvelopefrom} ) &&
+ $envfrom == 0 ) {
+ push(@msglines, "X-Envelope-From: $sender\r\n");
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Added X-Envelope-From"); }
+ }
+ if ( $self->{spampd}->{envelopeheaders} && $envto == 0 ) {
+ push(@msglines, "X-Envelope-To: $recips\r\n");
+ $addedenvto = 1;
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Added X-Envelope-To"); }
+ }
+ }
+ push(@msglines, $_);
+ # find the Message-ID for logging (code is mostly from spamd)
+ if ( $inhdr && /^Message-Id:\s+(.*?)\s*$/i ) {
+ $msgid = $1;
+ while ( $msgid =~ s/\([^\(\)]*\)// ) { } # remove comments and
+ $msgid =~ s/^\s+|\s+$//g; # leading and trailing spaces
+ $msgid =~ s/\s+/ /g; # collapse whitespaces
+ $msgid =~ s/^.*?<(.*?)>.*$/$1/; # keep only the id itself
+ $msgid =~ s/[^\x21-\x7e]/?/g; # replace all weird chars
+ $msgid =~ s/[<>]/?/g; # plus all dangling angle brackets
+ $msgid =~ s/^(.+)$/<$1>/; # re-bracket the id (if not empty)
+ }
+ }
+
+ $msgid ||= "(unknown)";
+
+ $self->log(2, "%s", "processing message $msgid for ". $recips);
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Timed out!\n" };
+ # save previous timer and start new
+ my $previous_alarm = alarm($self->{spampd}->{satimeout});
+
+ # Audit the message
+ if ($sa_version >= 3) {
+ $mail = $assassin->parse(\@msglines, 0);
+ undef @msglines; #clear some memory-- this screws up SA < v3
+ } elsif ($sa_version >= 2.70) {
+ $mail = Mail::SpamAssassin::MsgParser->parse(\@msglines);
+ } else {
+ $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines );
+ }
+
+
+ # Check spamminess (returns Mail::SpamAssassin:PerMsgStatus object)
+ my $status = $assassin->check($mail);
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Returned from checking by SpamAssassin"); }
+
+ # Rewrite mail if high spam factor or options --tagall
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Rewriting mail using SpamAssassin"); }
+
+ # use Mail::SpamAssassin:PerMsgStatus object to rewrite message
+ if ( $sa_version >= 3 ) {
+ $msg_resp = $status->rewrite_mail;
+ } else {
+ # SA versions prior to 3 need to get the response in a different manner
+ $status->rewrite_mail;
+ $msg_resp = join '', $mail->header, "\r\n", @{$mail->body};
+ }
+
+ # Build the new message to relay
+ # pause the timeout alarm while we do this (no point in timing
+ # out here and leaving a half-written file).
+ my @resplines = split(/\r?\n/, $msg_resp);
+ my $pause_alarm = alarm(0);
+ my $inhdr = 1;
+ my $skipline = 0;
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ $fh->truncate(0) or die "Can't truncate message file: $!";
+ my $arraycont = @resplines;
+
+ for ( 0..($arraycont-1) ) {
+ $inhdr=0 if ($resplines[$_] =~ m/^\r?\n$/);
+
+ # if we are still in the header, skip over any
+ # "X-Envelope-To: " line if we have previously added it.
+ if ( $inhdr == 1 &&
+ $addedenvto == 1 &&
+ $resplines[$_] =~ m/^X-Envelope-To: .*$/) {
+ $skipline = 1;
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Removing X-Envelope-To"); }
+ }
+
+ if (! $skipline) {
+ $fh->print($resplines[$_] . "\r\n")
+ or die "Can't print to message file: $!";
+ } else {
+ $skipline = 0; }
+ }
+
+ #restart the alarm
+ alarm($pause_alarm);
+
+ }
+
+ # Log what we did
+ my $was_it_spam = 'clean message';
+ if ($status->is_spam) { $was_it_spam = 'identified spam'; }
+ my $msg_score = sprintf("%.2f",$status->get_hits);
+ my $msg_threshold = sprintf("%.2f",$status->get_required_hits);
+ my $proc_time = sprintf("%.2f", time - $start);
+
+ $self->log(2, "%s", "$was_it_spam $msgid ($msg_score/$msg_threshold) from $sender for ".
+ "$recips in ". $proc_time . "s, $size bytes.");
+
+ # thanks to Kurt Andersen for this idea
+ if ( $self->{spampd}->{rh} ) {
+ $self->log(2, "%s", "rules hit for $msgid: " . $status->get_names_of_tests_hit); }
+
+ $status->finish();
+
+ # set the timeout alarm back to wherever it was at
+ alarm($previous_alarm);
+
+ };
+
+ if ( $@ ne '' ) {
+ $self->log(1, "%s", "WARNING!! SpamAssassin error on message $msgid: $@");
+ return 0;
+ }
+
+ } else {
+
+ $self->log(2, "skipped large message (". $size / 1024 ."KB)");
+
+ }
+
+ return 1;
+
+}
+
+sub process_request {
+ my $self = shift;
+ my $msg;
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Child server process timed out!\n" };
+ my $timeout = $self->{spampd}->{childtimeout};
+
+ # start a timeout alarm
+ alarm($timeout);
+
+ # start an smtp server
+ my $smtp_server = SpamPD::Server->new($self->{server}->{client});
+ unless ( defined $smtp_server ) {
+ die "Failed to create listening Server: $!"; }
+
+ $self->{smtp_server} = $smtp_server;
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Server"); }
+
+ # start an smtp "client" (really a sending server)
+ my $client = SpamPD::Client->new(interface => $self->{spampd}->{relayhost},
+ port => $self->{spampd}->{relayport});
+ unless ( defined $client ) {
+ die "Failed to create sending Client: $!"; }
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Initiated Client"); }
+
+ # pass on initial client response
+ # $client->hear can handle multiline responses so no need to loop
+ $smtp_server->ok($client->hear)
+ or die "Error in initial server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # while loop over incoming data from the server
+ while ( my $what = $smtp_server->chat ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # until end of DATA is sent, just pass the commands on transparently
+ if ($what ne '.') {
+
+ $client->say($what)
+ or die "Failure in client->say(what): $!";
+
+ # but once the data is sent now we want to process it
+ } else {
+
+ # spam checking routine - message might be rewritten here
+ my $pmrescode = $self->process_message($smtp_server->{data});
+
+ # pass on the messsage if exit code <> 0 or die-on-sa-errors flag is off
+ if ( $pmrescode or !$self->{spampd}->{dose} ) {
+
+ # need to give the client a rewound file
+ $smtp_server->{data}->seek(0,0)
+ or die "Can't rewind mail file: $!";
+
+ # now send the data on through the client
+ $client->yammer($smtp_server->{data})
+ or die "Failure in client->yammer(smtp_server->{data}): $!";
+
+ } else {
+
+ $smtp_server->ok("450 Temporary failure processing message, please try again later");
+ last;
+ }
+
+ #close the temp file
+ $smtp_server->{data}->close
+ or $self->log(1, "%s", "WARNING!! Couldn't close smtp_server->{data} temp file: $!");
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Finished sending DATA"); }
+ }
+
+ # pass on whatever the relayhost said in response
+ # $client->hear can handle multiline responses so no need to loop
+ my $destresp = $client->hear;
+ $smtp_server->ok($destresp)
+ or die "Error in server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "%s", "Destination response: '" . $destresp . "'"); }
+
+ # if we're in data state but the response is an error, exit data state.
+ # Shold not normally occur, but can happen. Thanks to Rodrigo Ventura for bug reports.
+ if ( $smtp_server->{state} =~ /^data/i and $destresp =~ /^[45]\d{2} / ) {
+ $smtp_server->{state} = "err_after_data";
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Destination response indicates error after DATA command"); }
+ }
+
+ # restart the timeout alarm
+ alarm($timeout);
+
+ } # server ends connection
+
+ # close connections
+ $client->{sock}->close
+ or die "Couldn't close client->{sock}: $!";
+ $smtp_server->{sock}->close
+ or die "Couldn't close smtp_server->{sock}: $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Closed connections"); }
+
+ }; # end eval block
+
+ alarm(0); # stop the timer
+ # check for error in eval block
+ if ($@ ne '') {
+ chomp($@);
+ $msg = "WARNING!! Error in process_request eval block: $@";
+ $self->log(0, "%s", $msg);
+ die ($msg . "\n");
+ }
+
+ $self->{spampd}->{instance}++;
+
+}
+
+# Net::Server hook
+# about to exit child process
+sub child_finish_hook {
+ my($self) = shift;
+ if ( $self->{spampd}->{debug} ) {
+ $self->log(2, "Exiting child process after handling ".
+ $self->{spampd}->{instance} ." requests"); }
+}
+
+
+my $relayhost = '127.0.0.1'; # relay to ip
+my $relayport = 25; # relay to port
+my $host = '127.0.0.1'; # listen on ip
+my $port = 10025; # listen on port
+my $children = 5; # number of child processes (servers) to spawn at start
+# my $maxchildren = $children; # max. number of child processes (servers) to spawn
+my $maxrequests = 20; # max requests handled by child b4 dying
+my $childtimeout = 6*60; # child process per-command timeout in seconds
+my $satimeout = 285; # SpamAssassin timeout in seconds (15s less than Postfix
+ # default for smtp_data_done_timeout)
+my $pidfile = '/var/run/spampd.pid'; # write pid to file
+my $user = 'mail'; # user to run as
+my $group = 'mail'; # group to run as
+my $tagall = 0; # mark-up all msgs with SA, not just spam
+my $maxsize = 64; # max. msg size to scan with SA, in KB.
+my $rh = 0; # log which rules were hit
+my $debug = 0; # debug flag
+my $dose = 0; # die-on-sa-errors flag
+my $logsock = "unix"; # default log socket (some systems like 'inet')
+my $nsloglevel = 2; # default log level for Net::Server (in the range 0-4)
+my $background = 1; # specifies whether to 'daemonize' and fork into background;
+ # apparently useful under Win32/cygwin to disable this via --nodetach option;
+my $envelopeheaders = 0; # Set X-Envelope-To and X-Envelope-From headers in the mail before
+ # passing it to spamassassin. Set to 1 to enable this
+my $setenvelopefrom = 0; # Set X-Envelope-From header only
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ relayport => \$relayport,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ childtimeout => \$childtimeout,
+ satimeout => \$satimeout,
+ children => \$children,
+ # maxchildren => \$maxchildren,
+ logsock => \$logsock,
+ envelopeheaders => \$envelopeheaders,
+ setenvelopefrom => \$setenvelopefrom,
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'relayport=i',
+ 'children|c=i',
+ # 'maxchildren|mc=i',
+ 'maxrequests|mr=i',
+ 'childtimeout=i',
+ 'satimeout=i',
+ 'dead-letters=s', # deprecated
+ 'user|u=s',
+ 'group|g=s',
+ 'pid|p=s',
+ 'maxsize=i',
+ 'heloname=s', # deprecated
+ 'tagall|a',
+ 'auto-whitelist|aw',
+ 'stop-at-threshold', # deprecated
+ 'debug|d',
+ 'help|h|?',
+ 'local-only|l',
+ 'log-rules-hit|rh',
+ 'dose',
+ 'add-sc-header|ash', # deprecated
+ 'hostname=s', # deprecated
+ 'logsock=s',
+ 'nodetach',
+ 'set-envelope-headers|seh',
+ 'set-envelope-from|sef',
+ );
+
+usage(0) if $options{help};
+
+if ( $logsock !~ /^(unix|inet)$/ ) {
+ print "--logsock parameter needs to be either unix or inet\n\n";
+ usage(0);
+}
+
+if ( $options{tagall} ) { $tagall = 1; }
+if ( $options{'log-rules-hit'} ) { $rh = 1; }
+if ( $options{debug} ) { $debug = 1; $nsloglevel = 4; }
+if ( $options{dose} ) { $dose = 1; }
+if ( $options{'nodetach'} ) { $background = undef; }
+if ( $options{'set-envelope-headers'} ) { $envelopeheaders = 1; }
+if ( $options{'set-envelope-from'} ) { $setenvelopefrom = 1; }
+# if ( !$options{maxchildren} or $maxchildren < $children ) { $maxchildren = $children; }
+
+if ( $children < 1 ) { print "Option --children must be greater than zero!\n"; exit shift; }
+
+# my $min_spare_servers = ($children == $maxchildren) ? 0 : 1;
+# my $max_spare_servers = ($min_spare_servers == 0) ? 0 : $maxchildren-1;
+
+my @tmp = split (/:/, $relayhost);
+$relayhost = $tmp[0];
+if ( $tmp[1] ) { $relayport = $tmp[1]; }
+
+@tmp = split (/:/, $host);
+$host = $tmp[0];
+if ( $tmp[1] ) { $port = $tmp[1]; }
+
+my $assassin = Mail::SpamAssassin->new({
+ 'dont_copy_prefs' => 1,
+ 'debug' => $debug,
+ 'local_tests_only' => $options{'local-only'} || 0 });
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now(0);
+
+# thanks to Kurt Andersen for the 'uname -s' fix
+if ( !$options{logsock} ) {
+ eval {
+ my $osname = `uname -s`;
+ if (($osname =~ 'HP-UX') || ($osname =~ 'SunOS')) {
+ $logsock = "inet";
+ }
+ };
+}
+
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ log_level => $nsloglevel,
+ syslog_logsock => $logsock,
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => $background,
+ # setsid => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ max_servers => $children,
+ max_requests => $maxrequests,
+ # min_servers => $children,
+ # max_servers => $maxchildren,
+ # min_spare_servers => $min_spare_servers,
+ # max_spare_servers => $max_spare_servers,
+ },
+ spampd => { relayhost => $relayhost,
+ relayport => $relayport,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ childtimeout => $childtimeout,
+ satimeout => $satimeout,
+ rh => $rh,
+ debug => $debug,
+ dose => $dose,
+ instance => 0,
+ envelopeheaders => $envelopeheaders,
+ setenvelopefrom => $setenvelopefrom,
+ },
+ }, 'SpamPD';
+
+# Redirect all warnings to Server::log
+$SIG{__WARN__} = sub { $server->log (2, "%s", $_[0]); };
+
+# call Net::Server to start up the daemon inside
+$server->run;
+
+exit 1; # shouldn't get here
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --host=host[:port] Hostname/IP and optional port to listen on.
+ Default is 127.0.0.1 port 10025
+ --port=n Port to listen on (alternate syntax to above).
+ --relayhost=host[:port] Host to relay mail to.
+ Default is 127.0.0.1 port 25.
+ --relayport=n Port to relay to (alternate syntax to above).
+
+ --children=n Number of child processes (servers) to start and
+ keep running. Default is 5 (plus 1 parent proc).
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Default is 20.
+ --childtimeout=n Time out children after this many seconds during
+ transactions (each S/LMTP command including the
+ time it takes to send the data).
+ Default is 360 seconds (6min).
+ --satimeout=n Time out SpamAssassin after this many seconds.
+ Default is 285 seconds.
+
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+ --nodetach Don't detach from the console and fork into
+ background. Useful for some daemon control
+ tools or when running as a win32 service
+ under cygwin.
+
+ --logsock=inet or unix Allows specifying the syslog socket type. Default is
+ 'unix' except on HPUX and SunOS which prefer 'inet'.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --dose (d)ie (o)n (s)pamAssassin (e)rrors. If this is
+ specified and SA times out or throws an error,
+ the mail will be rejected with a 450 temporary
+ error message. Default is to pass through email
+ even in the event of an SA problem.
+ --tagall Tag all messages with SA headers, not just spam.
+ --log-rules-hit Log the name of each SA test which matched the
+ or --rh current message.
+
+ --set-envelope-headers Set X-Envelope-From and X-Envelope-To headers before
+ or --seh passing the mail to SpamAssassin. This is
+ disabled by default because it potentially leaks
+ information. NOTE: Please read the manpage before
+ enabling this!
+ --set-envelope-from Same as above but only sets X-Envelope-From, for
+ or --sef those that don't feel comfortable with the
+ potential information leak.
+
+ --auto-whitelist Use the SA global auto-whitelist feature
+ or --aw (SA versions => 3.0 now control this via local.cf).
+ --local-only or -L Turn off all SA network-based tests (RBL, Razor, etc).
+ --debug or -d Turn on SA debugging (sent to log file).
+
+ --help or -h or -? This message
+
+Deprecated Options (still accepted for backwards compatibility):
+ --heloname=hostname No longer used in spampd v.2
+ --dead-letters=path No longer used in spampd v.2
+ --stop-at-threshold No longer implemented in SpamAssassin
+EOF
+
+# --maxchildren=n Maximum number of child processes (servers) to
+# run. Default is the value of --children.
+
+ exit shift;
+}
+
+__END__
+
+# Some commented-out documentation. POD doesn't have a way to comment
+# out sections!? This documents a feature which may be implemented later.
+#
+# =item B<--maxchildren=n> or B<--mc=n>
+#
+# Maximum number of children to spawn if needed (where n >= --children). When
+# I<spampd> starts it will spawn a number of child servers as specified by
+# --children. If all those servers become busy, a new child is spawned up to the
+# number specified in --maxchildren. Default is to have --maxchildren equal to
+# --children so extra child processes aren't started. Also see the --children
+# option, above. You may want to set your origination mail server to limit the
+# number of concurrent connections to I<spampd> to match this setting (for
+# Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+# 'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+#
+# Note that extra servers after the initial --children will only spawn on very
+# busy systems. This is because the check to see if a new server is needed (ie.
+# all current ones are busy) is only done around once per minute (this is
+# controlled by the Net::Server::PreFork module, in case you want to
+# hack at it :). It can still be useful as an "overflow valve," and is
+# especially nice since the extra child servers will die off once they're not
+# needed.
+
+=pod
+
+=head1 NAME
+
+SpamPD - Spam Proxy Daemon (version 2.2)
+
+=head1 Synopsis
+
+B<spampd>
+[B<--host=host[:port]>]
+[B<--relayhost=hostname[:port]>]
+[B<--user|u=username>]
+[B<--group|g=groupname>]
+[B<--children|c=n>]
+#[B<--maxchildren|mc=n>]
+[B<--maxrequests=n>]
+[B<--childtimeout=n>]
+[B<--satimeout=n>]
+[B<--pid|p=filename>]
+[B<--nodetach>]
+[B<--logsock=inet|unix>]
+[B<--maxsize=n>]
+[B<--dose>]
+[B<--tagall|a>]
+[B<--log-rules-hit|rh>]
+[B<--set-envelope-headers|seh>]
+[B<--set-envelope-from|sef>]
+[B<--auto-whitelist|aw>]
+[B<--local-only|L>]
+[B<--debug|d>]
+
+B<spampd> B<--help>
+
+=head1 Description
+
+I<spampd> is an SMTP/LMTP proxy that marks (or tags) spam using
+SpamAssassin (http://www.SpamAssassin.org/). The proxy is designed
+to be transparent to the sending and receiving mail servers and at no point
+takes responsibility for the message itself. If a failure occurs within
+I<spampd> (or SpamAssassin) then the mail servers will disconnect and the
+sending server is still responsible for retrying the message for as long
+as it is configured to do so.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+The latest version can be found at
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>.
+
+=head1 Requires
+
+=over 5
+
+Perl modules:
+
+=item B<Mail::SpamAssassin>
+
+=item B<Net::Server::PreForkSimple>
+
+=item B<IO::File>
+
+=item B<IO::Socket>
+
+=item B<Time::HiRes> (not actually required but recommended)
+
+=back
+
+=head1 Operation
+
+I<spampd> is meant to operate as an S/LMTP mail proxy which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing! It is very easy to set up an open relay this
+way.
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+
+B<Running between firewall/gateway and internal mail server>
+
+=over 3
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+ Internet -> [ MX gateway (@inter.net.host:25) ->
+ spampd (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+=back
+
+B<Using Postfix advanced content filtering>
+
+=over 3
+
+Please see the F<FILTER_README> that came with the Postfix distribution. You
+need to have a version of Postfix which supports this (ideally v.2 and up).
+
+ Internet -> [ Postfix (@inter.net.host:25) ->
+ spampd (@localhost:10025) ->
+ Postfix (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+=head1 Upgrading
+
+If upgrading from a version prior to 2.2, please note that the --add-sc-header
+option is no longer supported. Use SAs built-in header manipulation features
+instead (as of SA v2.6).
+
+Upgrading from version 1 simply involves replacing the F<spampd> program file
+with the latest one. Note that the I<dead-letters> folder is no longer being
+used and the --dead-letters option is no longer needed (though no errors are
+thrown if it's present). Check the L<"Options"> list below for a full list of new
+and deprecated options. Also be sure to check out the change log.
+
+=head1 Installation
+
+I<spampd> can be run directly from the command prompt if desired. This is
+useful for testing purposes, but for long term use you probably want to put
+it somewhere like /usr/bin or /usr/local/bin and execute it at system startup.
+For example on Red Hat-style Linux system one can use a script in
+/etc/rc.d/init.d to start I<spampd> (a sample script is available on the
+I<spampd> Web page @ http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm).
+
+The options all have reasonable defaults, especially for a Postfix-centric
+installation. You may want to specify the --children option if you have an
+especially beefy or weak server box because I<spampd> is a memory-hungry
+program. Check the L<"Options"> for details on this and all other parameters.
+
+Note that I<spampd> B<replaces> I<spamd> from the I<SpamAssassin> distribution
+in function. You do not need to run I<spamd> in order for I<spampd> to work.
+This has apparently been the source of some confusion, so now you know.
+
+=head2 Postfix-specific Notes
+
+Here is a typical setup for Postfix "advanced" content filtering as described
+in the F<FILTER_README> that came with the Postfix distribution (which you
+really need to read):
+
+F</etc/postfix/master.cf>:
+
+ smtp inet n - y - - smtpd
+ -o content_filter=smtp:localhost:10025
+ -o myhostname=mx.example.com
+
+ localhost:10026 inet n - n - 10 smtpd
+ -o content_filter=
+ -o myhostname=mx-int.example.com
+
+The first entry is the main public-facing MTA which uses localhost:10025
+as the content filter for all mail. The second entry receives mail from
+the content filter and does final delivery. Both smtpd instances use
+the same Postfix F<main.cf> file. I<spampd> is the process that listens on
+localhost:10025 and then connects to the Postfix listener on localhost:10026.
+Note that the C<myhostname> options must be different between the two instances,
+otherwise Postfix will think it's talking to itself and abort sending.
+
+For the above example you can simply start I<spampd> like this:
+
+ spampd --host=localhost:10025 --relayhost=localhost:10026
+
+F<FILTER_README> from the Postfix distro has more details and examples of
+various setups, including how to skip the content filter for outbound mail.
+
+Another tip for Postfix when considering what timeout values to use for
+--childtimout and --satimeout options is the following command:
+
+C<# postconf | grep timeout>
+
+This will return a list of useful timeout settings and their values. For
+explanations see the relevant C<man> page (smtp, smtpd, lmtp). By default
+I<spampd> is set up for the default Postfix timeout values.
+
+=head1 Options
+
+=over 5
+
+=item B<--host=ip[:port] or hostname[:port]>
+
+Specifies what hostname/IP and port I<spampd> listens on. By default, it listens
+on 127.0.0.1 (localhost) on port 10025.
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025. This is an alternate to using the above --host=ip:port notation.
+
+=item B<--relayhost=ip[:port] or hostname[:port]>
+
+Specifies the hostname/IP where I<spampd> will relay all
+messages. Defaults to 127.0.0.1 (localhost). If the port is not provided, that
+defaults to 25.
+
+=item B<--relayport=n>
+
+Specifies what port I<spampd> will relay to. Default is 25. This is an
+alternate to using the above --relayhost=ip:port notation.
+
+=item B<--user=username> or B<--u=username>
+
+=item B<--group=groupname> or B<--g=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--children=n> or B<--c=n>
+
+Number of child servers to start and maintain (where n > 0). Each child will
+process up to --maxrequests (below) before exiting and being replaced by
+another child. Keep this number low on systems w/out a lot of memory.
+Default is 5 (which seems OK on a 512MB lightly loaded system). Note that
+there is always a parent process running, so if you specify 5 children you
+will actually have 6 I<spampd> processes running.
+
+You may want to set your origination mail server to limit the
+number of concurrent connections to I<spampd> to match this setting (for
+Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--childtimeout=n>
+
+This is the number of seconds to allow each child server before it times out
+a transaction. In an S/LMTP transaction the timer is reset for every command.
+This timeout includes time it would take to send the message data, so it should
+not be too short. Note that it's more likely the origination or destination
+mail servers will timeout first, which is fine. This is just a "sane" failsafe.
+Default is 360 seconds (6 minutes).
+
+=item B<--satimeout=n>
+
+This is the number of seconds to allow for processing a message with
+SpamAssassin (including feeding it the message, analyzing it, and adding
+the headers/report if necessary).
+This should be less than your origination and destination servers' timeout
+settings for the DATA command. For Postfix the default is 300 seconds in both
+cases (smtp_data_done_timeout and smtpd_timeout). In the event of timeout
+while processing the message, the problem is logged and the message is passed
+on anyway (w/out spam tagging, obviously). To fail the message with a temp
+450 error, see the --dose (die-on-sa-errors) option, below.
+Default is 285 seconds.
+
+=item B<--pid=filename> or B<--p=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--logsock=unix or inet> C<(new in v2.20)>
+
+Syslog socket to use. May be either "unix" of "inet". Default is "unix"
+except on HP-UX and SunOS (Solaris) systems which seem to prefer "inet".
+
+=item B<--nodetach> C<(new in v2.20)>
+
+If this option is given spampd won't detach from the console and fork into the
+background. This can be useful for running under control of some daemon
+management tools or when configured as a win32 service under cygrunsrv's
+control.
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KBytes. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. The size includes headers and attachments (if any).
+
+=item B<--dose>
+
+Acronym for (d)ie (o)n (s)pamAssassin (e)rrors. By default if I<spampd>
+encounters a problem with processing the message through Spam Assassin (timeout
+or other error), it will still pass the mail on to the destination server. If
+you specify this option however, the mail is instead rejected with a temporary
+error (code 450, which means the origination server should keep retrying to send
+it). See the related --satimeout option, above.
+
+=item B<--tagall> or B<--a>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings). Note that
+for this option to work as of SA-2.50, the I<always_add_report> and/or
+I<always_add_headers> settings in your SpamAssassin F<local.cf> need to be
+set to 1/true.
+
+=item B<--log-rules-hit> or B<--rh>
+
+Logs the names of each SpamAssassin rule which matched the message being
+processed. This list is returned by SA.
+
+=item B<--set-envelope-headers> or B<--seh> C<(new in v2.30)>
+
+Turns on addition of X-Envelope-To and X-Envelope-From headers to the mail
+being scanned before it is passed to SpamAssassin. The idea is to help SA
+process any blacklist/whitelist to/from directives on the actual
+sender/recipients instead of the possibly bogus envelope headers. This
+potentially exposes the list of all recipients of that mail (even BCC'ed ones).
+Therefore usage of this option is discouraged.
+
+I<NOTE>: Even though spampd tries to prevent this leakage by removing the
+X-Envelope-To header after scanning, SpamAssassin itself might add headers
+itself which report one or more of the recipients which had been listed in
+this header.
+
+=item B<--set-envelope-from> or B<--sef> C<(new in v2.30)>
+
+Same as above option but only enables the addition of X-Envelope-From header.
+For those that don't feel comfortable with the possible information exposure
+of X-Envelope-To. The above option overrides this one.
+
+=item B<--auto-whitelist> or B<--aw>
+
+This option is no longer relevant with SA version 3.0 and above, which
+controls auto whitelist use via local.cf settings.
+
+For SA version < 3.0, turns on the SpamAssassin global whitelist feature.
+See the SA docs. Note that per-user whitelists are not available.
+
+=item B<--local-only> or B<--L>
+
+Turn off all SA network-based tests (DNS, Razor, etc).
+
+=item B<--debug> or B<--d>
+
+Turns on SpamAssassin debug messages which print to the system mail log
+(same log as spampd will log to). Also turns on more verbose logging of
+what spampd is doing (new in v2). Also increases log level of Net::Server
+to 4 (debug), adding yet more info (but not too much) (new in v2.2).
+
+=item B<--help> or B<--h>
+
+Prints usage information.
+
+=back
+
+=head2 Deprecated Options
+
+=over 5
+
+The following options are no longer used but still accepted for backwards
+compatibility with prevoius I<spampd> versions:
+
+=item B<--dead-letters>
+
+=item B<--heloname>
+
+=item B<--stop-at-threshold>
+
+=item B<--add-sc-header>
+
+=item B<--hostname>
+
+=back
+
+=head1 Examples
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 Credits
+
+I<spampd> is written and maintained by Maxim Paperno <MPaperno@WorldDesign.com>.
+See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info.
+
+I<spampd> v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+Stanley Dean Witter. These are distributed under the GNU GPL (see
+module code for more details). Both modules have been slightly modified
+from the originals and are included in this file under new names.
+
+Also thanks to Bennett Todd for the example smtpproxy script which helped create
+this version of I<spampd>. See http://bent.latency.net/smtpprox/ .
+
+I<spampd> v1 was based on code by Dave Carrigan named I<assassind>. Trace
+amounts of his code or documentation may still remain. Thanks to him for the
+original inspiration and code. See http://www.rudedog.org/assassind/ .
+
+Also thanks to I<spamd> (included with SpamAssassin) and
+I<amavisd-new> (http://www.ijs.si/software/amavisd/) for some tricks.
+
+Various people have contributed patches, bug reports, and ideas, all of whom
+I would like to thank. I have tried to include credits in code comments and
+in the change log, as appropriate.
+
+=head2 Code Contributors (in order of appearance):
+
+ Kurt Andersen
+ Roland Koeckel
+ Urban Petry
+ Sven Mueller
+
+=head1 Copyright, License, and Disclaimer
+
+I<spampd> is Copyright (c) 2002 by World Design Group, Inc. and Maxim Paperno.
+
+Portions are Copyright (C) 2001 Morgan Stanley Dean Witter as mentioned above
+in the Credits section.
+
+ 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.
+
+ The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+
+
+=head1 Bugs
+
+None known. Please report any to MPaperno@WorldDesign.com.
+
+=head1 To Do
+
+Figure out how to use Net::Server::PreFork because it has cool potential for
+load management. I tried but either I'm missing something or PreFork is
+somewhat broken in how it works. If anyone has experience here, please let
+me know.
+
+Add configurable option for rejecting mail outright based on spam score.
+It would be nice to make this program safe enough to sit in front of a mail
+server such as Postfix and be able to reject mail before it enters our systems.
+The only real problem is that Postfix will see localhost as the connecting
+client, so that disables any client-based checks Postfix can do and creates a
+possible relay hole if localhost is trusted.
+
+=head1 See Also
+
+perl(1), Spam::Assassin(3), L<http://www.spamassassin.org/>,
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>
diff --git a/previous-versions/spampd-2.32.pl b/previous-versions/spampd-2.32.pl
new file mode 100644
index 0000000..f6a2363
--- /dev/null
+++ b/previous-versions/spampd-2.32.pl
@@ -0,0 +1,1559 @@
+#!/usr/bin/perl -T
+
+######################
+# SpamPD - spam proxy daemon
+#
+# v2.32 - 02-Feb-06
+# v2.30 - 31-Oct-05
+# v2.21 - 23-Oct-05
+# v2.20 - 05-Oct-04
+# v2.13 - 24-Nov-03
+# v2.12 - 15-Nov-03
+# v2.11 - 15-Jul-03
+# v2.10 - 01-Jul-03
+# v2.00 - 10-Jun-03
+# v1.0.2 - 13-Apr-03
+# v1.0.1 - 03-Feb-03
+# v1.0.0 - May 2002
+#
+# spampd is Copyright (c) 2002 by World Design Group and Maxim Paperno
+# (see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# Written and maintained by Maxim Paperno (MPaperno@WorldDesign.com)
+#
+# 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.
+#
+# The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+#
+# spampd v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+# Stanley Dean Witter. These are also distributed under the GNU GPL (see
+# module code for more details). Both modules have been slightly modified
+# from the originals and are included in this file under new names.
+#
+# spampd v1 was based on code by Dave Carrigan named assassind. Trace amounts
+# of his code or documentation may still remain. Thanks to him for the
+# original inspiration and code. (see http://www.rudedog.org/assassind/)
+#
+######################
+
+
+################################################################################
+package SpamPD::Server;
+
+# Originally known as MSDW::SMTP::Server
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =item DESCRIPTION
+#
+# This server simply gathers the SMTP acquired information (envelope
+# sender and recipient, and data) into unparsed memory buffers (or a
+# file for the data), and returns control to the caller to explicitly
+# acknowlege each command or request. Since acknowlegement or failure
+# are driven explicitly from the caller, this module can be used to
+# create a robust SMTP content scanning proxy, transparent or not as
+# desired.
+#
+# =cut
+
+use strict;
+use IO::File;
+#use IO::Socket;
+
+# =item new(interface => $interface, port => $port);
+
+# The #interface and port to listen on must be specified. The interface
+# must be a valid numeric IP address (0.0.0.0 to listen on all
+# interfaces, as usual); the port must be numeric. If this call
+# succeeds, it returns a server structure with an open
+# IO::Socket::INET in it, ready to listen on. If it fails it dies, so
+# if you want anything other than an exit with an explanatory error
+# message, wrap the constructor call in an eval block and pull the
+# error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die.
+#
+# =cut
+
+sub new {
+
+# This now emulates Net::SMTP::Server::Client for use with Net::Server which
+# passes an already open socket.
+
+ my($this, $socket) = @_;
+
+ my $class = ref($this) || $this;
+ my $self = {};
+ $self->{sock} = $socket;
+
+ bless($self, $class);
+
+ die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+ $self->{state} = 'started';
+ return $self;
+
+# Original code, removed by MP for spampd use
+#
+# my ($this, @opts) = @_;
+# my $class = ref($this) || $this;
+# my $self = bless { @opts }, $class;
+# $self->{sock} = IO::Socket::INET->new(
+# LocalAddr => $self->{interface},
+# LocalPort => $self->{port},
+# Proto => 'tcp',
+# Type => SOCK_STREAM,
+# Listen => 65536,
+# Reuse => 1,
+# );
+# die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+# $self->{state} = 'just bound',
+# return $self;
+
+}
+
+# sub accept { }
+#
+# Removed by MP; not needed for spampd use
+#
+
+
+# =item chat;
+#
+# The chat method carries the SMTP dialogue up to the point where any
+# acknowlegement must be made. If chat returns true, then its return
+# value is the previous SMTP command. If the return value begins with
+# 'mail' (case insensitive), then the attribute 'from' has been filled
+# in, and may be checked; if the return value begins with 'rcpt' then
+# both from and to have been been filled in with scalars, and should
+# be checked, then either 'ok' or 'fail' should be called to accept
+# or reject the given sender/recipient pair. If the return value is
+# 'data', then the attributes from and to are populated; in this case,
+# the 'to' attribute is a reference to an anonymous array containing
+# all the recipients for this data. If the return value is '.', then
+# the 'data' attribute (which may be pre-populated in the "new" or
+# "accept" methods if desired) is a reference to a filehandle; if it's
+# created automatically by this module it will point to an unlinked
+# tmp file in /tmp. If chat returns false, the SMTP dialogue has been
+# completed and the socket closed; this server is ready to exit or to
+# accept again, as appropriate for the server style.
+#
+# The return value from chat is also remembered inside the server
+# structure in the "state" attribute.
+#
+# =cut
+
+sub chat {
+ my ($self) = @_;
+ local(*_);
+ if ($self->{state} !~ /^data/i) {
+ return 0 unless defined($_ = $self->_getline);
+ s/[\r\n]*$//;
+ $self->{state} = $_;
+ if (s/^.?he?lo\s+//i) { # mp: find helo|ehlo|lhlo
+ # mp: determine protocol (for future use)
+ if ( /^L/i ) {
+ $self->{proto} = "lmtp";
+ } elsif ( /^E/i ) {
+ $self->{proto} = "esmtp";
+ } else {
+ $self->{proto} = "smtp"; }
+ s/\s*$//;
+ s/\s+/ /g;
+ $self->{helo} = $_;
+ } elsif (s/^rset\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ } elsif (s/^mail\s+from:\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ s/\s*$//;
+ $self->{from} = $_;
+ } elsif (s/^rcpt\s+to:\s*//i) {
+ s/\s*$//; s/\s+/ /g;
+ $self->{to} = $_;
+ push @{$self->{recipients}}, $_;
+ } elsif (/^data/i) {
+ $self->{to} = $self->{recipients};
+ }
+ } else {
+ if (defined($self->{data})) {
+ $self->{data}->seek(0, 0);
+ $self->{data}->truncate(0);
+ } else {
+ $self->{data} = IO::File->new_tmpfile;
+ }
+ while (defined($_ = $self->_getline)) {
+ if ($_ eq ".\r\n") {
+ $self->{data}->seek(0,0);
+ return $self->{state} = '.';
+ }
+ s/^\.\./\./;
+ $self->{data}->print($_) or die "$0: write error saving data\n";
+ }
+ return(0);
+ }
+ return $self->{state};
+}
+
+# =item ok([message]);
+#
+# Approves of the data given to date, either the recipient or the
+# data, in the context of the sender [and, for data, recipients]
+# already given and available as attributes. If a message is given, it
+# will be sent instead of the internal default.
+#
+# =cut
+
+sub ok {
+ my ($self, @msg) = @_;
+ @msg = ("250 ok.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# =item fail([message]);
+#
+# Rejects the current info; if processing from, rejects the sender; if
+# processing 'to', rejects the current recipient; if processing data,
+# rejects the entire message. If a message is specified it means the
+# exact same thing as "ok" --- simply send that message to the sender.
+#
+# =cut
+
+sub fail {
+ my ($self, @msg) = @_;
+ @msg = ("550 no.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# utility functions
+
+sub _getline {
+ my ($self) = @_;
+ local ($/) = "\r\n";
+ my $tmp = $self->{sock}->getline;
+ if ( defined $self->{debug} ) {
+ $self->{debug}->print($tmp) if ($tmp);
+ }
+ return $tmp;
+}
+
+sub _print {
+ my ($self, @msg) = @_;
+ $self->{debug}->print(@msg) if defined $self->{debug};
+ $self->{sock}->print(@msg);
+}
+
+1;
+
+################################################################################
+package SpamPD::Client;
+
+# Originally known as MSDW::SMTP::Client
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =head1 DESCRIPTION
+#
+# MSDW::SMTP::Client provides a very lean SMTP client implementation;
+# the only protocol-specific knowlege it has is the structure of SMTP
+# multiline responses. All specifics lie in the hands of the calling
+# program; this makes it appropriate for a semi-transparent SMTP
+# proxy, passing commands between a talker and a listener.
+#
+# =cut
+
+use strict;
+use IO::Socket;
+
+# =item new(interface => $interface, port => $port[, timeout = 300]);
+#
+# The interface and port to talk to must be specified. The interface
+# must be a valid numeric IP address; the port must be numeric. If
+# this call succeeds, it returns a client structure with an open
+# IO::Socket::INET in it, ready to talk to. If it fails it dies,
+# so if you want anything other than an exit with an explanatory
+# error message, wrap the constructor call in an eval block and pull
+# the error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die. The timeout parameter is passed
+# on into the IO::Socket::INET constructor.
+#
+# =cut
+
+sub new {
+ my ($this, @opts) = @_;
+ my $class = ref($this) || $this;
+ my $self = bless { timeout => 300, @opts }, $class;
+ $self->{sock} = IO::Socket::INET->new(
+ PeerAddr => $self->{interface},
+ PeerPort => $self->{port},
+ Timeout => $self->{timeout},
+ Proto => 'tcp',
+ Type => SOCK_STREAM,
+ );
+ die "$0: socket connect failure: $!\n" unless defined $self->{sock};
+ return $self;
+}
+
+# =item hear
+#
+# hear collects a complete SMTP response and returns it with trailing
+# CRLF removed; for multi-line responses, intermediate CRLFs are left
+# intact. Returns undef if EOF is seen before a complete reply is
+# collected.
+#
+# =cut
+
+sub hear {
+ my ($self) = @_;
+ my ($tmp, $reply);
+ return undef unless $tmp = $self->{sock}->getline;
+ while ($tmp =~ /^\d{3}-/) {
+ $reply .= $tmp;
+ return undef unless $tmp = $self->{sock}->getline;
+ }
+ $reply .= $tmp;
+ $reply =~ s/\r\n$//;
+ return $reply;
+}
+
+# =item say("command text")
+#
+# say sends an SMTP command, appending CRLF.
+#
+# =cut
+
+sub say {
+ my ($self, @msg) = @_;
+ return unless @msg;
+ $self->{sock}->print("@msg", "\r\n") or die "$0: write error: $!";
+}
+
+# =item yammer(FILEHANDLE)
+#
+# yammer takes a filehandle (which should be positioned at the
+# beginning of the file, remember to $fh->seek(0,0) if you've just
+# written it) and sends its contents as the contents of DATA. This
+# should only be invoked after a $client->say("data") and a
+# $client->hear to collect the reply to the data command. It will send
+# the trailing "." as well. It will perform leading-dot-doubling in
+# accordance with the SMTP protocol spec, where "leading dot" is
+# defined in terms of CR-LF terminated lines --- i.e. the data should
+# contain CR-LF data without the leading-dot-quoting. The filehandle
+# will be left at EOF.
+#
+# =cut
+
+sub yammer {
+ my ($self, $fh) = (@_);
+ local (*_);
+ local ($/) = "\r\n";
+ $self->{sock}->autoflush(0); # use less writes (thx to Sam Horrocks for the tip)
+ while (<$fh>) {
+ s/^\./../;
+ $self->{sock}->print($_) or die "$0: write error: $!\n";
+ }
+ $self->{sock}->autoflush(1); # restore unbuffered socket operation
+ $self->{sock}->print(".\r\n") or die "$0: write error: $!\n";
+}
+
+1;
+
+
+################################################################################
+package SpamPD;
+
+use strict;
+use Net::Server::PreForkSimple;
+use IO::File;
+use Getopt::Long;
+use Mail::SpamAssassin;
+
+BEGIN {
+ # Load Time::HiRes if it's available
+ eval { require Time::HiRes };
+ Time::HiRes->import( qw(time) ) unless $@;
+
+ # use included modules
+ import SpamPD::Server;
+ import SpamPD::Client;
+}
+
+
+use vars qw(@ISA $VERSION);
+our @ISA = qw(Net::Server::PreForkSimple);
+our $VERSION = '2.30';
+
+sub process_message {
+ my ($self, $fh) = @_;
+
+ # output lists with a , delimeter by default
+ local ($") = ",";
+
+ # start a timer
+ my $start = time;
+ # use the assassin object created during startup
+ my $assassin = $self->{spampd}->{assassin};
+ my $sa_version = Mail::SpamAssassin::Version();
+ # $sa_version can have a non-numeric value if version_tag is
+ # set in local.cf. Only take first numeric value
+ $sa_version =~ s/([0-9]*\.[0-9]*).*/$1/;
+
+ # this gets info about the message temp file
+ my $size = ($fh->stat)[7] or die "Can't stat mail file: $!";
+
+ # Only process message under --maxsize KB
+ if ( $size < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ my (@msglines, $msgid, $sender, $recips, $tmp, $mail, $msg_resp);
+
+ my $inhdr = 1;
+ my $envfrom = 0;
+ my $envto = 0;
+ my $addedenvto = 0;
+
+ $recips = "@{$self->{smtp_server}->{to}}";
+ if ("$self->{smtp_server}->{from}" =~ /(\<.*?\>)/ ) {$sender = $1;}
+ $recips ||= "(unknown)";
+ $sender ||= "(unknown)";
+
+ ## read message into array of lines to feed to SA
+
+ # loop over message file content
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ while (<$fh>) {
+ $envto = 1 if (/^(?:X-)?Envelope-To: /);
+ $envfrom = 1 if (/^(?:X-)?Envelope-From: /);
+ if ( (/^\r?\n$/) && ($inhdr ==1) ) {
+ $inhdr = 0; # outside of msg header after first blank line
+ if ( ( $self->{spampd}->{envelopeheaders} ||
+ $self->{spampd}->{setenvelopefrom} ) &&
+ $envfrom == 0 ) {
+ push(@msglines, "X-Envelope-From: $sender\r\n");
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Added X-Envelope-From"); }
+ }
+ if ( $self->{spampd}->{envelopeheaders} && $envto == 0 ) {
+ push(@msglines, "X-Envelope-To: $recips\r\n");
+ $addedenvto = 1;
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Added X-Envelope-To"); }
+ }
+ }
+ push(@msglines, $_);
+ # find the Message-ID for logging (code is mostly from spamd)
+ if ( $inhdr && /^Message-Id:\s+(.*?)\s*$/i ) {
+ $msgid = $1;
+ while ( $msgid =~ s/\([^\(\)]*\)// ) { } # remove comments and
+ $msgid =~ s/^\s+|\s+$//g; # leading and trailing spaces
+ $msgid =~ s/\s+/ /g; # collapse whitespaces
+ $msgid =~ s/^.*?<(.*?)>.*$/$1/; # keep only the id itself
+ $msgid =~ s/[^\x21-\x7e]/?/g; # replace all weird chars
+ $msgid =~ s/[<>]/?/g; # plus all dangling angle brackets
+ $msgid =~ s/^(.+)$/<$1>/; # re-bracket the id (if not empty)
+ }
+ }
+
+ $msgid ||= "(unknown)";
+
+ $self->mylog(2, "processing message $msgid for ". $recips);
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Timed out!\n" };
+ # save previous timer and start new
+ my $previous_alarm = alarm($self->{spampd}->{satimeout});
+
+ # Audit the message
+ if ($sa_version >= 3) {
+ $mail = $assassin->parse(\@msglines, 0);
+ undef @msglines; #clear some memory-- this screws up SA < v3
+ } elsif ($sa_version >= 2.70) {
+ $mail = Mail::SpamAssassin::MsgParser->parse(\@msglines);
+ } else {
+ $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines );
+ }
+
+
+ # Check spamminess (returns Mail::SpamAssassin:PerMsgStatus object)
+ my $status = $assassin->check($mail);
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Returned from checking by SpamAssassin"); }
+
+ # Rewrite mail if high spam factor or options --tagall
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Rewriting mail using SpamAssassin"); }
+
+ # use Mail::SpamAssassin:PerMsgStatus object to rewrite message
+ if ( $sa_version >= 3 ) {
+ $msg_resp = $status->rewrite_mail;
+ } else {
+ # SA versions prior to 3 need to get the response in a different manner
+ $status->rewrite_mail;
+ $msg_resp = join '', $mail->header, "\r\n", @{$mail->body};
+ }
+
+ # Build the new message to relay
+ # pause the timeout alarm while we do this (no point in timing
+ # out here and leaving a half-written file).
+ my @resplines = split(/\r?\n/, $msg_resp);
+ my $pause_alarm = alarm(0);
+ my $inhdr = 1;
+ my $skipline = 0;
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ $fh->truncate(0) or die "Can't truncate message file: $!";
+ my $arraycont = @resplines;
+
+ for ( 0..($arraycont-1) ) {
+ $inhdr=0 if ($resplines[$_] =~ m/^\r?\n$/);
+
+ # if we are still in the header, skip over any
+ # "X-Envelope-To: " line if we have previously added it.
+ if ( $inhdr == 1 &&
+ $addedenvto == 1 &&
+ $resplines[$_] =~ m/^X-Envelope-To: .*$/) {
+ $skipline = 1;
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Removing X-Envelope-To"); }
+ }
+
+ if (! $skipline) {
+ $fh->print($resplines[$_] . "\r\n")
+ or die "Can't print to message file: $!";
+ } else {
+ $skipline = 0; }
+ }
+
+ #restart the alarm
+ alarm($pause_alarm);
+
+ }
+
+ # Log what we did
+ my $was_it_spam = 'clean message';
+ if ($status->is_spam) { $was_it_spam = 'identified spam'; }
+ my $msg_score = sprintf("%.2f",$status->get_hits);
+ my $msg_threshold = sprintf("%.2f",$status->get_required_hits);
+ my $proc_time = sprintf("%.2f", time - $start);
+
+ $self->mylog(2, "$was_it_spam $msgid ($msg_score/$msg_threshold) from $sender for ".
+ "$recips in ". $proc_time . "s, $size bytes.");
+
+ # thanks to Kurt Andersen for this idea
+ if ( $self->{spampd}->{rh} ) {
+ $self->mylog(2, "rules hit for $msgid: " . $status->get_names_of_tests_hit); }
+
+ $status->finish();
+ $mail->finish();
+
+ # set the timeout alarm back to wherever it was at
+ alarm($previous_alarm);
+
+ };
+
+ if ( $@ ne '' ) {
+ $self->mylog(1, "WARNING!! SpamAssassin error on message $msgid: $@");
+ return 0;
+ }
+
+ } else {
+
+ $self->mylog(2, "skipped large message (". $size / 1024 ."KB)");
+
+ }
+
+ return 1;
+
+}
+
+sub process_request {
+ my $self = shift;
+ my $msg;
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Child server process timed out!\n" };
+ my $timeout = $self->{spampd}->{childtimeout};
+
+ # start a timeout alarm
+ alarm($timeout);
+
+ # start an smtp server
+ my $smtp_server = SpamPD::Server->new($self->{server}->{client});
+ unless ( defined $smtp_server ) {
+ die "Failed to create listening Server: $!"; }
+
+ $self->{smtp_server} = $smtp_server;
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Initiated Server"); }
+
+ # start an smtp "client" (really a sending server)
+ my $client = SpamPD::Client->new(interface => $self->{spampd}->{relayhost},
+ port => $self->{spampd}->{relayport});
+ unless ( defined $client ) {
+ die "Failed to create sending Client: $!"; }
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Initiated Client"); }
+
+ # pass on initial client response
+ # $client->hear can handle multiline responses so no need to loop
+ $smtp_server->ok($client->hear)
+ or die "Error in initial server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # while loop over incoming data from the server
+ while ( my $what = $smtp_server->chat ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # until end of DATA is sent, just pass the commands on transparently
+ if ($what ne '.') {
+
+ $client->say($what)
+ or die "Failure in client->say(what): $!";
+
+ # but once the data is sent now we want to process it
+ } else {
+
+ # spam checking routine - message might be rewritten here
+ my $pmrescode = $self->process_message($smtp_server->{data});
+
+ # pass on the messsage if exit code <> 0 or die-on-sa-errors flag is off
+ if ( $pmrescode or !$self->{spampd}->{dose} ) {
+
+ # need to give the client a rewound file
+ $smtp_server->{data}->seek(0,0)
+ or die "Can't rewind mail file: $!";
+
+ # now send the data on through the client
+ $client->yammer($smtp_server->{data})
+ or die "Failure in client->yammer(smtp_server->{data}): $!";
+
+ } else {
+
+ $smtp_server->ok("450 Temporary failure processing message, please try again later");
+ last;
+ }
+
+ #close the temp file
+ $smtp_server->{data}->close
+ or $self->mylog(1, "WARNING!! Couldn't close smtp_server->{data} temp file: $!");
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Finished sending DATA"); }
+ }
+
+ # pass on whatever the relayhost said in response
+ # $client->hear can handle multiline responses so no need to loop
+ my $destresp = $client->hear;
+ $smtp_server->ok($destresp)
+ or die "Error in server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Destination response: '" . $destresp . "'"); }
+
+ # if we're in data state but the response is an error, exit data state.
+ # Shold not normally occur, but can happen. Thanks to Rodrigo Ventura for bug reports.
+ if ( $smtp_server->{state} =~ /^data/i and $destresp =~ /^[45]\d{2} / ) {
+ $smtp_server->{state} = "err_after_data";
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Destination response indicates error after DATA command"); }
+ }
+
+ # restart the timeout alarm
+ alarm($timeout);
+
+ } # server ends connection
+
+ # close connections
+ $client->{sock}->close
+ or die "Couldn't close client->{sock}: $!";
+ $smtp_server->{sock}->close
+ or die "Couldn't close smtp_server->{sock}: $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Closed connections"); }
+
+ }; # end eval block
+
+ alarm(0); # stop the timer
+ # check for error in eval block
+ if ($@ ne '') {
+ chomp($@);
+ $msg = "WARNING!! Error in process_request eval block: $@";
+ $self->mylog(0, $msg);
+ die ($msg . "\n");
+ }
+
+ $self->{spampd}->{instance}++;
+
+}
+
+# Net::Server hook
+# about to exit child process
+sub child_finish_hook {
+ my($self) = shift;
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Exiting child process after handling ".
+ $self->{spampd}->{instance} ." requests"); }
+}
+
+# older Net::Server versions (<= 0.87) die when logging a % character to Sys::Syslog
+sub mylog($$$) {
+ my ($self, $level, $msg) = @_;
+ $msg =~ s/\%/%%/g;
+ $self->log($level, $msg);
+}
+
+
+################## SETUP ######################
+
+
+my $relayhost = '127.0.0.1'; # relay to ip
+my $relayport = 25; # relay to port
+my $host = '127.0.0.1'; # listen on ip
+my $port = 10025; # listen on port
+my $children = 5; # number of child processes (servers) to spawn at start
+# my $maxchildren = $children; # max. number of child processes (servers) to spawn
+my $maxrequests = 20; # max requests handled by child b4 dying
+my $childtimeout = 6*60; # child process per-command timeout in seconds
+my $satimeout = 285; # SpamAssassin timeout in seconds (15s less than Postfix
+ # default for smtp_data_done_timeout)
+my $pidfile = '/var/run/spampd.pid'; # write pid to file
+my $user = 'mail'; # user to run as
+my $group = 'mail'; # group to run as
+my $tagall = 0; # mark-up all msgs with SA, not just spam
+my $maxsize = 64; # max. msg size to scan with SA, in KB.
+my $rh = 0; # log which rules were hit
+my $debug = 0; # debug flag
+my $dose = 0; # die-on-sa-errors flag
+my $logsock = "unix"; # default log socket (some systems like 'inet')
+my $nsloglevel = 2; # default log level for Net::Server (in the range 0-4)
+my $background = 1; # specifies whether to 'daemonize' and fork into background;
+ # apparently useful under Win32/cygwin to disable this via
+ # --nodetach option;
+my $envelopeheaders = 0; # Set X-Envelope-To and X-Envelope-From headers in the mail before
+ # passing it to spamassassin. Set to 1 to enable this.
+my $setenvelopefrom = 0; # Set X-Envelope-From header only
+my $saconfigfile = ""; # use this config file for SA settings (blank uses default local.cf)
+my $home_dir = '/var/spool/spamassassin/spampd'; # home directory for SA files
+ # (auto-whitelist, plugin helpers)
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ relayport => \$relayport,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ childtimeout => \$childtimeout,
+ satimeout => \$satimeout,
+ children => \$children,
+ # maxchildren => \$maxchildren,
+ logsock => \$logsock,
+ envelopeheaders => \$envelopeheaders,
+ setenvelopefrom => \$setenvelopefrom,
+ saconfigfile => \$saconfigfile,
+ homedir => \$homedir,
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'relayport=i',
+ 'children|c=i',
+ # 'maxchildren|mc=i',
+ 'maxrequests|mr=i',
+ 'childtimeout=i',
+ 'satimeout=i',
+ 'dead-letters=s', # deprecated
+ 'user|u=s',
+ 'group|g=s',
+ 'pid|p=s',
+ 'maxsize=i',
+ 'heloname=s', # deprecated
+ 'tagall|a',
+ 'auto-whitelist|aw',
+ 'stop-at-threshold', # deprecated
+ 'debug|d',
+ 'help|h|?',
+ 'local-only|l',
+ 'log-rules-hit|rh',
+ 'dose',
+ 'add-sc-header|ash', # deprecated
+ 'hostname=s', # deprecated
+ 'logsock=s',
+ 'nodetach',
+ 'set-envelope-headers|seh',
+ 'set-envelope-from|sef',
+ 'saconfig=s',
+ 'homedir=s',
+ );
+
+usage(0) if $options{help};
+
+if ( $logsock !~ /^(unix|inet)$/ ) {
+ print "--logsock parameter needs to be either unix or inet\n\n";
+ usage(0);
+}
+
+if ( $options{tagall} ) { $tagall = 1; }
+if ( $options{'log-rules-hit'} ) { $rh = 1; }
+if ( $options{debug} ) { $debug = 1; $nsloglevel = 4; }
+if ( $options{dose} ) { $dose = 1; }
+if ( $options{'nodetach'} ) { $background = undef; }
+if ( $options{'set-envelope-headers'} ) { $envelopeheaders = 1; }
+if ( $options{'set-envelope-from'} ) { $setenvelopefrom = 1; }
+if ( $options{'saconfig'} ) { $saconfigfile = $options{'saconfig'}; }
+if ( $options{'homedir'} ) { $homedir = $options{'homedir'}; }
+# if ( !$options{maxchildren} or $maxchildren < $children ) { $maxchildren = $children; }
+
+if ( $children < 1 ) { print "Option --children must be greater than zero!\n"; exit shift; }
+
+# my $min_spare_servers = ($children == $maxchildren) ? 0 : 1;
+# my $max_spare_servers = ($min_spare_servers == 0) ? 0 : $maxchildren-1;
+
+my @tmp = split (/:/, $relayhost);
+$relayhost = $tmp[0];
+if ( $tmp[1] ) { $relayport = $tmp[1]; }
+
+@tmp = split (/:/, $host);
+$host = $tmp[0];
+if ( $tmp[1] ) { $port = $tmp[1]; }
+
+my $sa_options = {
+ 'dont_copy_prefs' => 1,
+ 'debug' => $debug,
+ 'local_tests_only' => $options{'local-only'} || 0,
+ 'home_dir_for_helpers' => $home_dir,
+ 'userstate_dir' => $home_dir
+};
+
+my $use_user_prefs = 0;
+
+if ( $saconfigfile != "" ) {
+ $sa_options->{ 'userprefs_filename' } = $saconfigfile;
+ $use_user_prefs = 1;
+}
+
+
+#cleanup environment before starting SA (thanks to Alexander Wirt)
+$ENV{'PATH'} = '/bin:/usr/bin:/sbin:/usr/sbin';
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV', 'HOME'};
+
+my $assassin = Mail::SpamAssassin->new($sa_options);
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now($use_user_prefs);
+
+# thanks to Kurt Andersen for the 'uname -s' fix
+if ( !$options{logsock} ) {
+ eval {
+ my $osname = `uname -s`;
+ if (($osname =~ 'HP-UX') || ($osname =~ 'SunOS')) {
+ $logsock = "inet";
+ }
+ };
+}
+
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ log_level => $nsloglevel,
+ syslog_logsock => $logsock,
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => $background,
+ # setsid => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ max_servers => $children,
+ max_requests => $maxrequests,
+ # min_servers => $children,
+ # max_servers => $maxchildren,
+ # min_spare_servers => $min_spare_servers,
+ # max_spare_servers => $max_spare_servers,
+ },
+ spampd => { relayhost => $relayhost,
+ relayport => $relayport,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ childtimeout => $childtimeout,
+ satimeout => $satimeout,
+ rh => $rh,
+ debug => $debug,
+ dose => $dose,
+ instance => 0,
+ envelopeheaders => $envelopeheaders,
+ setenvelopefrom => $setenvelopefrom,
+ },
+ }, 'SpamPD';
+
+# Redirect all warnings to Server::log
+$SIG{__WARN__} = sub { $server->log (2, $_[0]); };
+
+# call Net::Server to start up the daemon inside
+$server->run;
+
+exit 1; # shouldn't get here
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --host=host[:port] Hostname/IP and optional port to listen on.
+ Default is 127.0.0.1 port 10025
+ --port=n Port to listen on (alternate syntax to above).
+ --relayhost=host[:port] Host to relay mail to.
+ Default is 127.0.0.1 port 25.
+ --relayport=n Port to relay to (alternate syntax to above).
+
+ --children=n Number of child processes (servers) to start and
+ keep running. Default is 5 (plus 1 parent proc).
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Default is 20.
+ --childtimeout=n Time out children after this many seconds during
+ transactions (each S/LMTP command including the
+ time it takes to send the data).
+ Default is 360 seconds (6min).
+ --satimeout=n Time out SpamAssassin after this many seconds.
+ Default is 285 seconds.
+
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+ --nodetach Don't detach from the console and fork into
+ background. Useful for some daemon control
+ tools or when running as a win32 service
+ under cygwin.
+
+ --logsock=inet or unix Allows specifying the syslog socket type. Default is
+ 'unix' except on HPUX and SunOS which prefer 'inet'.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --dose (d)ie (o)n (s)pamAssassin (e)rrors. If this is
+ specified and SA times out or throws an error,
+ the mail will be rejected with a 450 temporary
+ error message. Default is to pass through email
+ even in the event of an SA problem.
+ --tagall Tag all messages with SA headers, not just spam.
+ --log-rules-hit Log the name of each SA test which matched the
+ or --rh current message.
+
+ --set-envelope-headers Set X-Envelope-From and X-Envelope-To headers before
+ or --seh passing the mail to SpamAssassin. This is
+ disabled by default because it potentially leaks
+ information. NOTE: Please read the manpage before
+ enabling this!
+ --set-envelope-from Same as above but only sets X-Envelope-From, for
+ or --sef those that don't feel comfortable with the
+ potential information leak.
+
+ --auto-whitelist Use the SA global auto-whitelist feature
+ or --aw (SA versions => 3.0 now control this via local.cf).
+ --local-only or -L Turn off all SA network-based tests (RBL, Razor, etc).
+ --homedir=path Use the specified directory as home directory for
+ the SpamAssassin process.
+ Default is /var/spool/spamassassin/spampd
+ --saconfig=filename Use the specified file for loading SA configuration
+ options after the default local.cf file.
+ --debug or -d Turn on SA debugging (sent to log file).
+
+ --help or -h or -? This message
+
+Deprecated Options (still accepted for backwards compatibility):
+ --heloname=hostname No longer used in spampd v.2
+ --dead-letters=path No longer used in spampd v.2
+ --stop-at-threshold No longer implemented in SpamAssassin
+EOF
+
+# --maxchildren=n Maximum number of child processes (servers) to
+# run. Default is the value of --children.
+
+ exit shift;
+}
+
+__END__
+
+# Some commented-out documentation. POD doesn't have a way to comment
+# out sections!? This documents a feature which may be implemented later.
+#
+# =item B<--maxchildren=n> or B<--mc=n>
+#
+# Maximum number of children to spawn if needed (where n >= --children). When
+# I<spampd> starts it will spawn a number of child servers as specified by
+# --children. If all those servers become busy, a new child is spawned up to the
+# number specified in --maxchildren. Default is to have --maxchildren equal to
+# --children so extra child processes aren't started. Also see the --children
+# option, above. You may want to set your origination mail server to limit the
+# number of concurrent connections to I<spampd> to match this setting (for
+# Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+# 'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+#
+# Note that extra servers after the initial --children will only spawn on very
+# busy systems. This is because the check to see if a new server is needed (ie.
+# all current ones are busy) is only done around once per minute (this is
+# controlled by the Net::Server::PreFork module, in case you want to
+# hack at it :). It can still be useful as an "overflow valve," and is
+# especially nice since the extra child servers will die off once they're not
+# needed.
+
+=pod
+
+=head1 NAME
+
+SpamPD - Spam Proxy Daemon (version 2.2)
+
+=head1 Synopsis
+
+B<spampd>
+[B<--host=host[:port]>]
+[B<--relayhost=hostname[:port]>]
+[B<--user|u=username>]
+[B<--group|g=groupname>]
+[B<--children|c=n>]
+#[B<--maxchildren|mc=n>]
+[B<--maxrequests=n>]
+[B<--childtimeout=n>]
+[B<--satimeout=n>]
+[B<--pid|p=filename>]
+[B<--nodetach>]
+[B<--logsock=inet|unix>]
+[B<--maxsize=n>]
+[B<--dose>]
+[B<--tagall|a>]
+[B<--log-rules-hit|rh>]
+[B<--set-envelope-headers|seh>]
+[B<--set-envelope-from|sef>]
+[B<--auto-whitelist|aw>]
+[B<--local-only|L>]
+[B<--saconfig=filename>]
+[B<--debug|d>]
+
+B<spampd> B<--help>
+
+=head1 Description
+
+I<spampd> is an SMTP/LMTP proxy that marks (or tags) spam using
+SpamAssassin (http://www.SpamAssassin.org/). The proxy is designed
+to be transparent to the sending and receiving mail servers and at no point
+takes responsibility for the message itself. If a failure occurs within
+I<spampd> (or SpamAssassin) then the mail servers will disconnect and the
+sending server is still responsible for retrying the message for as long
+as it is configured to do so.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+The latest version can be found at
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>.
+
+=head1 Requires
+
+=over 5
+
+Perl modules:
+
+=item B<Mail::SpamAssassin>
+
+=item B<Net::Server::PreForkSimple>
+
+=item B<IO::File>
+
+=item B<IO::Socket>
+
+=item B<Time::HiRes> (not actually required but recommended)
+
+=back
+
+=head1 Operation
+
+I<spampd> is meant to operate as an S/LMTP mail proxy which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing! It is very easy to set up an open relay this
+way.
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+
+B<Running between firewall/gateway and internal mail server>
+
+=over 3
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+ Internet -> [ MX gateway (@inter.net.host:25) ->
+ spampd (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+=back
+
+B<Using Postfix advanced content filtering>
+
+=over 3
+
+Please see the F<FILTER_README> that came with the Postfix distribution. You
+need to have a version of Postfix which supports this (ideally v.2 and up).
+
+ Internet -> [ Postfix (@inter.net.host:25) ->
+ spampd (@localhost:10025) ->
+ Postfix (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+=head1 Upgrading
+
+If upgrading from a version prior to 2.2, please note that the --add-sc-header
+option is no longer supported. Use SAs built-in header manipulation features
+instead (as of SA v2.6).
+
+Upgrading from version 1 simply involves replacing the F<spampd> program file
+with the latest one. Note that the I<dead-letters> folder is no longer being
+used and the --dead-letters option is no longer needed (though no errors are
+thrown if it's present). Check the L<"Options"> list below for a full list of new
+and deprecated options. Also be sure to check out the change log.
+
+=head1 Installation
+
+I<spampd> can be run directly from the command prompt if desired. This is
+useful for testing purposes, but for long term use you probably want to put
+it somewhere like /usr/bin or /usr/local/bin and execute it at system startup.
+For example on Red Hat-style Linux system one can use a script in
+/etc/rc.d/init.d to start I<spampd> (a sample script is available on the
+I<spampd> Web page @ http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm).
+
+The options all have reasonable defaults, especially for a Postfix-centric
+installation. You may want to specify the --children option if you have an
+especially beefy or weak server box because I<spampd> is a memory-hungry
+program. Check the L<"Options"> for details on this and all other parameters.
+
+Note that I<spampd> B<replaces> I<spamd> from the I<SpamAssassin> distribution
+in function. You do not need to run I<spamd> in order for I<spampd> to work.
+This has apparently been the source of some confusion, so now you know.
+
+=head2 Postfix-specific Notes
+
+Here is a typical setup for Postfix "advanced" content filtering as described
+in the F<FILTER_README> that came with the Postfix distribution (which you
+really need to read):
+
+F</etc/postfix/master.cf>:
+
+ smtp inet n - y - - smtpd
+ -o content_filter=smtp:localhost:10025
+ -o myhostname=mx.example.com
+
+ localhost:10026 inet n - n - 10 smtpd
+ -o content_filter=
+ -o myhostname=mx-int.example.com
+
+The first entry is the main public-facing MTA which uses localhost:10025
+as the content filter for all mail. The second entry receives mail from
+the content filter and does final delivery. Both smtpd instances use
+the same Postfix F<main.cf> file. I<spampd> is the process that listens on
+localhost:10025 and then connects to the Postfix listener on localhost:10026.
+Note that the C<myhostname> options must be different between the two instances,
+otherwise Postfix will think it's talking to itself and abort sending.
+
+For the above example you can simply start I<spampd> like this:
+
+ spampd --host=localhost:10025 --relayhost=localhost:10026
+
+F<FILTER_README> from the Postfix distro has more details and examples of
+various setups, including how to skip the content filter for outbound mail.
+
+Another tip for Postfix when considering what timeout values to use for
+--childtimout and --satimeout options is the following command:
+
+C<# postconf | grep timeout>
+
+This will return a list of useful timeout settings and their values. For
+explanations see the relevant C<man> page (smtp, smtpd, lmtp). By default
+I<spampd> is set up for the default Postfix timeout values.
+
+=head1 Options
+
+=over 5
+
+=item B<--host=ip[:port] or hostname[:port]>
+
+Specifies what hostname/IP and port I<spampd> listens on. By default, it listens
+on 127.0.0.1 (localhost) on port 10025.
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025. This is an alternate to using the above --host=ip:port notation.
+
+=item B<--relayhost=ip[:port] or hostname[:port]>
+
+Specifies the hostname/IP where I<spampd> will relay all
+messages. Defaults to 127.0.0.1 (localhost). If the port is not provided, that
+defaults to 25.
+
+=item B<--relayport=n>
+
+Specifies what port I<spampd> will relay to. Default is 25. This is an
+alternate to using the above --relayhost=ip:port notation.
+
+=item B<--user=username> or B<--u=username>
+
+=item B<--group=groupname> or B<--g=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--children=n> or B<--c=n>
+
+Number of child servers to start and maintain (where n > 0). Each child will
+process up to --maxrequests (below) before exiting and being replaced by
+another child. Keep this number low on systems w/out a lot of memory.
+Default is 5 (which seems OK on a 512MB lightly loaded system). Note that
+there is always a parent process running, so if you specify 5 children you
+will actually have 6 I<spampd> processes running.
+
+You may want to set your origination mail server to limit the
+number of concurrent connections to I<spampd> to match this setting (for
+Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--childtimeout=n>
+
+This is the number of seconds to allow each child server before it times out
+a transaction. In an S/LMTP transaction the timer is reset for every command.
+This timeout includes time it would take to send the message data, so it should
+not be too short. Note that it's more likely the origination or destination
+mail servers will timeout first, which is fine. This is just a "sane" failsafe.
+Default is 360 seconds (6 minutes).
+
+=item B<--satimeout=n>
+
+This is the number of seconds to allow for processing a message with
+SpamAssassin (including feeding it the message, analyzing it, and adding
+the headers/report if necessary).
+This should be less than your origination and destination servers' timeout
+settings for the DATA command. For Postfix the default is 300 seconds in both
+cases (smtp_data_done_timeout and smtpd_timeout). In the event of timeout
+while processing the message, the problem is logged and the message is passed
+on anyway (w/out spam tagging, obviously). To fail the message with a temp
+450 error, see the --dose (die-on-sa-errors) option, below.
+Default is 285 seconds.
+
+=item B<--pid=filename> or B<--p=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--logsock=unix or inet> C<(new in v2.20)>
+
+Syslog socket to use. May be either "unix" of "inet". Default is "unix"
+except on HP-UX and SunOS (Solaris) systems which seem to prefer "inet".
+
+=item B<--nodetach> C<(new in v2.20)>
+
+If this option is given spampd won't detach from the console and fork into the
+background. This can be useful for running under control of some daemon
+management tools or when configured as a win32 service under cygrunsrv's
+control.
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KBytes. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. The size includes headers and attachments (if any).
+
+=item B<--dose>
+
+Acronym for (d)ie (o)n (s)pamAssassin (e)rrors. By default if I<spampd>
+encounters a problem with processing the message through Spam Assassin (timeout
+or other error), it will still pass the mail on to the destination server. If
+you specify this option however, the mail is instead rejected with a temporary
+error (code 450, which means the origination server should keep retrying to send
+it). See the related --satimeout option, above.
+
+=item B<--tagall> or B<--a>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings). Note that
+for this option to work as of SA-2.50, the I<always_add_report> and/or
+I<always_add_headers> settings in your SpamAssassin F<local.cf> need to be
+set to 1/true.
+
+=item B<--log-rules-hit> or B<--rh>
+
+Logs the names of each SpamAssassin rule which matched the message being
+processed. This list is returned by SA.
+
+=item B<--set-envelope-headers> or B<--seh> C<(new in v2.30)>
+
+Turns on addition of X-Envelope-To and X-Envelope-From headers to the mail
+being scanned before it is passed to SpamAssassin. The idea is to help SA
+process any blacklist/whitelist to/from directives on the actual
+sender/recipients instead of the possibly bogus envelope headers. This
+potentially exposes the list of all recipients of that mail (even BCC'ed ones).
+Therefore usage of this option is discouraged.
+
+I<NOTE>: Even though spampd tries to prevent this leakage by removing the
+X-Envelope-To header after scanning, SpamAssassin itself might add headers
+itself which report one or more of the recipients which had been listed in
+this header.
+
+=item B<--set-envelope-from> or B<--sef> C<(new in v2.30)>
+
+Same as above option but only enables the addition of X-Envelope-From header.
+For those that don't feel comfortable with the possible information exposure
+of X-Envelope-To. The above option overrides this one.
+
+=item B<--auto-whitelist> or B<--aw>
+
+This option is no longer relevant with SA version 3.0 and above, which
+controls auto whitelist use via local.cf settings.
+
+For SA version < 3.0, turns on the SpamAssassin global whitelist feature.
+See the SA docs. Note that per-user whitelists are not available.
+
+=item B<--local-only> or B<--L>
+
+Turn off all SA network-based tests (DNS, Razor, etc).
+
+=item B<--homedir=directory>
+
+Use the specified directory as home directory for the spamassassin process.
+Defaul is /var/spool/spamassassin/spampd.
+
+=item B<--saconfig=filename>
+
+Use the specified file for SpamAssassin configuration options in addition to the
+default local.cf file. Any options specified here will override the same
+option from local.cf. Default is to not use any additional configuration file.
+
+=item B<--debug> or B<--d>
+
+Turns on SpamAssassin debug messages which print to the system mail log
+(same log as spampd will log to). Also turns on more verbose logging of
+what spampd is doing (new in v2). Also increases log level of Net::Server
+to 4 (debug), adding yet more info (but not too much) (new in v2.2).
+
+=item B<--help> or B<--h>
+
+Prints usage information.
+
+=back
+
+=head2 Deprecated Options
+
+=over 5
+
+The following options are no longer used but still accepted for backwards
+compatibility with prevoius I<spampd> versions:
+
+=item B<--dead-letters>
+
+=item B<--heloname>
+
+=item B<--stop-at-threshold>
+
+=item B<--add-sc-header>
+
+=item B<--hostname>
+
+=back
+
+=head1 Examples
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 Credits
+
+I<spampd> is written and maintained by Maxim Paperno <MPaperno@WorldDesign.com>.
+See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info.
+
+I<spampd> v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+Stanley Dean Witter. These are distributed under the GNU GPL (see
+module code for more details). Both modules have been slightly modified
+from the originals and are included in this file under new names.
+
+Also thanks to Bennett Todd for the example smtpproxy script which helped create
+this version of I<spampd>. See http://bent.latency.net/smtpprox/ .
+
+I<spampd> v1 was based on code by Dave Carrigan named I<assassind>. Trace
+amounts of his code or documentation may still remain. Thanks to him for the
+original inspiration and code. See http://www.rudedog.org/assassind/ .
+
+Also thanks to I<spamd> (included with SpamAssassin) and
+I<amavisd-new> (http://www.ijs.si/software/amavisd/) for some tricks.
+
+Various people have contributed patches, bug reports, and ideas, all of whom
+I would like to thank. I have tried to include credits in code comments and
+in the change log, as appropriate.
+
+=head2 Code Contributors (in order of appearance):
+
+ Kurt Andersen
+ Roland Koeckel
+ Urban Petry
+ Sven Mueller
+
+=head1 Copyright, License, and Disclaimer
+
+I<spampd> is Copyright (c) 2002 by World Design Group, Inc. and Maxim Paperno.
+
+Portions are Copyright (C) 2001 Morgan Stanley Dean Witter as mentioned above
+in the Credits section.
+
+ 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.
+
+ The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+
+
+=head1 Bugs
+
+None known. Please report any to MPaperno@WorldDesign.com.
+
+=head1 To Do
+
+Figure out how to use Net::Server::PreFork because it has cool potential for
+load management. I tried but either I'm missing something or PreFork is
+somewhat broken in how it works. If anyone has experience here, please let
+me know.
+
+Add configurable option for rejecting mail outright based on spam score.
+It would be nice to make this program safe enough to sit in front of a mail
+server such as Postfix and be able to reject mail before it enters our systems.
+The only real problem is that Postfix will see localhost as the connecting
+client, so that disables any client-based checks Postfix can do and creates a
+possible relay hole if localhost is trusted.
+
+=head1 See Also
+
+perl(1), Spam::Assassin(3), L<http://www.spamassassin.org/>,
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>
diff --git a/previous-versions/spampd-2.40.forceuser.pl b/previous-versions/spampd-2.40.forceuser.pl
new file mode 100644
index 0000000..dac9d18
--- /dev/null
+++ b/previous-versions/spampd-2.40.forceuser.pl
@@ -0,0 +1,1592 @@
+#!/usr/bin/perl -T
+
+######################
+# SpamPD - spam proxy daemon
+#
+# v2.32 - 02-Feb-06
+# v2.30 - 31-Oct-05
+# v2.21 - 23-Oct-05
+# v2.20 - 05-Oct-04
+# v2.13 - 24-Nov-03
+# v2.12 - 15-Nov-03
+# v2.11 - 15-Jul-03
+# v2.10 - 01-Jul-03
+# v2.00 - 10-Jun-03
+# v1.0.2 - 13-Apr-03
+# v1.0.1 - 03-Feb-03
+# v1.0.0 - May 2002
+#
+# spampd is Copyright (c) 2002 by World Design Group and Maxim Paperno
+# (see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# Written and maintained by Maxim Paperno (MPaperno@WorldDesign.com)
+#
+# 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.
+#
+# The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+#
+# spampd v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+# Stanley Dean Witter. These are also distributed under the GNU GPL (see
+# module code for more details). Both modules have been slightly modified
+# from the originals and are included in this file under new names.
+#
+# spampd v1 was based on code by Dave Carrigan named assassind. Trace amounts
+# of his code or documentation may still remain. Thanks to him for the
+# original inspiration and code. (see http://www.rudedog.org/assassind/)
+#
+######################
+
+
+################################################################################
+package SpamPD::Server;
+
+# Originally known as MSDW::SMTP::Server
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =item DESCRIPTION
+#
+# This server simply gathers the SMTP acquired information (envelope
+# sender and recipient, and data) into unparsed memory buffers (or a
+# file for the data), and returns control to the caller to explicitly
+# acknowlege each command or request. Since acknowlegement or failure
+# are driven explicitly from the caller, this module can be used to
+# create a robust SMTP content scanning proxy, transparent or not as
+# desired.
+#
+# =cut
+
+use strict;
+use IO::File;
+#use IO::Socket;
+
+# =item new(interface => $interface, port => $port);
+
+# The #interface and port to listen on must be specified. The interface
+# must be a valid numeric IP address (0.0.0.0 to listen on all
+# interfaces, as usual); the port must be numeric. If this call
+# succeeds, it returns a server structure with an open
+# IO::Socket::INET in it, ready to listen on. If it fails it dies, so
+# if you want anything other than an exit with an explanatory error
+# message, wrap the constructor call in an eval block and pull the
+# error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die.
+#
+# =cut
+
+sub new {
+
+# This now emulates Net::SMTP::Server::Client for use with Net::Server which
+# passes an already open socket.
+
+ my($this, $socket) = @_;
+
+ my $class = ref($this) || $this;
+ my $self = {};
+ $self->{sock} = $socket;
+
+ bless($self, $class);
+
+ die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+ $self->{state} = 'started';
+ return $self;
+
+# Original code, removed by MP for spampd use
+#
+# my ($this, @opts) = @_;
+# my $class = ref($this) || $this;
+# my $self = bless { @opts }, $class;
+# $self->{sock} = IO::Socket::INET->new(
+# LocalAddr => $self->{interface},
+# LocalPort => $self->{port},
+# Proto => 'tcp',
+# Type => SOCK_STREAM,
+# Listen => 65536,
+# Reuse => 1,
+# );
+# die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+# $self->{state} = 'just bound',
+# return $self;
+
+}
+
+# sub accept { }
+#
+# Removed by MP; not needed for spampd use
+#
+
+
+# =item chat;
+#
+# The chat method carries the SMTP dialogue up to the point where any
+# acknowlegement must be made. If chat returns true, then its return
+# value is the previous SMTP command. If the return value begins with
+# 'mail' (case insensitive), then the attribute 'from' has been filled
+# in, and may be checked; if the return value begins with 'rcpt' then
+# both from and to have been been filled in with scalars, and should
+# be checked, then either 'ok' or 'fail' should be called to accept
+# or reject the given sender/recipient pair. If the return value is
+# 'data', then the attributes from and to are populated; in this case,
+# the 'to' attribute is a reference to an anonymous array containing
+# all the recipients for this data. If the return value is '.', then
+# the 'data' attribute (which may be pre-populated in the "new" or
+# "accept" methods if desired) is a reference to a filehandle; if it's
+# created automatically by this module it will point to an unlinked
+# tmp file in /tmp. If chat returns false, the SMTP dialogue has been
+# completed and the socket closed; this server is ready to exit or to
+# accept again, as appropriate for the server style.
+#
+# The return value from chat is also remembered inside the server
+# structure in the "state" attribute.
+#
+# =cut
+
+sub chat {
+ my ($self) = @_;
+ local(*_);
+ if ($self->{state} !~ /^data/i) {
+ return 0 unless defined($_ = $self->_getline);
+ s/[\r\n]*$//;
+ $self->{state} = $_;
+ if (s/^(l|h)?he?lo\s+//i) { # mp: find helo|ehlo|lhlo
+ # mp: determine protocol (for future use)
+ if (s/^helo\s+//i) {
+ $self->{proto} = "smtp";
+ } elsif (s/^ehlo\s+//i) {
+ $self->{proto} = "esmtp";
+ } elsif (s/^lhlo\s+//i) {
+ $self->{proto} = "lmtp";
+ }
+
+# if ( /^L/i ) {
+# $self->{proto} = "lmtp";
+# } elsif ( /^E/i ) {
+# $self->{proto} = "esmtp";
+# } else {
+# $self->{proto} = "smtp"; }
+ s/\s*$//;
+ s/\s+/ /g;
+ $self->{helo} = $_;
+ } elsif (s/^rset\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ } elsif (s/^mail\s+from:\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ s/\s*$//;
+ $self->{from} = $_;
+ } elsif (s/^rcpt\s+to:\s*//i) {
+ s/\s*$//; s/\s+/ /g;
+ $self->{to} = $_;
+ push @{$self->{recipients}}, $_;
+ } elsif (/^data/i) {
+ $self->{to} = $self->{recipients};
+ }
+ } else {
+ if (defined($self->{data})) {
+ $self->{data}->seek(0, 0);
+ $self->{data}->truncate(0);
+ } else {
+ $self->{data} = IO::File->new_tmpfile;
+ }
+ while (defined($_ = $self->_getline)) {
+ if ($_ eq ".\r\n") {
+ $self->{data}->seek(0,0);
+ return $self->{state} = '.';
+ }
+ s/^\.\./\./;
+ $self->{data}->print($_) or die "$0: write error saving data\n";
+ }
+ return(0);
+ }
+ return $self->{state};
+}
+
+# =item ok([message]);
+#
+# Approves of the data given to date, either the recipient or the
+# data, in the context of the sender [and, for data, recipients]
+# already given and available as attributes. If a message is given, it
+# will be sent instead of the internal default.
+#
+# =cut
+
+sub ok {
+ my ($self, @msg) = @_;
+ @msg = ("250 ok.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# =item fail([message]);
+#
+# Rejects the current info; if processing from, rejects the sender; if
+# processing 'to', rejects the current recipient; if processing data,
+# rejects the entire message. If a message is specified it means the
+# exact same thing as "ok" --- simply send that message to the sender.
+#
+# =cut
+
+sub fail {
+ my ($self, @msg) = @_;
+ @msg = ("550 no.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# utility functions
+
+sub _getline {
+ my ($self) = @_;
+ local ($/) = "\r\n";
+ my $tmp = $self->{sock}->getline;
+ if ( defined $self->{debug} ) {
+ $self->{debug}->print($tmp) if ($tmp);
+ }
+ return $tmp;
+}
+
+sub _print {
+ my ($self, @msg) = @_;
+ $self->{debug}->print(@msg) if defined $self->{debug};
+ $self->{sock}->print(@msg);
+}
+
+1;
+
+################################################################################
+package SpamPD::Client;
+
+# Originally known as MSDW::SMTP::Client
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =head1 DESCRIPTION
+#
+# MSDW::SMTP::Client provides a very lean SMTP client implementation;
+# the only protocol-specific knowlege it has is the structure of SMTP
+# multiline responses. All specifics lie in the hands of the calling
+# program; this makes it appropriate for a semi-transparent SMTP
+# proxy, passing commands between a talker and a listener.
+#
+# =cut
+
+use strict;
+use IO::Socket;
+
+# =item new(interface => $interface, port => $port[, timeout = 300]);
+#
+# The interface and port to talk to must be specified. The interface
+# must be a valid numeric IP address; the port must be numeric. If
+# this call succeeds, it returns a client structure with an open
+# IO::Socket::INET in it, ready to talk to. If it fails it dies,
+# so if you want anything other than an exit with an explanatory
+# error message, wrap the constructor call in an eval block and pull
+# the error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die. The timeout parameter is passed
+# on into the IO::Socket::INET constructor.
+#
+# =cut
+
+sub new {
+ my ($this, @opts) = @_;
+ my $class = ref($this) || $this;
+ my $self = bless { timeout => 300, @opts }, $class;
+ $self->{sock} = IO::Socket::INET->new(
+ PeerAddr => $self->{interface},
+ PeerPort => $self->{port},
+ Timeout => $self->{timeout},
+ Proto => 'tcp',
+ Type => SOCK_STREAM,
+ );
+ die "$0: socket connect failure: $!\n" unless defined $self->{sock};
+ return $self;
+}
+
+# =item hear
+#
+# hear collects a complete SMTP response and returns it with trailing
+# CRLF removed; for multi-line responses, intermediate CRLFs are left
+# intact. Returns undef if EOF is seen before a complete reply is
+# collected.
+#
+# =cut
+
+sub hear {
+ my ($self) = @_;
+ my ($tmp, $reply);
+ return undef unless $tmp = $self->{sock}->getline;
+ while ($tmp =~ /^\d{3}-/) {
+ $reply .= $tmp;
+ return undef unless $tmp = $self->{sock}->getline;
+ }
+ $reply .= $tmp;
+ $reply =~ s/\r\n$//;
+ return $reply;
+}
+
+# =item say("command text")
+#
+# say sends an SMTP command, appending CRLF.
+#
+# =cut
+
+sub say {
+ my ($self, @msg) = @_;
+ return unless @msg;
+ $self->{sock}->print("@msg", "\r\n") or die "$0: write error: $!";
+}
+
+# =item yammer(FILEHANDLE)
+#
+# yammer takes a filehandle (which should be positioned at the
+# beginning of the file, remember to $fh->seek(0,0) if you've just
+# written it) and sends its contents as the contents of DATA. This
+# should only be invoked after a $client->say("data") and a
+# $client->hear to collect the reply to the data command. It will send
+# the trailing "." as well. It will perform leading-dot-doubling in
+# accordance with the SMTP protocol spec, where "leading dot" is
+# defined in terms of CR-LF terminated lines --- i.e. the data should
+# contain CR-LF data without the leading-dot-quoting. The filehandle
+# will be left at EOF.
+#
+# =cut
+
+sub yammer {
+ my ($self, $fh) = (@_);
+ local (*_);
+ local ($/) = "\r\n";
+ $self->{sock}->autoflush(0); # use less writes (thx to Sam Horrocks for the tip)
+ while (<$fh>) {
+ s/^\./../;
+ $self->{sock}->print($_) or die "$0: write error: $!\n";
+ }
+ $self->{sock}->autoflush(1); # restore unbuffered socket operation
+ $self->{sock}->print(".\r\n") or die "$0: write error: $!\n";
+}
+
+1;
+
+
+################################################################################
+package SpamPD;
+
+use strict;
+use Net::Server::PreForkSimple;
+use IO::File;
+use Getopt::Long;
+use Mail::SpamAssassin;
+
+BEGIN {
+ # Load Time::HiRes if it's available
+ eval { require Time::HiRes };
+ Time::HiRes->import( qw(time) ) unless $@;
+
+ # use included modules
+ import SpamPD::Server;
+ import SpamPD::Client;
+}
+
+
+use vars qw(@ISA $VERSION);
+our @ISA = qw(Net::Server::PreForkSimple);
+our $VERSION = '2.30';
+
+sub process_message {
+ my ($self, $fh) = @_;
+
+ # output lists with a , delimeter by default
+ local ($") = ",";
+
+ # start a timer
+ my $start = time;
+ # use the assassin object created during startup
+ my $assassin = $self->{spampd}->{assassin};
+ my $sa_version = Mail::SpamAssassin::Version();
+ # $sa_version can have a non-numeric value if version_tag is
+ # set in local.cf. Only take first numeric value
+ $sa_version =~ s/([0-9]*\.[0-9]*).*/$1/;
+
+ # this gets info about the message temp file
+ my $size = ($fh->stat)[7] or die "Can't stat mail file: $!";
+
+ # Only process message under --maxsize KB
+ if ( $size < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ my (@msglines, $msgid, $sender, $recips, $tmp, $mail, $msg_resp);
+
+ my $inhdr = 1;
+ my $envfrom = 0;
+ my $envto = 0;
+ my $addedenvto = 0;
+
+ $recips = "@{$self->{smtp_server}->{to}}";
+ if ("$self->{smtp_server}->{from}" =~ /(\<.*?\>)/ ) {$sender = $1;}
+ $recips ||= "(unknown)";
+ $sender ||= "(unknown)";
+
+ ## read message into array of lines to feed to SA
+
+ # loop over message file content
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ while (<$fh>) {
+ $envto = 1 if (/^(?:X-)?Envelope-To: /);
+ $envfrom = 1 if (/^(?:X-)?Envelope-From: /);
+ if ( (/^\r?\n$/) && ($inhdr ==1) ) {
+ $inhdr = 0; # outside of msg header after first blank line
+ if ( ( $self->{spampd}->{envelopeheaders} ||
+ $self->{spampd}->{setenvelopefrom} ) &&
+ $envfrom == 0 ) {
+ push(@msglines, "X-Envelope-From: $sender\r\n");
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Added X-Envelope-From"); }
+ }
+ if ( $self->{spampd}->{envelopeheaders} && $envto == 0 ) {
+ push(@msglines, "X-Envelope-To: $recips\r\n");
+ $addedenvto = 1;
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Added X-Envelope-To"); }
+ }
+ }
+ push(@msglines, $_);
+ # find the Message-ID for logging (code is mostly from spamd)
+ if ( $inhdr && /^Message-Id:\s+(.*?)\s*$/i ) {
+ $msgid = $1;
+ while ( $msgid =~ s/\([^\(\)]*\)// ) { } # remove comments and
+ $msgid =~ s/^\s+|\s+$//g; # leading and trailing spaces
+ $msgid =~ s/\s+/ /g; # collapse whitespaces
+ $msgid =~ s/^.*?<(.*?)>.*$/$1/; # keep only the id itself
+ $msgid =~ s/[^\x21-\x7e]/?/g; # replace all weird chars
+ $msgid =~ s/[<>]/?/g; # plus all dangling angle brackets
+ $msgid =~ s/^(.+)$/<$1>/; # re-bracket the id (if not empty)
+ }
+ }
+
+ $msgid ||= "(unknown)";
+
+ $self->mylog(2, "processing message $msgid for ". $recips);
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Timed out!\n" };
+ # save previous timer and start new
+ my $previous_alarm = alarm($self->{spampd}->{satimeout});
+
+ # Audit the message
+ if ($sa_version >= 3) {
+ $mail = $assassin->parse(\@msglines, 0);
+ undef @msglines; #clear some memory-- this screws up SA < v3
+ } elsif ($sa_version >= 2.70) {
+ $mail = Mail::SpamAssassin::MsgParser->parse(\@msglines);
+ } else {
+ $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines );
+ }
+
+
+ # Check spamminess (returns Mail::SpamAssassin:PerMsgStatus object)
+ my $status = $assassin->check($mail);
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Returned from checking by SpamAssassin"); }
+
+ # Rewrite mail if high spam factor or options --tagall
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Rewriting mail using SpamAssassin"); }
+
+ # use Mail::SpamAssassin:PerMsgStatus object to rewrite message
+ if ( $sa_version >= 3 ) {
+ $msg_resp = $status->rewrite_mail;
+ } else {
+ # SA versions prior to 3 need to get the response in a different manner
+ $status->rewrite_mail;
+ $msg_resp = join '', $mail->header, "\r\n", @{$mail->body};
+ }
+
+ # Build the new message to relay
+ # pause the timeout alarm while we do this (no point in timing
+ # out here and leaving a half-written file).
+ my @resplines = split(/\r?\n/, $msg_resp);
+ my $pause_alarm = alarm(0);
+ my $inhdr = 1;
+ my $skipline = 0;
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ $fh->truncate(0) or die "Can't truncate message file: $!";
+ my $arraycont = @resplines;
+
+ for ( 0..($arraycont-1) ) {
+ $inhdr=0 if ($resplines[$_] =~ m/^\r?\n$/);
+
+ # if we are still in the header, skip over any
+ # "X-Envelope-To: " line if we have previously added it.
+ if ( $inhdr == 1 &&
+ $addedenvto == 1 &&
+ $resplines[$_] =~ m/^X-Envelope-To: .*$/) {
+ $skipline = 1;
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Removing X-Envelope-To"); }
+ }
+
+ if (! $skipline) {
+ $fh->print($resplines[$_] . "\r\n")
+ or die "Can't print to message file: $!";
+ } else {
+ $skipline = 0; }
+ }
+
+ #restart the alarm
+ alarm($pause_alarm);
+
+ }
+
+ # Log what we did
+ my $was_it_spam = 'clean message';
+ if ($status->is_spam) { $was_it_spam = 'identified spam'; }
+ my $msg_score = sprintf("%.2f",$status->get_hits);
+ my $msg_threshold = sprintf("%.2f",$status->get_required_hits);
+ my $proc_time = sprintf("%.2f", time - $start);
+
+ $self->mylog(2, "$was_it_spam $msgid ($msg_score/$msg_threshold) from $sender for ".
+ "$recips in ". $proc_time . "s, $size bytes.");
+
+ # thanks to Kurt Andersen for this idea
+ if ( $self->{spampd}->{rh} ) {
+ $self->mylog(2, "rules hit for $msgid: " . $status->get_names_of_tests_hit); }
+
+ $status->finish();
+ $mail->finish();
+
+ # set the timeout alarm back to wherever it was at
+ alarm($previous_alarm);
+
+ };
+
+ if ( $@ ne '' ) {
+ $self->mylog(1, "WARNING!! SpamAssassin error on message $msgid: $@");
+ return 0;
+ }
+
+ } else {
+
+ $self->mylog(2, "skipped large message (". $size / 1024 ."KB)");
+
+ }
+
+ return 1;
+
+}
+
+sub process_request {
+ my $self = shift;
+ my $msg;
+ my $rcpt_ok;
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Child server process timed out!\n" };
+ my $timeout = $self->{spampd}->{childtimeout};
+
+ # start a timeout alarm
+ alarm($timeout);
+
+ # start an smtp server
+ my $smtp_server = SpamPD::Server->new($self->{server}->{client});
+ unless ( defined $smtp_server ) {
+ die "Failed to create listening Server: $!"; }
+
+ $self->{smtp_server} = $smtp_server;
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Initiated Server"); }
+
+ # start an smtp "client" (really a sending server)
+ my $client = SpamPD::Client->new(interface => $self->{spampd}->{relayhost},
+ port => $self->{spampd}->{relayport});
+ unless ( defined $client ) {
+ die "Failed to create sending Client: $!"; }
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Initiated Client"); }
+
+ # pass on initial client response
+ # $client->hear can handle multiline responses so no need to loop
+ $smtp_server->ok($client->hear)
+ or die "Error in initial server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # while loop over incoming data from the server
+ while ( my $what = $smtp_server->chat ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # until end of DATA is sent, just pass the commands on transparently
+ if ($what ne '.') {
+
+ $client->say($what)
+ or die "Failure in client->say(what): $!";
+
+ # but once the data is sent now we want to process it
+ } else {
+
+ # spam checking routine - message might be rewritten here
+ my $pmrescode = $self->process_message($smtp_server->{data});
+
+ # pass on the messsage if exit code <> 0 or die-on-sa-errors flag is off
+ if ( $pmrescode or !$self->{spampd}->{dose} ) {
+
+ # need to give the client a rewound file
+ $smtp_server->{data}->seek(0,0)
+ or die "Can't rewind mail file: $!";
+
+ # now send the data on through the client
+ $client->yammer($smtp_server->{data})
+ or die "Failure in client->yammer(smtp_server->{data}): $!";
+
+ } else {
+
+ $smtp_server->ok("450 Temporary failure processing message, please try again later");
+ last;
+ }
+
+ #close the temp file
+ $smtp_server->{data}->close
+ or $self->mylog(1, "WARNING!! Couldn't close smtp_server->{data} temp file: $!");
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Finished sending DATA"); }
+ }
+
+ # pass on whatever the relayhost said in response
+ # $client->hear can handle multiline responses so no need to loop
+ my $destresp = $client->hear;
+ $smtp_server->ok($destresp)
+ or die "Error in server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Destination response: '" . $destresp . "'"); }
+
+ # if we're in data state but the response is an error, exit data state.
+ # Shold not normally occur, but can happen. Thanks to Rodrigo Ventura for bug reports.
+ if ( $smtp_server->{state} =~ /^data/i and $destresp =~ /^[45]\d{2} / ) {
+ $smtp_server->{state} = "err_after_data";
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Destination response indicates error after DATA command"); }
+ }
+
+ # patch for LMTP - multiple responses after . after DATA, done by Vladislav Kurz
+ # we have to count sucessful RCPT commands and then read the same amount of responses
+ if ( $smtp_server->{proto} eq 'lmtp' ) {
+ if ( $smtp_server->{state} =~ /^rset/i ) { $rcpt_ok=0; }
+ if ( $smtp_server->{state} =~ /^mail/i ) { $rcpt_ok=0; }
+ if ( $smtp_server->{state} =~ /^rcpt/i and $destresp =~ /^25/ ) { $rcpt_ok++; }
+ if ( $smtp_server->{state} eq '.' ) {
+ while ( --$rcpt_ok ) {
+ $destresp = $client->hear;
+ $smtp_server->ok($destresp)
+ or die "Error in server->ok(client->hear): $!";
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Destination response: '" . $destresp . "'");
+ }
+ }
+ }
+ }
+
+ # restart the timeout alarm
+ alarm($timeout);
+
+ } # server ends connection
+
+ # close connections
+ $client->{sock}->close
+ or die "Couldn't close client->{sock}: $!";
+ $smtp_server->{sock}->close
+ or die "Couldn't close smtp_server->{sock}: $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Closed connections"); }
+
+ }; # end eval block
+
+ alarm(0); # stop the timer
+ # check for error in eval block
+ if ($@ ne '') {
+ chomp($@);
+ $msg = "WARNING!! Error in process_request eval block: $@";
+ $self->mylog(0, $msg);
+ die ($msg . "\n");
+ }
+
+ $self->{spampd}->{instance}++;
+
+}
+
+# Net::Server hook
+# about to exit child process
+sub child_finish_hook {
+ my($self) = shift;
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Exiting child process after handling ".
+ $self->{spampd}->{instance} ." requests"); }
+}
+
+# older Net::Server versions (<= 0.87) die when logging a % character to Sys::Syslog
+sub mylog($$$) {
+ my ($self, $level, $msg) = @_;
+ $msg =~ s/\%/%%/g;
+ $self->log($level, $msg);
+}
+
+
+################## SETUP ######################
+
+
+my $relayhost = '127.0.0.1'; # relay to ip
+my $relayport = 25; # relay to port
+my $host = '127.0.0.1'; # listen on ip
+my $port = 10025; # listen on port
+my $children = 5; # number of child processes (servers) to spawn at start
+# my $maxchildren = $children; # max. number of child processes (servers) to spawn
+my $maxrequests = 20; # max requests handled by child b4 dying
+my $childtimeout = 6*60; # child process per-command timeout in seconds
+my $satimeout = 285; # SpamAssassin timeout in seconds (15s less than Postfix
+ # default for smtp_data_done_timeout)
+my $pidfile = '/var/run/spampd.pid'; # write pid to file
+my $user = 'mail'; # user to run as
+my $group = 'mail'; # group to run as
+my $tagall = 0; # mark-up all msgs with SA, not just spam
+my $maxsize = 64; # max. msg size to scan with SA, in KB.
+my $rh = 0; # log which rules were hit
+my $debug = 0; # debug flag
+my $dose = 0; # die-on-sa-errors flag
+my $logsock = "unix"; # default log socket (some systems like 'inet')
+my $nsloglevel = 2; # default log level for Net::Server (in the range 0-4)
+my $background = 1; # specifies whether to 'daemonize' and fork into background;
+ # apparently useful under Win32/cygwin to disable this via
+ # --nodetach option;
+my $envelopeheaders = 0; # Set X-Envelope-To and X-Envelope-From headers in the mail before
+ # passing it to spamassassin. Set to 1 to enable this.
+my $setenvelopefrom = 0; # Set X-Envelope-From header only
+my $saconfigfile = ""; # use this config file for SA settings (blank uses default local.cf)
+my $sa_home_dir = '/var/spool/spamassassin/spampd'; # home directory for SA files
+ # (auto-whitelist, plugin helpers)
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ relayport => \$relayport,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ childtimeout => \$childtimeout,
+ satimeout => \$satimeout,
+ children => \$children,
+ # maxchildren => \$maxchildren,
+ logsock => \$logsock,
+ envelopeheaders => \$envelopeheaders,
+ setenvelopefrom => \$setenvelopefrom,
+ saconfigfile => \$saconfigfile,
+ sa_home_dir => \$sa_home_dir,
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'relayport=i',
+ 'children|c=i',
+ # 'maxchildren|mc=i',
+ 'maxrequests|mr=i',
+ 'childtimeout=i',
+ 'satimeout=i',
+ 'dead-letters=s', # deprecated
+ 'user|u=s',
+ 'group|g=s',
+ 'pid|p=s',
+ 'maxsize=i',
+ 'heloname=s', # deprecated
+ 'tagall|a',
+ 'auto-whitelist|aw',
+ 'stop-at-threshold', # deprecated
+ 'debug|d',
+ 'help|h|?',
+ 'local-only|l',
+ 'log-rules-hit|rh',
+ 'dose',
+ 'add-sc-header|ash', # deprecated
+ 'hostname=s', # deprecated
+ 'logsock=s',
+ 'nodetach',
+ 'set-envelope-headers|seh',
+ 'set-envelope-from|sef',
+ 'saconfig=s',
+ 'homedir=s',
+ );
+
+usage(0) if $options{help};
+
+if ( $logsock !~ /^(unix|inet)$/ ) {
+ print "--logsock parameter needs to be either unix or inet\n\n";
+ usage(0);
+}
+
+if ( $options{tagall} ) { $tagall = 1; }
+if ( $options{'log-rules-hit'} ) { $rh = 1; }
+if ( $options{debug} ) { $debug = 1; $nsloglevel = 4; }
+if ( $options{dose} ) { $dose = 1; }
+if ( $options{'nodetach'} ) { $background = undef; }
+if ( $options{'set-envelope-headers'} ) { $envelopeheaders = 1; }
+if ( $options{'set-envelope-from'} ) { $setenvelopefrom = 1; }
+if ( $options{'saconfig'} ) { $saconfigfile = $options{'saconfig'}; }
+if ( $options{'homedir'} ) { $sa_home_dir = $options{'homedir'}; }
+# if ( !$options{maxchildren} or $maxchildren < $children ) { $maxchildren = $children; }
+
+if ( $children < 1 ) { print "Option --children must be greater than zero!\n"; exit shift; }
+
+# my $min_spare_servers = ($children == $maxchildren) ? 0 : 1;
+# my $max_spare_servers = ($min_spare_servers == 0) ? 0 : $maxchildren-1;
+
+my @tmp = split (/:/, $relayhost);
+$relayhost = $tmp[0];
+if ( $tmp[1] ) { $relayport = $tmp[1]; }
+
+@tmp = split (/:/, $host);
+$host = $tmp[0];
+if ( $tmp[1] ) { $port = $tmp[1]; }
+
+my $sa_options = {
+ 'dont_copy_prefs' => 1,
+ 'debug' => $debug,
+ 'local_tests_only' => $options{'local-only'} || 0,
+ #'home_dir_for_helpers' => $sa_home_dir,
+ #'userstate_dir' => $sa_home_dir,
+ 'username' => $user
+};
+
+my $use_user_prefs = 0;
+
+if ( $saconfigfile != "" ) {
+ $sa_options->{ 'userprefs_filename' } = $saconfigfile;
+ $use_user_prefs = 1;
+}
+
+
+#cleanup environment before starting SA (thanks to Alexander Wirt)
+$ENV{'PATH'} = '/bin:/usr/bin:/sbin:/usr/sbin';
+#delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV', 'HOME'};
+
+my $assassin = Mail::SpamAssassin->new($sa_options);
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now($use_user_prefs);
+
+# thanks to Kurt Andersen for the 'uname -s' fix
+if ( !$options{logsock} ) {
+ eval {
+ my $osname = `uname -s`;
+ if (($osname =~ 'HP-UX') || ($osname =~ 'SunOS')) {
+ $logsock = "inet";
+ }
+ };
+}
+
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ log_level => $nsloglevel,
+ syslog_logsock => $logsock,
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => $background,
+ # setsid => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ max_servers => $children,
+ max_requests => $maxrequests,
+ # min_servers => $children,
+ # max_servers => $maxchildren,
+ # min_spare_servers => $min_spare_servers,
+ # max_spare_servers => $max_spare_servers,
+ },
+ spampd => { relayhost => $relayhost,
+ relayport => $relayport,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ childtimeout => $childtimeout,
+ satimeout => $satimeout,
+ rh => $rh,
+ debug => $debug,
+ dose => $dose,
+ instance => 0,
+ envelopeheaders => $envelopeheaders,
+ setenvelopefrom => $setenvelopefrom,
+ },
+ }, 'SpamPD';
+
+# Redirect all warnings to Server::log
+$SIG{__WARN__} = sub { $server->log (2, $_[0]); };
+
+# call Net::Server to start up the daemon inside
+$server->run;
+
+exit 1; # shouldn't get here
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --host=host[:port] Hostname/IP and optional port to listen on.
+ Default is 127.0.0.1 port 10025
+ --port=n Port to listen on (alternate syntax to above).
+ --relayhost=host[:port] Host to relay mail to.
+ Default is 127.0.0.1 port 25.
+ --relayport=n Port to relay to (alternate syntax to above).
+
+ --children=n Number of child processes (servers) to start and
+ keep running. Default is 5 (plus 1 parent proc).
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Default is 20.
+ --childtimeout=n Time out children after this many seconds during
+ transactions (each S/LMTP command including the
+ time it takes to send the data).
+ Default is 360 seconds (6min).
+ --satimeout=n Time out SpamAssassin after this many seconds.
+ Default is 285 seconds.
+
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+ --nodetach Don't detach from the console and fork into
+ background. Useful for some daemon control
+ tools or when running as a win32 service
+ under cygwin.
+
+ --logsock=inet or unix Allows specifying the syslog socket type. Default is
+ 'unix' except on HPUX and SunOS which prefer 'inet'.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --dose (d)ie (o)n (s)pamAssassin (e)rrors. If this is
+ specified and SA times out or throws an error,
+ the mail will be rejected with a 450 temporary
+ error message. Default is to pass through email
+ even in the event of an SA problem.
+ --tagall Tag all messages with SA headers, not just spam.
+ --log-rules-hit Log the name of each SA test which matched the
+ or --rh current message.
+
+ --set-envelope-headers Set X-Envelope-From and X-Envelope-To headers before
+ or --seh passing the mail to SpamAssassin. This is
+ disabled by default because it potentially leaks
+ information. NOTE: Please read the manpage before
+ enabling this!
+ --set-envelope-from Same as above but only sets X-Envelope-From, for
+ or --sef those that don't feel comfortable with the
+ potential information leak.
+
+ --auto-whitelist Use the SA global auto-whitelist feature
+ or --aw (SA versions => 3.0 now control this via local.cf).
+ --local-only or -L Turn off all SA network-based tests (RBL, Razor, etc).
+ --homedir=path Use the specified directory as home directory for
+ the SpamAssassin process.
+ Default is /var/spool/spamassassin/spampd
+ --saconfig=filename Use the specified file for loading SA configuration
+ options after the default local.cf file.
+ --debug or -d Turn on SA debugging (sent to log file).
+
+ --help or -h or -? This message
+
+Deprecated Options (still accepted for backwards compatibility):
+ --heloname=hostname No longer used in spampd v.2
+ --dead-letters=path No longer used in spampd v.2
+ --stop-at-threshold No longer implemented in SpamAssassin
+EOF
+
+# --maxchildren=n Maximum number of child processes (servers) to
+# run. Default is the value of --children.
+
+ exit shift;
+}
+
+__END__
+
+# Some commented-out documentation. POD doesn't have a way to comment
+# out sections!? This documents a feature which may be implemented later.
+#
+# =item B<--maxchildren=n> or B<--mc=n>
+#
+# Maximum number of children to spawn if needed (where n >= --children). When
+# I<spampd> starts it will spawn a number of child servers as specified by
+# --children. If all those servers become busy, a new child is spawned up to the
+# number specified in --maxchildren. Default is to have --maxchildren equal to
+# --children so extra child processes aren't started. Also see the --children
+# option, above. You may want to set your origination mail server to limit the
+# number of concurrent connections to I<spampd> to match this setting (for
+# Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+# 'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+#
+# Note that extra servers after the initial --children will only spawn on very
+# busy systems. This is because the check to see if a new server is needed (ie.
+# all current ones are busy) is only done around once per minute (this is
+# controlled by the Net::Server::PreFork module, in case you want to
+# hack at it :). It can still be useful as an "overflow valve," and is
+# especially nice since the extra child servers will die off once they're not
+# needed.
+
+=pod
+
+=head1 NAME
+
+SpamPD - Spam Proxy Daemon (version 2.2)
+
+=head1 Synopsis
+
+B<spampd>
+[B<--host=host[:port]>]
+[B<--relayhost=hostname[:port]>]
+[B<--user|u=username>]
+[B<--group|g=groupname>]
+[B<--children|c=n>]
+#[B<--maxchildren|mc=n>]
+[B<--maxrequests=n>]
+[B<--childtimeout=n>]
+[B<--satimeout=n>]
+[B<--pid|p=filename>]
+[B<--nodetach>]
+[B<--logsock=inet|unix>]
+[B<--maxsize=n>]
+[B<--dose>]
+[B<--tagall|a>]
+[B<--log-rules-hit|rh>]
+[B<--set-envelope-headers|seh>]
+[B<--set-envelope-from|sef>]
+[B<--auto-whitelist|aw>]
+[B<--local-only|L>]
+[B<--saconfig=filename>]
+[B<--debug|d>]
+
+B<spampd> B<--help>
+
+=head1 Description
+
+I<spampd> is an SMTP/LMTP proxy that marks (or tags) spam using
+SpamAssassin (http://www.SpamAssassin.org/). The proxy is designed
+to be transparent to the sending and receiving mail servers and at no point
+takes responsibility for the message itself. If a failure occurs within
+I<spampd> (or SpamAssassin) then the mail servers will disconnect and the
+sending server is still responsible for retrying the message for as long
+as it is configured to do so.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+The latest version can be found at
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>.
+
+=head1 Requires
+
+=over 5
+
+Perl modules:
+
+=item B<Mail::SpamAssassin>
+
+=item B<Net::Server::PreForkSimple>
+
+=item B<IO::File>
+
+=item B<IO::Socket>
+
+=item B<Time::HiRes> (not actually required but recommended)
+
+=back
+
+=head1 Operation
+
+I<spampd> is meant to operate as an S/LMTP mail proxy which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing! It is very easy to set up an open relay this
+way.
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+
+B<Running between firewall/gateway and internal mail server>
+
+=over 3
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+ Internet -> [ MX gateway (@inter.net.host:25) ->
+ spampd (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+=back
+
+B<Using Postfix advanced content filtering>
+
+=over 3
+
+Please see the F<FILTER_README> that came with the Postfix distribution. You
+need to have a version of Postfix which supports this (ideally v.2 and up).
+
+ Internet -> [ Postfix (@inter.net.host:25) ->
+ spampd (@localhost:10025) ->
+ Postfix (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+=head1 Upgrading
+
+If upgrading from a version prior to 2.2, please note that the --add-sc-header
+option is no longer supported. Use SAs built-in header manipulation features
+instead (as of SA v2.6).
+
+Upgrading from version 1 simply involves replacing the F<spampd> program file
+with the latest one. Note that the I<dead-letters> folder is no longer being
+used and the --dead-letters option is no longer needed (though no errors are
+thrown if it's present). Check the L<"Options"> list below for a full list of new
+and deprecated options. Also be sure to check out the change log.
+
+=head1 Installation
+
+I<spampd> can be run directly from the command prompt if desired. This is
+useful for testing purposes, but for long term use you probably want to put
+it somewhere like /usr/bin or /usr/local/bin and execute it at system startup.
+For example on Red Hat-style Linux system one can use a script in
+/etc/rc.d/init.d to start I<spampd> (a sample script is available on the
+I<spampd> Web page @ http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm).
+
+The options all have reasonable defaults, especially for a Postfix-centric
+installation. You may want to specify the --children option if you have an
+especially beefy or weak server box because I<spampd> is a memory-hungry
+program. Check the L<"Options"> for details on this and all other parameters.
+
+Note that I<spampd> B<replaces> I<spamd> from the I<SpamAssassin> distribution
+in function. You do not need to run I<spamd> in order for I<spampd> to work.
+This has apparently been the source of some confusion, so now you know.
+
+=head2 Postfix-specific Notes
+
+Here is a typical setup for Postfix "advanced" content filtering as described
+in the F<FILTER_README> that came with the Postfix distribution (which you
+really need to read):
+
+F</etc/postfix/master.cf>:
+
+ smtp inet n - y - - smtpd
+ -o content_filter=smtp:localhost:10025
+ -o myhostname=mx.example.com
+
+ localhost:10026 inet n - n - 10 smtpd
+ -o content_filter=
+ -o myhostname=mx-int.example.com
+
+The first entry is the main public-facing MTA which uses localhost:10025
+as the content filter for all mail. The second entry receives mail from
+the content filter and does final delivery. Both smtpd instances use
+the same Postfix F<main.cf> file. I<spampd> is the process that listens on
+localhost:10025 and then connects to the Postfix listener on localhost:10026.
+Note that the C<myhostname> options must be different between the two instances,
+otherwise Postfix will think it's talking to itself and abort sending.
+
+For the above example you can simply start I<spampd> like this:
+
+ spampd --host=localhost:10025 --relayhost=localhost:10026
+
+F<FILTER_README> from the Postfix distro has more details and examples of
+various setups, including how to skip the content filter for outbound mail.
+
+Another tip for Postfix when considering what timeout values to use for
+--childtimout and --satimeout options is the following command:
+
+C<# postconf | grep timeout>
+
+This will return a list of useful timeout settings and their values. For
+explanations see the relevant C<man> page (smtp, smtpd, lmtp). By default
+I<spampd> is set up for the default Postfix timeout values.
+
+=head1 Options
+
+=over 5
+
+=item B<--host=ip[:port] or hostname[:port]>
+
+Specifies what hostname/IP and port I<spampd> listens on. By default, it listens
+on 127.0.0.1 (localhost) on port 10025.
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025. This is an alternate to using the above --host=ip:port notation.
+
+=item B<--relayhost=ip[:port] or hostname[:port]>
+
+Specifies the hostname/IP where I<spampd> will relay all
+messages. Defaults to 127.0.0.1 (localhost). If the port is not provided, that
+defaults to 25.
+
+=item B<--relayport=n>
+
+Specifies what port I<spampd> will relay to. Default is 25. This is an
+alternate to using the above --relayhost=ip:port notation.
+
+=item B<--user=username> or B<--u=username>
+
+=item B<--group=groupname> or B<--g=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--children=n> or B<--c=n>
+
+Number of child servers to start and maintain (where n > 0). Each child will
+process up to --maxrequests (below) before exiting and being replaced by
+another child. Keep this number low on systems w/out a lot of memory.
+Default is 5 (which seems OK on a 512MB lightly loaded system). Note that
+there is always a parent process running, so if you specify 5 children you
+will actually have 6 I<spampd> processes running.
+
+You may want to set your origination mail server to limit the
+number of concurrent connections to I<spampd> to match this setting (for
+Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--childtimeout=n>
+
+This is the number of seconds to allow each child server before it times out
+a transaction. In an S/LMTP transaction the timer is reset for every command.
+This timeout includes time it would take to send the message data, so it should
+not be too short. Note that it's more likely the origination or destination
+mail servers will timeout first, which is fine. This is just a "sane" failsafe.
+Default is 360 seconds (6 minutes).
+
+=item B<--satimeout=n>
+
+This is the number of seconds to allow for processing a message with
+SpamAssassin (including feeding it the message, analyzing it, and adding
+the headers/report if necessary).
+This should be less than your origination and destination servers' timeout
+settings for the DATA command. For Postfix the default is 300 seconds in both
+cases (smtp_data_done_timeout and smtpd_timeout). In the event of timeout
+while processing the message, the problem is logged and the message is passed
+on anyway (w/out spam tagging, obviously). To fail the message with a temp
+450 error, see the --dose (die-on-sa-errors) option, below.
+Default is 285 seconds.
+
+=item B<--pid=filename> or B<--p=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--logsock=unix or inet> C<(new in v2.20)>
+
+Syslog socket to use. May be either "unix" of "inet". Default is "unix"
+except on HP-UX and SunOS (Solaris) systems which seem to prefer "inet".
+
+=item B<--nodetach> C<(new in v2.20)>
+
+If this option is given spampd won't detach from the console and fork into the
+background. This can be useful for running under control of some daemon
+management tools or when configured as a win32 service under cygrunsrv's
+control.
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KBytes. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. The size includes headers and attachments (if any).
+
+=item B<--dose>
+
+Acronym for (d)ie (o)n (s)pamAssassin (e)rrors. By default if I<spampd>
+encounters a problem with processing the message through Spam Assassin (timeout
+or other error), it will still pass the mail on to the destination server. If
+you specify this option however, the mail is instead rejected with a temporary
+error (code 450, which means the origination server should keep retrying to send
+it). See the related --satimeout option, above.
+
+=item B<--tagall> or B<--a>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings). Note that
+for this option to work as of SA-2.50, the I<always_add_report> and/or
+I<always_add_headers> settings in your SpamAssassin F<local.cf> need to be
+set to 1/true.
+
+=item B<--log-rules-hit> or B<--rh>
+
+Logs the names of each SpamAssassin rule which matched the message being
+processed. This list is returned by SA.
+
+=item B<--set-envelope-headers> or B<--seh> C<(new in v2.30)>
+
+Turns on addition of X-Envelope-To and X-Envelope-From headers to the mail
+being scanned before it is passed to SpamAssassin. The idea is to help SA
+process any blacklist/whitelist to/from directives on the actual
+sender/recipients instead of the possibly bogus envelope headers. This
+potentially exposes the list of all recipients of that mail (even BCC'ed ones).
+Therefore usage of this option is discouraged.
+
+I<NOTE>: Even though spampd tries to prevent this leakage by removing the
+X-Envelope-To header after scanning, SpamAssassin itself might add headers
+itself which report one or more of the recipients which had been listed in
+this header.
+
+=item B<--set-envelope-from> or B<--sef> C<(new in v2.30)>
+
+Same as above option but only enables the addition of X-Envelope-From header.
+For those that don't feel comfortable with the possible information exposure
+of X-Envelope-To. The above option overrides this one.
+
+=item B<--auto-whitelist> or B<--aw>
+
+This option is no longer relevant with SA version 3.0 and above, which
+controls auto whitelist use via local.cf settings.
+
+For SA version < 3.0, turns on the SpamAssassin global whitelist feature.
+See the SA docs. Note that per-user whitelists are not available.
+
+=item B<--local-only> or B<--L>
+
+Turn off all SA network-based tests (DNS, Razor, etc).
+
+=item B<--homedir=directory>
+
+Use the specified directory as home directory for the spamassassin process.
+Things like the auto-whitelist and other plugin (razor/pyzor) files get
+written to here.
+Defaul is /var/spool/spamassassin/spampd. A good place for this is in the same
+place your bayes_path SA config setting points to (if any). Make sure this
+directory is accessible to the user that spampd is running as (default: mail).
+New in v2.40. Thanks to Alexander Wirt for this fix.
+
+=item B<--saconfig=filename>
+
+Use the specified file for SpamAssassin configuration options in addition to the
+default local.cf file. Any options specified here will override the same
+option from local.cf. Default is to not use any additional configuration file.
+
+=item B<--debug> or B<--d>
+
+Turns on SpamAssassin debug messages which print to the system mail log
+(same log as spampd will log to). Also turns on more verbose logging of
+what spampd is doing (new in v2). Also increases log level of Net::Server
+to 4 (debug), adding yet more info (but not too much) (new in v2.2).
+
+=item B<--help> or B<--h>
+
+Prints usage information.
+
+=back
+
+=head2 Deprecated Options
+
+=over 5
+
+The following options are no longer used but still accepted for backwards
+compatibility with prevoius I<spampd> versions:
+
+=item B<--dead-letters>
+
+=item B<--heloname>
+
+=item B<--stop-at-threshold>
+
+=item B<--add-sc-header>
+
+=item B<--hostname>
+
+=back
+
+=head1 Examples
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 Credits
+
+I<spampd> is written and maintained by Maxim Paperno <MPaperno@WorldDesign.com>.
+See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info.
+
+I<spampd> v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+Stanley Dean Witter. These are distributed under the GNU GPL (see
+module code for more details). Both modules have been slightly modified
+from the originals and are included in this file under new names.
+
+Also thanks to Bennett Todd for the example smtpproxy script which helped create
+this version of I<spampd>. See http://bent.latency.net/smtpprox/ .
+
+I<spampd> v1 was based on code by Dave Carrigan named I<assassind>. Trace
+amounts of his code or documentation may still remain. Thanks to him for the
+original inspiration and code. See http://www.rudedog.org/assassind/ .
+
+Also thanks to I<spamd> (included with SpamAssassin) and
+I<amavisd-new> (http://www.ijs.si/software/amavisd/) for some tricks.
+
+Various people have contributed patches, bug reports, and ideas, all of whom
+I would like to thank. I have tried to include credits in code comments and
+in the change log, as appropriate.
+
+=head2 Code Contributors (in order of appearance):
+
+ Kurt Andersen
+ Roland Koeckel
+ Urban Petry
+ Sven Mueller
+
+=head1 Copyright, License, and Disclaimer
+
+I<spampd> is Copyright (c) 2002 by World Design Group, Inc. and Maxim Paperno.
+
+Portions are Copyright (C) 2001 Morgan Stanley Dean Witter as mentioned above
+in the Credits section.
+
+ 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.
+
+ The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+
+
+=head1 Bugs
+
+None known. Please report any to MPaperno@WorldDesign.com.
+
+=head1 To Do
+
+Figure out how to use Net::Server::PreFork because it has cool potential for
+load management. I tried but either I'm missing something or PreFork is
+somewhat broken in how it works. If anyone has experience here, please let
+me know.
+
+Add configurable option for rejecting mail outright based on spam score.
+It would be nice to make this program safe enough to sit in front of a mail
+server such as Postfix and be able to reject mail before it enters our systems.
+The only real problem is that Postfix will see localhost as the connecting
+client, so that disables any client-based checks Postfix can do and creates a
+possible relay hole if localhost is trusted.
+
+=head1 See Also
+
+perl(1), Spam::Assassin(3), L<http://www.spamassassin.org/>,
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>
diff --git a/previous-versions/spampd-2.40.pl b/previous-versions/spampd-2.40.pl
new file mode 100644
index 0000000..b74f555
--- /dev/null
+++ b/previous-versions/spampd-2.40.pl
@@ -0,0 +1,1591 @@
+#!/usr/bin/perl -T
+
+######################
+# SpamPD - spam proxy daemon
+#
+# v2.32 - 02-Feb-06
+# v2.30 - 31-Oct-05
+# v2.21 - 23-Oct-05
+# v2.20 - 05-Oct-04
+# v2.13 - 24-Nov-03
+# v2.12 - 15-Nov-03
+# v2.11 - 15-Jul-03
+# v2.10 - 01-Jul-03
+# v2.00 - 10-Jun-03
+# v1.0.2 - 13-Apr-03
+# v1.0.1 - 03-Feb-03
+# v1.0.0 - May 2002
+#
+# spampd is Copyright (c) 2002 by World Design Group and Maxim Paperno
+# (see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# Written and maintained by Maxim Paperno (MPaperno@WorldDesign.com)
+#
+# 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.
+#
+# The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+#
+# spampd v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+# Stanley Dean Witter. These are also distributed under the GNU GPL (see
+# module code for more details). Both modules have been slightly modified
+# from the originals and are included in this file under new names.
+#
+# spampd v1 was based on code by Dave Carrigan named assassind. Trace amounts
+# of his code or documentation may still remain. Thanks to him for the
+# original inspiration and code. (see http://www.rudedog.org/assassind/)
+#
+######################
+
+
+################################################################################
+package SpamPD::Server;
+
+# Originally known as MSDW::SMTP::Server
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =item DESCRIPTION
+#
+# This server simply gathers the SMTP acquired information (envelope
+# sender and recipient, and data) into unparsed memory buffers (or a
+# file for the data), and returns control to the caller to explicitly
+# acknowlege each command or request. Since acknowlegement or failure
+# are driven explicitly from the caller, this module can be used to
+# create a robust SMTP content scanning proxy, transparent or not as
+# desired.
+#
+# =cut
+
+use strict;
+use IO::File;
+#use IO::Socket;
+
+# =item new(interface => $interface, port => $port);
+
+# The #interface and port to listen on must be specified. The interface
+# must be a valid numeric IP address (0.0.0.0 to listen on all
+# interfaces, as usual); the port must be numeric. If this call
+# succeeds, it returns a server structure with an open
+# IO::Socket::INET in it, ready to listen on. If it fails it dies, so
+# if you want anything other than an exit with an explanatory error
+# message, wrap the constructor call in an eval block and pull the
+# error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die.
+#
+# =cut
+
+sub new {
+
+# This now emulates Net::SMTP::Server::Client for use with Net::Server which
+# passes an already open socket.
+
+ my($this, $socket) = @_;
+
+ my $class = ref($this) || $this;
+ my $self = {};
+ $self->{sock} = $socket;
+
+ bless($self, $class);
+
+ die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+ $self->{state} = 'started';
+ return $self;
+
+# Original code, removed by MP for spampd use
+#
+# my ($this, @opts) = @_;
+# my $class = ref($this) || $this;
+# my $self = bless { @opts }, $class;
+# $self->{sock} = IO::Socket::INET->new(
+# LocalAddr => $self->{interface},
+# LocalPort => $self->{port},
+# Proto => 'tcp',
+# Type => SOCK_STREAM,
+# Listen => 65536,
+# Reuse => 1,
+# );
+# die "$0: socket bind failure: $!\n" unless defined $self->{sock};
+# $self->{state} = 'just bound',
+# return $self;
+
+}
+
+# sub accept { }
+#
+# Removed by MP; not needed for spampd use
+#
+
+
+# =item chat;
+#
+# The chat method carries the SMTP dialogue up to the point where any
+# acknowlegement must be made. If chat returns true, then its return
+# value is the previous SMTP command. If the return value begins with
+# 'mail' (case insensitive), then the attribute 'from' has been filled
+# in, and may be checked; if the return value begins with 'rcpt' then
+# both from and to have been been filled in with scalars, and should
+# be checked, then either 'ok' or 'fail' should be called to accept
+# or reject the given sender/recipient pair. If the return value is
+# 'data', then the attributes from and to are populated; in this case,
+# the 'to' attribute is a reference to an anonymous array containing
+# all the recipients for this data. If the return value is '.', then
+# the 'data' attribute (which may be pre-populated in the "new" or
+# "accept" methods if desired) is a reference to a filehandle; if it's
+# created automatically by this module it will point to an unlinked
+# tmp file in /tmp. If chat returns false, the SMTP dialogue has been
+# completed and the socket closed; this server is ready to exit or to
+# accept again, as appropriate for the server style.
+#
+# The return value from chat is also remembered inside the server
+# structure in the "state" attribute.
+#
+# =cut
+
+sub chat {
+ my ($self) = @_;
+ local(*_);
+ if ($self->{state} !~ /^data/i) {
+ return 0 unless defined($_ = $self->_getline);
+ s/[\r\n]*$//;
+ $self->{state} = $_;
+ if (s/^(l|h)?he?lo\s+//i) { # mp: find helo|ehlo|lhlo
+ # mp: determine protocol (for future use)
+ if (s/^helo\s+//i) {
+ $self->{proto} = "smtp";
+ } elsif (s/^ehlo\s+//i) {
+ $self->{proto} = "esmtp";
+ } elsif (s/^lhlo\s+//i) {
+ $self->{proto} = "lmtp";
+ }
+
+# if ( /^L/i ) {
+# $self->{proto} = "lmtp";
+# } elsif ( /^E/i ) {
+# $self->{proto} = "esmtp";
+# } else {
+# $self->{proto} = "smtp"; }
+ s/\s*$//;
+ s/\s+/ /g;
+ $self->{helo} = $_;
+ } elsif (s/^rset\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ } elsif (s/^mail\s+from:\s*//i) {
+ delete $self->{to};
+ delete $self->{data};
+ delete $self->{recipients};
+ s/\s*$//;
+ $self->{from} = $_;
+ } elsif (s/^rcpt\s+to:\s*//i) {
+ s/\s*$//; s/\s+/ /g;
+ $self->{to} = $_;
+ push @{$self->{recipients}}, $_;
+ } elsif (/^data/i) {
+ $self->{to} = $self->{recipients};
+ }
+ } else {
+ if (defined($self->{data})) {
+ $self->{data}->seek(0, 0);
+ $self->{data}->truncate(0);
+ } else {
+ $self->{data} = IO::File->new_tmpfile;
+ }
+ while (defined($_ = $self->_getline)) {
+ if ($_ eq ".\r\n") {
+ $self->{data}->seek(0,0);
+ return $self->{state} = '.';
+ }
+ s/^\.\./\./;
+ $self->{data}->print($_) or die "$0: write error saving data\n";
+ }
+ return(0);
+ }
+ return $self->{state};
+}
+
+# =item ok([message]);
+#
+# Approves of the data given to date, either the recipient or the
+# data, in the context of the sender [and, for data, recipients]
+# already given and available as attributes. If a message is given, it
+# will be sent instead of the internal default.
+#
+# =cut
+
+sub ok {
+ my ($self, @msg) = @_;
+ @msg = ("250 ok.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# =item fail([message]);
+#
+# Rejects the current info; if processing from, rejects the sender; if
+# processing 'to', rejects the current recipient; if processing data,
+# rejects the entire message. If a message is specified it means the
+# exact same thing as "ok" --- simply send that message to the sender.
+#
+# =cut
+
+sub fail {
+ my ($self, @msg) = @_;
+ @msg = ("550 no.") unless @msg;
+ $self->_print("@msg\r\n") or
+ die "$0: write error acknowledging $self->{state}: $!\n";
+}
+
+# utility functions
+
+sub _getline {
+ my ($self) = @_;
+ local ($/) = "\r\n";
+ my $tmp = $self->{sock}->getline;
+ if ( defined $self->{debug} ) {
+ $self->{debug}->print($tmp) if ($tmp);
+ }
+ return $tmp;
+}
+
+sub _print {
+ my ($self, @msg) = @_;
+ $self->{debug}->print(@msg) if defined $self->{debug};
+ $self->{sock}->print(@msg);
+}
+
+1;
+
+################################################################################
+package SpamPD::Client;
+
+# Originally known as MSDW::SMTP::Client
+#
+# This code is Copyright (C) 2001 Morgan Stanley Dean Witter, and
+# is distributed according to the terms of the GNU Public License
+# as found at <URL:http://www.fsf.org/copyleft/gpl.html>.
+#
+# Modified for use in SpamPD by Maxim Paperno (June, 2003)
+#
+# 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.
+#
+# Written by Bennett Todd <bet@rahul.net>
+
+# =head1 DESCRIPTION
+#
+# MSDW::SMTP::Client provides a very lean SMTP client implementation;
+# the only protocol-specific knowlege it has is the structure of SMTP
+# multiline responses. All specifics lie in the hands of the calling
+# program; this makes it appropriate for a semi-transparent SMTP
+# proxy, passing commands between a talker and a listener.
+#
+# =cut
+
+use strict;
+use IO::Socket;
+
+# =item new(interface => $interface, port => $port[, timeout = 300]);
+#
+# The interface and port to talk to must be specified. The interface
+# must be a valid numeric IP address; the port must be numeric. If
+# this call succeeds, it returns a client structure with an open
+# IO::Socket::INET in it, ready to talk to. If it fails it dies,
+# so if you want anything other than an exit with an explanatory
+# error message, wrap the constructor call in an eval block and pull
+# the error out of $@ as usual. This is also the case for all other
+# methods; they succeed or they die. The timeout parameter is passed
+# on into the IO::Socket::INET constructor.
+#
+# =cut
+
+sub new {
+ my ($this, @opts) = @_;
+ my $class = ref($this) || $this;
+ my $self = bless { timeout => 300, @opts }, $class;
+ $self->{sock} = IO::Socket::INET->new(
+ PeerAddr => $self->{interface},
+ PeerPort => $self->{port},
+ Timeout => $self->{timeout},
+ Proto => 'tcp',
+ Type => SOCK_STREAM,
+ );
+ die "$0: socket connect failure: $!\n" unless defined $self->{sock};
+ return $self;
+}
+
+# =item hear
+#
+# hear collects a complete SMTP response and returns it with trailing
+# CRLF removed; for multi-line responses, intermediate CRLFs are left
+# intact. Returns undef if EOF is seen before a complete reply is
+# collected.
+#
+# =cut
+
+sub hear {
+ my ($self) = @_;
+ my ($tmp, $reply);
+ return undef unless $tmp = $self->{sock}->getline;
+ while ($tmp =~ /^\d{3}-/) {
+ $reply .= $tmp;
+ return undef unless $tmp = $self->{sock}->getline;
+ }
+ $reply .= $tmp;
+ $reply =~ s/\r\n$//;
+ return $reply;
+}
+
+# =item say("command text")
+#
+# say sends an SMTP command, appending CRLF.
+#
+# =cut
+
+sub say {
+ my ($self, @msg) = @_;
+ return unless @msg;
+ $self->{sock}->print("@msg", "\r\n") or die "$0: write error: $!";
+}
+
+# =item yammer(FILEHANDLE)
+#
+# yammer takes a filehandle (which should be positioned at the
+# beginning of the file, remember to $fh->seek(0,0) if you've just
+# written it) and sends its contents as the contents of DATA. This
+# should only be invoked after a $client->say("data") and a
+# $client->hear to collect the reply to the data command. It will send
+# the trailing "." as well. It will perform leading-dot-doubling in
+# accordance with the SMTP protocol spec, where "leading dot" is
+# defined in terms of CR-LF terminated lines --- i.e. the data should
+# contain CR-LF data without the leading-dot-quoting. The filehandle
+# will be left at EOF.
+#
+# =cut
+
+sub yammer {
+ my ($self, $fh) = (@_);
+ local (*_);
+ local ($/) = "\r\n";
+ $self->{sock}->autoflush(0); # use less writes (thx to Sam Horrocks for the tip)
+ while (<$fh>) {
+ s/^\./../;
+ $self->{sock}->print($_) or die "$0: write error: $!\n";
+ }
+ $self->{sock}->autoflush(1); # restore unbuffered socket operation
+ $self->{sock}->print(".\r\n") or die "$0: write error: $!\n";
+}
+
+1;
+
+
+################################################################################
+package SpamPD;
+
+use strict;
+use Net::Server::PreForkSimple;
+use IO::File;
+use Getopt::Long;
+use Mail::SpamAssassin;
+
+BEGIN {
+ # Load Time::HiRes if it's available
+ eval { require Time::HiRes };
+ Time::HiRes->import( qw(time) ) unless $@;
+
+ # use included modules
+ import SpamPD::Server;
+ import SpamPD::Client;
+}
+
+
+use vars qw(@ISA $VERSION);
+our @ISA = qw(Net::Server::PreForkSimple);
+our $VERSION = '2.30';
+
+sub process_message {
+ my ($self, $fh) = @_;
+
+ # output lists with a , delimeter by default
+ local ($") = ",";
+
+ # start a timer
+ my $start = time;
+ # use the assassin object created during startup
+ my $assassin = $self->{spampd}->{assassin};
+ my $sa_version = Mail::SpamAssassin::Version();
+ # $sa_version can have a non-numeric value if version_tag is
+ # set in local.cf. Only take first numeric value
+ $sa_version =~ s/([0-9]*\.[0-9]*).*/$1/;
+
+ # this gets info about the message temp file
+ my $size = ($fh->stat)[7] or die "Can't stat mail file: $!";
+
+ # Only process message under --maxsize KB
+ if ( $size < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ my (@msglines, $msgid, $sender, $recips, $tmp, $mail, $msg_resp);
+
+ my $inhdr = 1;
+ my $envfrom = 0;
+ my $envto = 0;
+ my $addedenvto = 0;
+
+ $recips = "@{$self->{smtp_server}->{to}}";
+ if ("$self->{smtp_server}->{from}" =~ /(\<.*?\>)/ ) {$sender = $1;}
+ $recips ||= "(unknown)";
+ $sender ||= "(unknown)";
+
+ ## read message into array of lines to feed to SA
+
+ # loop over message file content
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ while (<$fh>) {
+ $envto = 1 if (/^(?:X-)?Envelope-To: /);
+ $envfrom = 1 if (/^(?:X-)?Envelope-From: /);
+ if ( (/^\r?\n$/) && ($inhdr ==1) ) {
+ $inhdr = 0; # outside of msg header after first blank line
+ if ( ( $self->{spampd}->{envelopeheaders} ||
+ $self->{spampd}->{setenvelopefrom} ) &&
+ $envfrom == 0 ) {
+ push(@msglines, "X-Envelope-From: $sender\r\n");
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Added X-Envelope-From"); }
+ }
+ if ( $self->{spampd}->{envelopeheaders} && $envto == 0 ) {
+ push(@msglines, "X-Envelope-To: $recips\r\n");
+ $addedenvto = 1;
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Added X-Envelope-To"); }
+ }
+ }
+ push(@msglines, $_);
+ # find the Message-ID for logging (code is mostly from spamd)
+ if ( $inhdr && /^Message-Id:\s+(.*?)\s*$/i ) {
+ $msgid = $1;
+ while ( $msgid =~ s/\([^\(\)]*\)// ) { } # remove comments and
+ $msgid =~ s/^\s+|\s+$//g; # leading and trailing spaces
+ $msgid =~ s/\s+/ /g; # collapse whitespaces
+ $msgid =~ s/^.*?<(.*?)>.*$/$1/; # keep only the id itself
+ $msgid =~ s/[^\x21-\x7e]/?/g; # replace all weird chars
+ $msgid =~ s/[<>]/?/g; # plus all dangling angle brackets
+ $msgid =~ s/^(.+)$/<$1>/; # re-bracket the id (if not empty)
+ }
+ }
+
+ $msgid ||= "(unknown)";
+
+ $self->mylog(2, "processing message $msgid for ". $recips);
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Timed out!\n" };
+ # save previous timer and start new
+ my $previous_alarm = alarm($self->{spampd}->{satimeout});
+
+ # Audit the message
+ if ($sa_version >= 3) {
+ $mail = $assassin->parse(\@msglines, 0);
+ undef @msglines; #clear some memory-- this screws up SA < v3
+ } elsif ($sa_version >= 2.70) {
+ $mail = Mail::SpamAssassin::MsgParser->parse(\@msglines);
+ } else {
+ $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines );
+ }
+
+
+ # Check spamminess (returns Mail::SpamAssassin:PerMsgStatus object)
+ my $status = $assassin->check($mail);
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Returned from checking by SpamAssassin"); }
+
+ # Rewrite mail if high spam factor or options --tagall
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Rewriting mail using SpamAssassin"); }
+
+ # use Mail::SpamAssassin:PerMsgStatus object to rewrite message
+ if ( $sa_version >= 3 ) {
+ $msg_resp = $status->rewrite_mail;
+ } else {
+ # SA versions prior to 3 need to get the response in a different manner
+ $status->rewrite_mail;
+ $msg_resp = join '', $mail->header, "\r\n", @{$mail->body};
+ }
+
+ # Build the new message to relay
+ # pause the timeout alarm while we do this (no point in timing
+ # out here and leaving a half-written file).
+ my @resplines = split(/\r?\n/, $msg_resp);
+ my $pause_alarm = alarm(0);
+ my $inhdr = 1;
+ my $skipline = 0;
+ $fh->seek(0,0) or die "Can't rewind message file: $!";
+ $fh->truncate(0) or die "Can't truncate message file: $!";
+ my $arraycont = @resplines;
+
+ for ( 0..($arraycont-1) ) {
+ $inhdr=0 if ($resplines[$_] =~ m/^\r?\n$/);
+
+ # if we are still in the header, skip over any
+ # "X-Envelope-To: " line if we have previously added it.
+ if ( $inhdr == 1 &&
+ $addedenvto == 1 &&
+ $resplines[$_] =~ m/^X-Envelope-To: .*$/) {
+ $skipline = 1;
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Removing X-Envelope-To"); }
+ }
+
+ if (! $skipline) {
+ $fh->print($resplines[$_] . "\r\n")
+ or die "Can't print to message file: $!";
+ } else {
+ $skipline = 0; }
+ }
+
+ #restart the alarm
+ alarm($pause_alarm);
+
+ }
+
+ # Log what we did
+ my $was_it_spam = 'clean message';
+ if ($status->is_spam) { $was_it_spam = 'identified spam'; }
+ my $msg_score = sprintf("%.2f",$status->get_hits);
+ my $msg_threshold = sprintf("%.2f",$status->get_required_hits);
+ my $proc_time = sprintf("%.2f", time - $start);
+
+ $self->mylog(2, "$was_it_spam $msgid ($msg_score/$msg_threshold) from $sender for ".
+ "$recips in ". $proc_time . "s, $size bytes.");
+
+ # thanks to Kurt Andersen for this idea
+ if ( $self->{spampd}->{rh} ) {
+ $self->mylog(2, "rules hit for $msgid: " . $status->get_names_of_tests_hit); }
+
+ $status->finish();
+ $mail->finish();
+
+ # set the timeout alarm back to wherever it was at
+ alarm($previous_alarm);
+
+ };
+
+ if ( $@ ne '' ) {
+ $self->mylog(1, "WARNING!! SpamAssassin error on message $msgid: $@");
+ return 0;
+ }
+
+ } else {
+
+ $self->mylog(2, "skipped large message (". $size / 1024 ."KB)");
+
+ }
+
+ return 1;
+
+}
+
+sub process_request {
+ my $self = shift;
+ my $msg;
+ my $rcpt_ok;
+
+ eval {
+
+ local $SIG{ALRM} = sub { die "Child server process timed out!\n" };
+ my $timeout = $self->{spampd}->{childtimeout};
+
+ # start a timeout alarm
+ alarm($timeout);
+
+ # start an smtp server
+ my $smtp_server = SpamPD::Server->new($self->{server}->{client});
+ unless ( defined $smtp_server ) {
+ die "Failed to create listening Server: $!"; }
+
+ $self->{smtp_server} = $smtp_server;
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Initiated Server"); }
+
+ # start an smtp "client" (really a sending server)
+ my $client = SpamPD::Client->new(interface => $self->{spampd}->{relayhost},
+ port => $self->{spampd}->{relayport});
+ unless ( defined $client ) {
+ die "Failed to create sending Client: $!"; }
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Initiated Client"); }
+
+ # pass on initial client response
+ # $client->hear can handle multiline responses so no need to loop
+ $smtp_server->ok($client->hear)
+ or die "Error in initial server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # while loop over incoming data from the server
+ while ( my $what = $smtp_server->chat ) {
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "smtp_server state: '" . $smtp_server->{state} . "'"); }
+
+ # until end of DATA is sent, just pass the commands on transparently
+ if ($what ne '.') {
+
+ $client->say($what)
+ or die "Failure in client->say(what): $!";
+
+ # but once the data is sent now we want to process it
+ } else {
+
+ # spam checking routine - message might be rewritten here
+ my $pmrescode = $self->process_message($smtp_server->{data});
+
+ # pass on the messsage if exit code <> 0 or die-on-sa-errors flag is off
+ if ( $pmrescode or !$self->{spampd}->{dose} ) {
+
+ # need to give the client a rewound file
+ $smtp_server->{data}->seek(0,0)
+ or die "Can't rewind mail file: $!";
+
+ # now send the data on through the client
+ $client->yammer($smtp_server->{data})
+ or die "Failure in client->yammer(smtp_server->{data}): $!";
+
+ } else {
+
+ $smtp_server->ok("450 Temporary failure processing message, please try again later");
+ last;
+ }
+
+ #close the temp file
+ $smtp_server->{data}->close
+ or $self->mylog(1, "WARNING!! Couldn't close smtp_server->{data} temp file: $!");
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Finished sending DATA"); }
+ }
+
+ # pass on whatever the relayhost said in response
+ # $client->hear can handle multiline responses so no need to loop
+ my $destresp = $client->hear;
+ $smtp_server->ok($destresp)
+ or die "Error in server->ok(client->hear): $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Destination response: '" . $destresp . "'"); }
+
+ # if we're in data state but the response is an error, exit data state.
+ # Shold not normally occur, but can happen. Thanks to Rodrigo Ventura for bug reports.
+ if ( $smtp_server->{state} =~ /^data/i and $destresp =~ /^[45]\d{2} / ) {
+ $smtp_server->{state} = "err_after_data";
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Destination response indicates error after DATA command"); }
+ }
+
+ # patch for LMTP - multiple responses after . after DATA, done by Vladislav Kurz
+ # we have to count sucessful RCPT commands and then read the same amount of responses
+ if ( $smtp_server->{proto} eq 'lmtp' ) {
+ if ( $smtp_server->{state} =~ /^rset/i ) { $rcpt_ok=0; }
+ if ( $smtp_server->{state} =~ /^mail/i ) { $rcpt_ok=0; }
+ if ( $smtp_server->{state} =~ /^rcpt/i and $destresp =~ /^25/ ) { $rcpt_ok++; }
+ if ( $smtp_server->{state} eq '.' ) {
+ while ( --$rcpt_ok ) {
+ $destresp = $client->hear;
+ $smtp_server->ok($destresp)
+ or die "Error in server->ok(client->hear): $!";
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Destination response: '" . $destresp . "'");
+ }
+ }
+ }
+ }
+
+ # restart the timeout alarm
+ alarm($timeout);
+
+ } # server ends connection
+
+ # close connections
+ $client->{sock}->close
+ or die "Couldn't close client->{sock}: $!";
+ $smtp_server->{sock}->close
+ or die "Couldn't close smtp_server->{sock}: $!";
+
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Closed connections"); }
+
+ }; # end eval block
+
+ alarm(0); # stop the timer
+ # check for error in eval block
+ if ($@ ne '') {
+ chomp($@);
+ $msg = "WARNING!! Error in process_request eval block: $@";
+ $self->mylog(0, $msg);
+ die ($msg . "\n");
+ }
+
+ $self->{spampd}->{instance}++;
+
+}
+
+# Net::Server hook
+# about to exit child process
+sub child_finish_hook {
+ my($self) = shift;
+ if ( $self->{spampd}->{debug} ) {
+ $self->mylog(2, "Exiting child process after handling ".
+ $self->{spampd}->{instance} ." requests"); }
+}
+
+# older Net::Server versions (<= 0.87) die when logging a % character to Sys::Syslog
+sub mylog($$$) {
+ my ($self, $level, $msg) = @_;
+ $msg =~ s/\%/%%/g;
+ $self->log($level, $msg);
+}
+
+
+################## SETUP ######################
+
+
+my $relayhost = '127.0.0.1'; # relay to ip
+my $relayport = 25; # relay to port
+my $host = '127.0.0.1'; # listen on ip
+my $port = 10025; # listen on port
+my $children = 5; # number of child processes (servers) to spawn at start
+# my $maxchildren = $children; # max. number of child processes (servers) to spawn
+my $maxrequests = 20; # max requests handled by child b4 dying
+my $childtimeout = 6*60; # child process per-command timeout in seconds
+my $satimeout = 285; # SpamAssassin timeout in seconds (15s less than Postfix
+ # default for smtp_data_done_timeout)
+my $pidfile = '/var/run/spampd.pid'; # write pid to file
+my $user = 'mail'; # user to run as
+my $group = 'mail'; # group to run as
+my $tagall = 0; # mark-up all msgs with SA, not just spam
+my $maxsize = 64; # max. msg size to scan with SA, in KB.
+my $rh = 0; # log which rules were hit
+my $debug = 0; # debug flag
+my $dose = 0; # die-on-sa-errors flag
+my $logsock = "unix"; # default log socket (some systems like 'inet')
+my $nsloglevel = 2; # default log level for Net::Server (in the range 0-4)
+my $background = 1; # specifies whether to 'daemonize' and fork into background;
+ # apparently useful under Win32/cygwin to disable this via
+ # --nodetach option;
+my $envelopeheaders = 0; # Set X-Envelope-To and X-Envelope-From headers in the mail before
+ # passing it to spamassassin. Set to 1 to enable this.
+my $setenvelopefrom = 0; # Set X-Envelope-From header only
+my $saconfigfile = ""; # use this config file for SA settings (blank uses default local.cf)
+my $sa_home_dir = '/var/spool/spamassassin/spampd'; # home directory for SA files
+ # (auto-whitelist, plugin helpers)
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ relayport => \$relayport,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ childtimeout => \$childtimeout,
+ satimeout => \$satimeout,
+ children => \$children,
+ # maxchildren => \$maxchildren,
+ logsock => \$logsock,
+ envelopeheaders => \$envelopeheaders,
+ setenvelopefrom => \$setenvelopefrom,
+ saconfigfile => \$saconfigfile,
+ sa_home_dir => \$sa_home_dir,
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'relayport=i',
+ 'children|c=i',
+ # 'maxchildren|mc=i',
+ 'maxrequests|mr=i',
+ 'childtimeout=i',
+ 'satimeout=i',
+ 'dead-letters=s', # deprecated
+ 'user|u=s',
+ 'group|g=s',
+ 'pid|p=s',
+ 'maxsize=i',
+ 'heloname=s', # deprecated
+ 'tagall|a',
+ 'auto-whitelist|aw',
+ 'stop-at-threshold', # deprecated
+ 'debug|d',
+ 'help|h|?',
+ 'local-only|l',
+ 'log-rules-hit|rh',
+ 'dose',
+ 'add-sc-header|ash', # deprecated
+ 'hostname=s', # deprecated
+ 'logsock=s',
+ 'nodetach',
+ 'set-envelope-headers|seh',
+ 'set-envelope-from|sef',
+ 'saconfig=s',
+ 'homedir=s',
+ );
+
+usage(0) if $options{help};
+
+if ( $logsock !~ /^(unix|inet)$/ ) {
+ print "--logsock parameter needs to be either unix or inet\n\n";
+ usage(0);
+}
+
+if ( $options{tagall} ) { $tagall = 1; }
+if ( $options{'log-rules-hit'} ) { $rh = 1; }
+if ( $options{debug} ) { $debug = 1; $nsloglevel = 4; }
+if ( $options{dose} ) { $dose = 1; }
+if ( $options{'nodetach'} ) { $background = undef; }
+if ( $options{'set-envelope-headers'} ) { $envelopeheaders = 1; }
+if ( $options{'set-envelope-from'} ) { $setenvelopefrom = 1; }
+if ( $options{'saconfig'} ) { $saconfigfile = $options{'saconfig'}; }
+if ( $options{'homedir'} ) { $sa_home_dir = $options{'homedir'}; }
+# if ( !$options{maxchildren} or $maxchildren < $children ) { $maxchildren = $children; }
+
+if ( $children < 1 ) { print "Option --children must be greater than zero!\n"; exit shift; }
+
+# my $min_spare_servers = ($children == $maxchildren) ? 0 : 1;
+# my $max_spare_servers = ($min_spare_servers == 0) ? 0 : $maxchildren-1;
+
+my @tmp = split (/:/, $relayhost);
+$relayhost = $tmp[0];
+if ( $tmp[1] ) { $relayport = $tmp[1]; }
+
+@tmp = split (/:/, $host);
+$host = $tmp[0];
+if ( $tmp[1] ) { $port = $tmp[1]; }
+
+my $sa_options = {
+ 'dont_copy_prefs' => 1,
+ 'debug' => $debug,
+ 'local_tests_only' => $options{'local-only'} || 0,
+ 'home_dir_for_helpers' => $sa_home_dir,
+ 'userstate_dir' => $sa_home_dir
+};
+
+my $use_user_prefs = 0;
+
+if ( $saconfigfile != "" ) {
+ $sa_options->{ 'userprefs_filename' } = $saconfigfile;
+ $use_user_prefs = 1;
+}
+
+
+#cleanup environment before starting SA (thanks to Alexander Wirt)
+$ENV{'PATH'} = '/bin:/usr/bin:/sbin:/usr/sbin';
+delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV', 'HOME'};
+
+my $assassin = Mail::SpamAssassin->new($sa_options);
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now($use_user_prefs);
+
+# thanks to Kurt Andersen for the 'uname -s' fix
+if ( !$options{logsock} ) {
+ eval {
+ my $osname = `uname -s`;
+ if (($osname =~ 'HP-UX') || ($osname =~ 'SunOS')) {
+ $logsock = "inet";
+ }
+ };
+}
+
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ log_level => $nsloglevel,
+ syslog_logsock => $logsock,
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => $background,
+ # setsid => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ max_servers => $children,
+ max_requests => $maxrequests,
+ # min_servers => $children,
+ # max_servers => $maxchildren,
+ # min_spare_servers => $min_spare_servers,
+ # max_spare_servers => $max_spare_servers,
+ },
+ spampd => { relayhost => $relayhost,
+ relayport => $relayport,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ childtimeout => $childtimeout,
+ satimeout => $satimeout,
+ rh => $rh,
+ debug => $debug,
+ dose => $dose,
+ instance => 0,
+ envelopeheaders => $envelopeheaders,
+ setenvelopefrom => $setenvelopefrom,
+ },
+ }, 'SpamPD';
+
+# Redirect all warnings to Server::log
+$SIG{__WARN__} = sub { $server->log (2, $_[0]); };
+
+# call Net::Server to start up the daemon inside
+$server->run;
+
+exit 1; # shouldn't get here
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --host=host[:port] Hostname/IP and optional port to listen on.
+ Default is 127.0.0.1 port 10025
+ --port=n Port to listen on (alternate syntax to above).
+ --relayhost=host[:port] Host to relay mail to.
+ Default is 127.0.0.1 port 25.
+ --relayport=n Port to relay to (alternate syntax to above).
+
+ --children=n Number of child processes (servers) to start and
+ keep running. Default is 5 (plus 1 parent proc).
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Default is 20.
+ --childtimeout=n Time out children after this many seconds during
+ transactions (each S/LMTP command including the
+ time it takes to send the data).
+ Default is 360 seconds (6min).
+ --satimeout=n Time out SpamAssassin after this many seconds.
+ Default is 285 seconds.
+
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+ --nodetach Don't detach from the console and fork into
+ background. Useful for some daemon control
+ tools or when running as a win32 service
+ under cygwin.
+
+ --logsock=inet or unix Allows specifying the syslog socket type. Default is
+ 'unix' except on HPUX and SunOS which prefer 'inet'.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --dose (d)ie (o)n (s)pamAssassin (e)rrors. If this is
+ specified and SA times out or throws an error,
+ the mail will be rejected with a 450 temporary
+ error message. Default is to pass through email
+ even in the event of an SA problem.
+ --tagall Tag all messages with SA headers, not just spam.
+ --log-rules-hit Log the name of each SA test which matched the
+ or --rh current message.
+
+ --set-envelope-headers Set X-Envelope-From and X-Envelope-To headers before
+ or --seh passing the mail to SpamAssassin. This is
+ disabled by default because it potentially leaks
+ information. NOTE: Please read the manpage before
+ enabling this!
+ --set-envelope-from Same as above but only sets X-Envelope-From, for
+ or --sef those that don't feel comfortable with the
+ potential information leak.
+
+ --auto-whitelist Use the SA global auto-whitelist feature
+ or --aw (SA versions => 3.0 now control this via local.cf).
+ --local-only or -L Turn off all SA network-based tests (RBL, Razor, etc).
+ --homedir=path Use the specified directory as home directory for
+ the SpamAssassin process.
+ Default is /var/spool/spamassassin/spampd
+ --saconfig=filename Use the specified file for loading SA configuration
+ options after the default local.cf file.
+ --debug or -d Turn on SA debugging (sent to log file).
+
+ --help or -h or -? This message
+
+Deprecated Options (still accepted for backwards compatibility):
+ --heloname=hostname No longer used in spampd v.2
+ --dead-letters=path No longer used in spampd v.2
+ --stop-at-threshold No longer implemented in SpamAssassin
+EOF
+
+# --maxchildren=n Maximum number of child processes (servers) to
+# run. Default is the value of --children.
+
+ exit shift;
+}
+
+__END__
+
+# Some commented-out documentation. POD doesn't have a way to comment
+# out sections!? This documents a feature which may be implemented later.
+#
+# =item B<--maxchildren=n> or B<--mc=n>
+#
+# Maximum number of children to spawn if needed (where n >= --children). When
+# I<spampd> starts it will spawn a number of child servers as specified by
+# --children. If all those servers become busy, a new child is spawned up to the
+# number specified in --maxchildren. Default is to have --maxchildren equal to
+# --children so extra child processes aren't started. Also see the --children
+# option, above. You may want to set your origination mail server to limit the
+# number of concurrent connections to I<spampd> to match this setting (for
+# Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+# 'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+#
+# Note that extra servers after the initial --children will only spawn on very
+# busy systems. This is because the check to see if a new server is needed (ie.
+# all current ones are busy) is only done around once per minute (this is
+# controlled by the Net::Server::PreFork module, in case you want to
+# hack at it :). It can still be useful as an "overflow valve," and is
+# especially nice since the extra child servers will die off once they're not
+# needed.
+
+=pod
+
+=head1 NAME
+
+SpamPD - Spam Proxy Daemon (version 2.2)
+
+=head1 Synopsis
+
+B<spampd>
+[B<--host=host[:port]>]
+[B<--relayhost=hostname[:port]>]
+[B<--user|u=username>]
+[B<--group|g=groupname>]
+[B<--children|c=n>]
+#[B<--maxchildren|mc=n>]
+[B<--maxrequests=n>]
+[B<--childtimeout=n>]
+[B<--satimeout=n>]
+[B<--pid|p=filename>]
+[B<--nodetach>]
+[B<--logsock=inet|unix>]
+[B<--maxsize=n>]
+[B<--dose>]
+[B<--tagall|a>]
+[B<--log-rules-hit|rh>]
+[B<--set-envelope-headers|seh>]
+[B<--set-envelope-from|sef>]
+[B<--auto-whitelist|aw>]
+[B<--local-only|L>]
+[B<--saconfig=filename>]
+[B<--debug|d>]
+
+B<spampd> B<--help>
+
+=head1 Description
+
+I<spampd> is an SMTP/LMTP proxy that marks (or tags) spam using
+SpamAssassin (http://www.SpamAssassin.org/). The proxy is designed
+to be transparent to the sending and receiving mail servers and at no point
+takes responsibility for the message itself. If a failure occurs within
+I<spampd> (or SpamAssassin) then the mail servers will disconnect and the
+sending server is still responsible for retrying the message for as long
+as it is configured to do so.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+The latest version can be found at
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>.
+
+=head1 Requires
+
+=over 5
+
+Perl modules:
+
+=item B<Mail::SpamAssassin>
+
+=item B<Net::Server::PreForkSimple>
+
+=item B<IO::File>
+
+=item B<IO::Socket>
+
+=item B<Time::HiRes> (not actually required but recommended)
+
+=back
+
+=head1 Operation
+
+I<spampd> is meant to operate as an S/LMTP mail proxy which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing! It is very easy to set up an open relay this
+way.
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+
+B<Running between firewall/gateway and internal mail server>
+
+=over 3
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+ Internet -> [ MX gateway (@inter.net.host:25) ->
+ spampd (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+=back
+
+B<Using Postfix advanced content filtering>
+
+=over 3
+
+Please see the F<FILTER_README> that came with the Postfix distribution. You
+need to have a version of Postfix which supports this (ideally v.2 and up).
+
+ Internet -> [ Postfix (@inter.net.host:25) ->
+ spampd (@localhost:10025) ->
+ Postfix (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+=head1 Upgrading
+
+If upgrading from a version prior to 2.2, please note that the --add-sc-header
+option is no longer supported. Use SAs built-in header manipulation features
+instead (as of SA v2.6).
+
+Upgrading from version 1 simply involves replacing the F<spampd> program file
+with the latest one. Note that the I<dead-letters> folder is no longer being
+used and the --dead-letters option is no longer needed (though no errors are
+thrown if it's present). Check the L<"Options"> list below for a full list of new
+and deprecated options. Also be sure to check out the change log.
+
+=head1 Installation
+
+I<spampd> can be run directly from the command prompt if desired. This is
+useful for testing purposes, but for long term use you probably want to put
+it somewhere like /usr/bin or /usr/local/bin and execute it at system startup.
+For example on Red Hat-style Linux system one can use a script in
+/etc/rc.d/init.d to start I<spampd> (a sample script is available on the
+I<spampd> Web page @ http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm).
+
+The options all have reasonable defaults, especially for a Postfix-centric
+installation. You may want to specify the --children option if you have an
+especially beefy or weak server box because I<spampd> is a memory-hungry
+program. Check the L<"Options"> for details on this and all other parameters.
+
+Note that I<spampd> B<replaces> I<spamd> from the I<SpamAssassin> distribution
+in function. You do not need to run I<spamd> in order for I<spampd> to work.
+This has apparently been the source of some confusion, so now you know.
+
+=head2 Postfix-specific Notes
+
+Here is a typical setup for Postfix "advanced" content filtering as described
+in the F<FILTER_README> that came with the Postfix distribution (which you
+really need to read):
+
+F</etc/postfix/master.cf>:
+
+ smtp inet n - y - - smtpd
+ -o content_filter=smtp:localhost:10025
+ -o myhostname=mx.example.com
+
+ localhost:10026 inet n - n - 10 smtpd
+ -o content_filter=
+ -o myhostname=mx-int.example.com
+
+The first entry is the main public-facing MTA which uses localhost:10025
+as the content filter for all mail. The second entry receives mail from
+the content filter and does final delivery. Both smtpd instances use
+the same Postfix F<main.cf> file. I<spampd> is the process that listens on
+localhost:10025 and then connects to the Postfix listener on localhost:10026.
+Note that the C<myhostname> options must be different between the two instances,
+otherwise Postfix will think it's talking to itself and abort sending.
+
+For the above example you can simply start I<spampd> like this:
+
+ spampd --host=localhost:10025 --relayhost=localhost:10026
+
+F<FILTER_README> from the Postfix distro has more details and examples of
+various setups, including how to skip the content filter for outbound mail.
+
+Another tip for Postfix when considering what timeout values to use for
+--childtimout and --satimeout options is the following command:
+
+C<# postconf | grep timeout>
+
+This will return a list of useful timeout settings and their values. For
+explanations see the relevant C<man> page (smtp, smtpd, lmtp). By default
+I<spampd> is set up for the default Postfix timeout values.
+
+=head1 Options
+
+=over 5
+
+=item B<--host=ip[:port] or hostname[:port]>
+
+Specifies what hostname/IP and port I<spampd> listens on. By default, it listens
+on 127.0.0.1 (localhost) on port 10025.
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025. This is an alternate to using the above --host=ip:port notation.
+
+=item B<--relayhost=ip[:port] or hostname[:port]>
+
+Specifies the hostname/IP where I<spampd> will relay all
+messages. Defaults to 127.0.0.1 (localhost). If the port is not provided, that
+defaults to 25.
+
+=item B<--relayport=n>
+
+Specifies what port I<spampd> will relay to. Default is 25. This is an
+alternate to using the above --relayhost=ip:port notation.
+
+=item B<--user=username> or B<--u=username>
+
+=item B<--group=groupname> or B<--g=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--children=n> or B<--c=n>
+
+Number of child servers to start and maintain (where n > 0). Each child will
+process up to --maxrequests (below) before exiting and being replaced by
+another child. Keep this number low on systems w/out a lot of memory.
+Default is 5 (which seems OK on a 512MB lightly loaded system). Note that
+there is always a parent process running, so if you specify 5 children you
+will actually have 6 I<spampd> processes running.
+
+You may want to set your origination mail server to limit the
+number of concurrent connections to I<spampd> to match this setting (for
+Postfix this is the C<xxxx_destination_concurrency_limit> setting where
+'xxxx' is the transport being used, usually 'smtp', and the default is 100).
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--childtimeout=n>
+
+This is the number of seconds to allow each child server before it times out
+a transaction. In an S/LMTP transaction the timer is reset for every command.
+This timeout includes time it would take to send the message data, so it should
+not be too short. Note that it's more likely the origination or destination
+mail servers will timeout first, which is fine. This is just a "sane" failsafe.
+Default is 360 seconds (6 minutes).
+
+=item B<--satimeout=n>
+
+This is the number of seconds to allow for processing a message with
+SpamAssassin (including feeding it the message, analyzing it, and adding
+the headers/report if necessary).
+This should be less than your origination and destination servers' timeout
+settings for the DATA command. For Postfix the default is 300 seconds in both
+cases (smtp_data_done_timeout and smtpd_timeout). In the event of timeout
+while processing the message, the problem is logged and the message is passed
+on anyway (w/out spam tagging, obviously). To fail the message with a temp
+450 error, see the --dose (die-on-sa-errors) option, below.
+Default is 285 seconds.
+
+=item B<--pid=filename> or B<--p=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--logsock=unix or inet> C<(new in v2.20)>
+
+Syslog socket to use. May be either "unix" of "inet". Default is "unix"
+except on HP-UX and SunOS (Solaris) systems which seem to prefer "inet".
+
+=item B<--nodetach> C<(new in v2.20)>
+
+If this option is given spampd won't detach from the console and fork into the
+background. This can be useful for running under control of some daemon
+management tools or when configured as a win32 service under cygrunsrv's
+control.
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KBytes. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. The size includes headers and attachments (if any).
+
+=item B<--dose>
+
+Acronym for (d)ie (o)n (s)pamAssassin (e)rrors. By default if I<spampd>
+encounters a problem with processing the message through Spam Assassin (timeout
+or other error), it will still pass the mail on to the destination server. If
+you specify this option however, the mail is instead rejected with a temporary
+error (code 450, which means the origination server should keep retrying to send
+it). See the related --satimeout option, above.
+
+=item B<--tagall> or B<--a>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings). Note that
+for this option to work as of SA-2.50, the I<always_add_report> and/or
+I<always_add_headers> settings in your SpamAssassin F<local.cf> need to be
+set to 1/true.
+
+=item B<--log-rules-hit> or B<--rh>
+
+Logs the names of each SpamAssassin rule which matched the message being
+processed. This list is returned by SA.
+
+=item B<--set-envelope-headers> or B<--seh> C<(new in v2.30)>
+
+Turns on addition of X-Envelope-To and X-Envelope-From headers to the mail
+being scanned before it is passed to SpamAssassin. The idea is to help SA
+process any blacklist/whitelist to/from directives on the actual
+sender/recipients instead of the possibly bogus envelope headers. This
+potentially exposes the list of all recipients of that mail (even BCC'ed ones).
+Therefore usage of this option is discouraged.
+
+I<NOTE>: Even though spampd tries to prevent this leakage by removing the
+X-Envelope-To header after scanning, SpamAssassin itself might add headers
+itself which report one or more of the recipients which had been listed in
+this header.
+
+=item B<--set-envelope-from> or B<--sef> C<(new in v2.30)>
+
+Same as above option but only enables the addition of X-Envelope-From header.
+For those that don't feel comfortable with the possible information exposure
+of X-Envelope-To. The above option overrides this one.
+
+=item B<--auto-whitelist> or B<--aw>
+
+This option is no longer relevant with SA version 3.0 and above, which
+controls auto whitelist use via local.cf settings.
+
+For SA version < 3.0, turns on the SpamAssassin global whitelist feature.
+See the SA docs. Note that per-user whitelists are not available.
+
+=item B<--local-only> or B<--L>
+
+Turn off all SA network-based tests (DNS, Razor, etc).
+
+=item B<--homedir=directory>
+
+Use the specified directory as home directory for the spamassassin process.
+Things like the auto-whitelist and other plugin (razor/pyzor) files get
+written to here.
+Defaul is /var/spool/spamassassin/spampd. A good place for this is in the same
+place your bayes_path SA config setting points to (if any). Make sure this
+directory is accessible to the user that spampd is running as (default: mail).
+New in v2.40. Thanks to Alexander Wirt for this fix.
+
+=item B<--saconfig=filename>
+
+Use the specified file for SpamAssassin configuration options in addition to the
+default local.cf file. Any options specified here will override the same
+option from local.cf. Default is to not use any additional configuration file.
+
+=item B<--debug> or B<--d>
+
+Turns on SpamAssassin debug messages which print to the system mail log
+(same log as spampd will log to). Also turns on more verbose logging of
+what spampd is doing (new in v2). Also increases log level of Net::Server
+to 4 (debug), adding yet more info (but not too much) (new in v2.2).
+
+=item B<--help> or B<--h>
+
+Prints usage information.
+
+=back
+
+=head2 Deprecated Options
+
+=over 5
+
+The following options are no longer used but still accepted for backwards
+compatibility with prevoius I<spampd> versions:
+
+=item B<--dead-letters>
+
+=item B<--heloname>
+
+=item B<--stop-at-threshold>
+
+=item B<--add-sc-header>
+
+=item B<--hostname>
+
+=back
+
+=head1 Examples
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 Credits
+
+I<spampd> is written and maintained by Maxim Paperno <MPaperno@WorldDesign.com>.
+See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info.
+
+I<spampd> v2 uses two Perl modules by Bennett Todd and Copyright (C) 2001 Morgan
+Stanley Dean Witter. These are distributed under the GNU GPL (see
+module code for more details). Both modules have been slightly modified
+from the originals and are included in this file under new names.
+
+Also thanks to Bennett Todd for the example smtpproxy script which helped create
+this version of I<spampd>. See http://bent.latency.net/smtpprox/ .
+
+I<spampd> v1 was based on code by Dave Carrigan named I<assassind>. Trace
+amounts of his code or documentation may still remain. Thanks to him for the
+original inspiration and code. See http://www.rudedog.org/assassind/ .
+
+Also thanks to I<spamd> (included with SpamAssassin) and
+I<amavisd-new> (http://www.ijs.si/software/amavisd/) for some tricks.
+
+Various people have contributed patches, bug reports, and ideas, all of whom
+I would like to thank. I have tried to include credits in code comments and
+in the change log, as appropriate.
+
+=head2 Code Contributors (in order of appearance):
+
+ Kurt Andersen
+ Roland Koeckel
+ Urban Petry
+ Sven Mueller
+
+=head1 Copyright, License, and Disclaimer
+
+I<spampd> is Copyright (c) 2002 by World Design Group, Inc. and Maxim Paperno.
+
+Portions are Copyright (C) 2001 Morgan Stanley Dean Witter as mentioned above
+in the Credits section.
+
+ 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.
+
+ The GNU GPL can be found at http://www.fsf.org/copyleft/gpl.html
+
+
+=head1 Bugs
+
+None known. Please report any to MPaperno@WorldDesign.com.
+
+=head1 To Do
+
+Figure out how to use Net::Server::PreFork because it has cool potential for
+load management. I tried but either I'm missing something or PreFork is
+somewhat broken in how it works. If anyone has experience here, please let
+me know.
+
+Add configurable option for rejecting mail outright based on spam score.
+It would be nice to make this program safe enough to sit in front of a mail
+server such as Postfix and be able to reject mail before it enters our systems.
+The only real problem is that Postfix will see localhost as the connecting
+client, so that disables any client-based checks Postfix can do and creates a
+possible relay hole if localhost is trusted.
+
+=head1 See Also
+
+perl(1), Spam::Assassin(3), L<http://www.spamassassin.org/>,
+L<http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm>
diff --git a/previous-versions/spampd.1.0.1.pl b/previous-versions/spampd.1.0.1.pl
new file mode 100644
index 0000000..fca765f
--- /dev/null
+++ b/previous-versions/spampd.1.0.1.pl
@@ -0,0 +1,584 @@
+#! /usr/bin/perl
+
+# spampd - spam proxy daemon
+#
+# v1.0.1 - minor bug fix (3-Feb-03)
+# v1.0.0 - initial release (May 2002)
+#
+# Original assassind code by and Copyright (c) 2002 Dave Carrigan
+#(see http://www.rudedog.org/assassind/)
+# Changed and renamed to spampd by Maxim Paperno (MPaperno@WorldDesign.com)
+# whose contributions are placed in the Public Domain.
+#(see http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm)
+#
+# 1.0.1 update:
+# - fixed minor but substantial bug preventing child processes
+# from exiting properly since the counter wasn't being incremented (d'oh!).
+# Thanks to Mark Blackman for pointing this out.
+#
+# - fixed typo in pod docs (Thx to James Sizemore for pointing out)
+#
+# Changes to assassind (1.0.0 initial release of spampd):
+# A different message rewriting method (using
+# Mail::SpamAssassin::NoMailAudit instead of Dave Carrigan's
+# custom headers and Mail::Audit);
+# Adding more options for message handling, network/protocol options,
+# some options to pass on to SpamAssassin (such as whitelist usage);
+# More orientation to being used as a content filter for the
+# Postfix MTA, mostly by changing some default values;
+# Documentation changes;
+#
+
+package SpamPD;
+
+use strict;
+use Net::Server::PreFork;
+use IO::File;
+use Getopt::Long;
+use Net::SMTP;
+use Net::SMTP::Server::Client;
+use Mail::SpamAssassin;
+use Mail::SpamAssassin::NoMailAudit;
+use Error qw(:try);
+
+our @ISA = qw(Net::Server::PreFork);
+our $VERSION = '1.0.1';
+
+sub dead_letter {
+ my($self, $client, $message) = @_;
+
+ my $filename = join("/", $self->{spampd}->{dead_letters},
+ sprintf("spampd.%d.%d.%f.dead", time(), $$, rand));
+
+ my $dead = IO::File->new;
+ unless ($dead->open(">$filename")) {
+ $self->log(0, "Can't open dead letter file $filename: $!");
+ return;
+ }
+ chmod 0600, $filename;
+
+ try {
+ if (defined $message) {
+ $dead->print($message, "\r\n") or
+ throw Error -text => "Can't print to dead letter: $!";
+ }
+ foreach (@{$client->{TO}}) {
+ $dead->print("TO $_\r\n") or
+ throw Error -text => "Can't print to dead letter: $!";
+ }
+ $dead->print("FROM ", $client->{FROM}, "\r\n\r\n") or
+ throw Error -text => "Can't print to dead letter: $!";
+ $dead->print($client->{MSG}) or
+ throw Error -text => "Can't print to dead letter: $!";
+ } catch Error with {
+ my $e = shift;
+ $self->log(0, "Warning!!!! Couldn't print dead letter: " . $e->stringify);
+ };
+
+ unless ($dead->close) {
+ $self->log(0, "Warning!!!! Could not close the dead letter file: $!");
+ }
+}
+
+sub relay_message {
+ my($self, $client) = @_;
+
+ my $start = time;
+ my $msg_resp;
+
+ # Now read in message
+ my $message = $client->{MSG};
+
+ # Skip processing message over n KB
+ if ( length($message) < ($self->{spampd}->{maxsize} * 1024) ) {
+
+ # prep the message (is this necessary?)
+ my @msglines = split (/\r?\n/, $message);
+ my $arraycont = @msglines; for(0..$arraycont) { $msglines[$_] .= "\r\n"; }
+
+ # Audit the message
+ my $mail = Mail::SpamAssassin::NoMailAudit->new (
+ data => \@msglines,
+ add_From_line => 0
+ );
+
+ my $assassin = $self->{spampd}->{assassin};
+ # Check spamminess
+ my $status = $assassin->check($mail);
+ # Rewrite mail if high spam factor or option --tagall
+ if ( $status->is_spam || $self->{spampd}->{tagall} ) {
+ $status->rewrite_mail;
+ }
+
+ # Build the message to send back
+ $msg_resp = join '',$mail->header,"\n",@{$mail->body};
+
+ # Log what we did, FWIW
+ my $was_it_spam;
+ if($status->is_spam) { $was_it_spam = 'identified spam'; } else { $was_it_spam = 'clean message'; }
+ my $msg_score = int($status->get_hits);
+ my $msg_threshold = int($status->get_required_hits);
+ $self->log(2, "$was_it_spam ($msg_score/$msg_threshold) in ". sprintf("%3d", time - $start) ." seconds.");
+
+ $status->finish();
+
+ } else {
+
+ $msg_resp = $message;
+ $self->log(2, "Scanning skipped due to size (". length($message) .")");
+
+ }
+
+ my $smtp = Net::SMTP->new($self->{spampd}->{relayhost}, Hello => $self->{spampd}->{heloname});
+ unless (defined $smtp) {
+ $self->log(1, "Connection to SMTP server failed");
+ $self->dead_letter($client);
+ return;
+ }
+
+ try {
+ $smtp->mail($client->{FROM});
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+
+ foreach (@{$client->{TO}}) {
+ $smtp->recipient($_);
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+ }
+
+ $smtp->data($msg_resp);
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+
+ $smtp->quit;
+ throw Error -text => sprintf("Relay failed; server said %s %s",
+ $smtp->code, $smtp->message) unless $smtp->ok;
+ $self->log(4, "Message relayed successfully.");
+ } catch Error with {
+ my $e = shift;
+ $self->dead_letter($client, $e->stringify);
+ };
+}
+
+sub process_request {
+ my $self = shift;
+ my $client = Net::SMTP::Server::Client->new($self->{server}->{client});
+ if ($client->process) {
+ $self->log(2, "Received message from '".$client->{FROM}."'");
+ $SIG{TERM} = sub {
+ $self->dead_letter($client, "Process interrupted by SIGTERM");
+ };
+ $self->relay_message($client);
+ $SIG{TERM} = sub { exit 0; };
+ } else {
+ $self->log(1, "An error occurred while receiving message");
+ }
+ $self->{spampd}->{instance} = 1 unless defined $self->{spampd}->{instance};
+ exit 0 if $self->{spampd}->{instance}++ > $self->{spampd}->{maxrequests};
+}
+
+my $relayhost = '127.0.0.1';
+my $host = '127.0.0.1';
+my $port = 10025;
+my $maxrequests = 20;
+my $dead_letters = '/var/tmp';
+my $pidfile = '/var/run/spampd.pid';
+my $user = 'mail';
+my $group = 'mail';
+my $tagall = 0;
+my $maxsize = 64;
+my $heloname = 'spampd.localdomain';
+
+my %options = (port => \$port,
+ host => \$host,
+ relayhost => \$relayhost,
+ 'dead-letters' => \$dead_letters,
+ pid => \$pidfile,
+ user => \$user,
+ group => \$group,
+ maxrequests => \$maxrequests,
+ maxsize => \$maxsize,
+ heloname => \$heloname
+ );
+
+usage(1) unless GetOptions(\%options,
+ 'port=i',
+ 'host=s',
+ 'relayhost=s',
+ 'maxrequests=i',
+ 'dead-letters=s',
+ 'user=s',
+ 'group=s',
+ 'pid=s',
+ 'maxsize=i',
+ 'heloname=s',
+ 'tagall',
+ 'auto-whitelist',
+ 'stop-at-threshold',
+ 'debug',
+ 'help');
+
+usage(0) if $options{help};
+if ( $options{tagall} ) { $tagall = 1; }
+
+my $assassin = Mail::SpamAssassin->new({
+ 'dont_copy_prefs' => 1,
+ 'stop_at_threshold' => $options{'stop_at_threshold'} || 0,
+ 'debug' => $options{'debug'} || 0 });
+
+$options{'auto-whitelist'} and eval {
+ require Mail::SpamAssassin::DBBasedAddrList;
+
+ # create a factory for the persistent address list
+ my $addrlistfactory = Mail::SpamAssassin::DBBasedAddrList->new();
+ $assassin->set_persistent_address_list_factory ($addrlistfactory);
+};
+
+$assassin->compile_now();
+$/ = "\n"; # argh, Razor resets this! Bad Razor!
+
+my $server = bless {
+ server => {host => $host,
+ port => [ $port ],
+ log_file => 'Sys::Syslog',
+ syslog_ident => 'spampd',
+ syslog_facility => 'mail',
+ background => 1,
+ pid_file => $pidfile,
+ user => $user,
+ group => $group,
+ },
+ spampd => {maxrequests => $maxrequests,
+ relayhost => $relayhost,
+ dead_letters => $dead_letters,
+ tagall => $tagall,
+ maxsize => $maxsize,
+ assassin => $assassin,
+ heloname => $heloname,
+ },
+ }, 'SpamPD';
+$server->run;
+
+sub usage {
+ print <<EOF ;
+usage: $0 [ options ]
+
+Options:
+ --port=n Port to listen on. Defaults to 10025.
+ --host=host Hostname/IP to listen on. Default is 127.0.0.1
+ --relayhost=host[:port] Host to relay mail to.
+ Defaults to 127.0.0.1 on port 25.
+ --heloname=hostname Hostname to use in HELO command when sending mail.
+ Defaults to 'spampd.localdomain'.
+
+ --maxrequests=n Maximum requests that each child can process before
+ exiting. Defaults to 20.
+ --pid=filename Store the daemon's process ID in this file.
+ Default is /var/run/spampd.pid
+ --user=username Specifies the user that the daemon runs as.
+ Default is mail.
+ --group=groupname Specifies the group that the daemon runs as.
+ Default is mail.
+ --dead-letters=path Path to store letters that couldn't be relayed.
+ Defaults to /var/tmp.
+
+ --maxsize=n Maximum size of mail to scan (in KB).
+ Default is 64KB.
+ --tagall Tag all messages with a header, not just spam.
+
+ --auto-whitelist Use the SA global auto-whitelist feature.
+ --stop-at-threshold Use SA feature to stop scanning once score
+ threshold is reached.
+ --debug Turn on SA debugging.
+
+ --help This message
+EOF
+ exit shift;
+}
+
+=pod
+
+=head1 NAME
+
+spampd - Spam Proxy Daemon
+
+=head1 SYNOPSIS
+
+B<spampd>
+[B<--port=n>]
+[B<--host=host>]
+[B<--relayhost=hostname[:port]>]
+[B<--heloname=hostname>]
+[B<--user=username>]
+[B<--group=groupname>]
+[B<--maxrequests=n>]
+[B<--dead-letters=/path>]
+[B<--pid=filename>]
+[B<--maxsize=n>]
+[B<--tagall>]
+[B<--auto-whitelist>]
+[B<--stop-at-threshold>]
+[B<--debug>]
+
+B<spampd> B<--help>
+
+=head1 DESCRIPTION
+
+I<spampd> is a relaying SMTP proxy that filters spam using
+SpamAssassin (http://www.SpamAssassin.org). The proxy is designed
+to be robust in the face of exceptional errors, and will (hopefully)
+never lose a message.
+
+I<spampd> uses SpamAssassin to modify (tag) relayed messages based on
+their spam score, so all SA settings apply. This is described in the SA
+documentation. I<spampd> will by default only tell SA to tag a
+message if it exceeds the spam threshold score, however you can have
+it rewrite all messages passing through by adding the --tagall option
+(see SA for how non-spam messages are tagged).
+
+I<spampd> logs all aspects of its operation to syslog(8), using the
+mail syslog facility.
+
+=head1 REQUIRES
+
+Perl modules:
+
+B<Error>
+
+B<Mail::SpamAssassin>
+
+B<Net::Server>
+
+B<Net::SMTP>
+
+
+=head1 OPERATION
+
+I<spampd> is meant to operate as an SMTP mail relay which passes
+each message through SpamAssassin for analysis. Note that I<spampd>
+does not do anything other than check for spam, so it is not suitable as
+an anti-relay system. It is meant to work in conjunction with your
+regular mail system. Typically one would pipe any messages they wanted
+scanned through I<spampd> after initial acceptance by your MX host.
+This is especially useful for using Postfix's (http://www.postfix.org)
+advanced content filtering mechanism, although certainly not limited to
+that application.
+
+Please re-read the second sentence in the above paragraph. You should NOT
+enable I<spampd> to listen on a public interface (IP address) unless you
+know exactly what you're doing!
+
+Here are some simple examples (square brackets in the "diagrams" indicate
+physical machines):
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+The firewall/gateway MTA would be configured to forward all of its mail
+to the port that I<spampd> listens on, and I<spampd> would relay its
+messages to port 25 of your internal server. I<spampd> could either
+run on its own host (and listen on any port) or it could run on either
+mail server (and listen on any port except port 25).
+
+Internet -> [ MX gateway (@inter.net.host:25) ->
+ I<spampd> (@localhost:2025) ] ->
+ Internal mail (@private.host.ip:25)
+
+
+=item Using Postfix advanced content filtering
+
+Please see the FILTER_README that came with the Postfix distribution. You
+need to have a version of Postfix which supports this.
+
+Internet -> [ I<Postfix> (@inter.net.host:25) ->
+ I<spampd> (@localhost:10025) ->
+ I<Postfix> (@localhost:10026) ] -> final delivery
+
+=back
+
+Note that these examples only show incoming mail delivery. Since it is
+usually unnecessary to scan mail coming from your network (right?),
+it may be desirable to set up a separate outbound route which bypasses
+I<spampd>.
+
+=head1 OPTIONS
+
+=over 5
+
+=item B<--port=n>
+
+Specifies what port I<spampd> listens on. By default, it listens on
+port 10025.
+
+=item B<--host=ip>
+
+Specifies what interface/IP I<spampd> listens on. By default, it listens on
+127.0.0.1 (localhost).
+
+B<Important!> You should NOT enable I<spampd> to listen on a
+public interface (IP address) unless you know exactly what you're doing!
+
+=item B<--relayhost=hostname[:port]>
+
+Specifies the hostname where I<spampd> will relay all
+messages. Defaults to 127.0.0.1. If the port is not provided, that
+defaults to 25.
+
+=item B<--heloname=hostname>
+
+Hostname to use in HELO command when sending mail. Default is
+'spampd.localdomain'. The HELO name may show up in the
+Received headers of any processed message, depending on your setup.
+
+=item B<--user=username>
+
+=item B<--group=groupname>
+
+Specifies the user and group that the proxy will run as. Default is
+I<mail>/I<mail>.
+
+=item B<--maxrequests=n>
+
+I<spampd> works by forking child servers to handle each message. The
+B<maxrequests> parameter specifies how many requests will be handled
+before the child exits. Since a child never gives back memory, a large
+message can cause it to become quite bloated; the only way to reclaim
+the memory is for the child to exit. The default is 20.
+
+=item B<--dead-letters=/path>
+
+Specifies the directory where I<spampd> will store any message that
+it fails to deliver. The default is F</var/tmp>. You should periodically
+examine this directory to see if there are any messages that couldn't be
+delivered.
+
+B<Important!> This path should not be on the same partition as your mail
+server's message spool, because if your mail server rejects a message
+because of a full disk, I<spampd> will not be able to save the
+message, and it will be lost.
+
+=item B<--pid=filename>
+
+Specifies a filename where I<spampd> will write its process ID so
+that it is easy to kill it later. The directory that will contain this
+file must be writable by the I<spampd> user. The default is
+F</var/run/spampd.pid>.
+
+=item B<--tagall>
+
+Tells I<spampd> to have SpamAssassin add headers to all scanned mail,
+not just spam. By default I<spampd> will only rewrite messages which
+exceed the spam threshold score (as defined in the SA settings).
+
+=item B<--maxsize=n>
+
+The maximum message size to send to SpamAssassin, in KB. By default messages
+over 64KB are not scanned at all, and an appropriate message is logged
+indicating this. This includes headers.
+
+=item B<--auto-whitelist>
+
+Turns on the SpamAssassin global whitelist feature. See the SA docs. Note
+that per-user whitelists are not available.
+
+=item B<--stop-at-threshold>
+
+Turns on the SpamAssassin (v2.20 and up) "stop at threshold" feature which
+stops any further scanning of a message once the minimum spam score
+is reached. See the SA docs for more info.
+
+=item B<--debug>
+
+Turns on SpamAssassin debug messages.
+
+=item B<--help>
+
+Prints usage information.
+
+=back
+
+=head1 EXAMPLES
+
+=over 5
+
+=item Running between firewall/gateway and internal mail server
+
+
+I<spampd> listens on port 10025 on the same host as the internal mail server.
+
+ spampd --host=192.168.1.10
+
+Same as above but I<spampd> runs on port 10025 of the same host as
+the firewall/gateway and passes messages on to the internal mail server
+on another host.
+
+ spampd --relayhost=192.168.1.10
+
+=item Using Postfix advanced content filtering example
+and the SA auto-whitelist feature
+
+ spampd --port=10025 --relayhost=127.0.0.1:10026 --auto-whitelist
+
+=back
+
+=head1 AUTHORS
+
+Based on I<assassind> by Dave Carrigan, <dave@rudedog.org>
+see http://www.rudedog.org/assassind/
+
+Modified and renamed to I<spampd> (to avoid confusion) by
+Maxim Paperno, <MPaperno@WorldDesign.com>. My modifications are mostly
+based on code included with the SpamAssassin distribution, namely spamd
+and spamproxy.
+
+=head1 COPYRIGHT AND DISCLAIMER
+
+Portions of this program are Copyright © 2002, Dave Carrigan, all rights
+reserved. Other contributions can be considered Public Domain property.
+This program is free software; you can redistribute it and/or
+modify it under the same terms as Perl.
+
+This program is distributed "as is", without warranty of any kind,
+either expressed or implied, including, but not limited to, the implied
+warranties of merchantability and fitness for a particular purpose. The
+entire risk as to the quality and performance of the program is with
+you. Should the program prove defective, you assume the cost of all
+necessary servicing, repair or correction.
+
+=head1 BUGS
+
+Due to the nature of Perl's SMTP::Server module, an SMTP message is
+stored completely in memory. However, as soon as the module receives its
+entire message data from the SMTP client, it returns a 250, signifying
+to the client that the message has been delivered. This means
+that there is a period of time where the message is vulnerable to being
+lost if the I<spampd> process is killed before it has relayed or
+saved the message. Caveat Emptor!
+
+No message loop protection.
+
+Net::SMTP::Server::Client has a "problem" with spaces in email addresses.
+For example during the SMTP dialog, if a mail is
+FROM:<"some spammer"@some.dom.ain> the address gets truncated after
+the first space to just '<"some' . This causes a problem when relaying
+the message to the receiving server, because the sender address is now
+in an illegal format. The mail is then rejected, and it ends
+up in the dead-letters directory. I have actually seen this happen several
+times, and of course they were bogus messages each time. I don't believe
+there are any legitimate envelope email addresses with spaces in them,
+so don't see this as much of an issue (except that it's un elegant).
+
+
+=head1 TO DO
+
+Add option for extracting recipient address(es) and using SpamAssassin's
+SQL lookup capability check for user-specific preferences.
+
+Deal with above bugs.
+
+=head1 SEE ALSO
+
+perl(1), Spam::Assassin(3), http://www.spamassassin.org/,
+http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm, http://www.rudedog.org/assassind/