From c3c8a5107d39e3b7980d5aa25ad77cacd8b77085 Mon Sep 17 00:00:00 2001 From: mpaperno Date: Thu, 30 Aug 2012 00:01:15 -0400 Subject: Old versions archived. --- previous-versions/spampd-0.0.1.pl | 523 +++++++++ previous-versions/spampd-0.0.5.pl | 510 +++++++++ previous-versions/spampd-1.0.2.pl | 595 +++++++++++ previous-versions/spampd-2.00.pl | 1071 +++++++++++++++++++ previous-versions/spampd-2.10.pl | 1392 ++++++++++++++++++++++++ previous-versions/spampd-2.11.pl | 1401 ++++++++++++++++++++++++ previous-versions/spampd-2.12.pl | 1413 ++++++++++++++++++++++++ previous-versions/spampd-2.13.pl | 1417 +++++++++++++++++++++++++ previous-versions/spampd-2.2.pl | 1418 +++++++++++++++++++++++++ previous-versions/spampd-2.30.pl | 1506 ++++++++++++++++++++++++++ previous-versions/spampd-2.32.pl | 1559 +++++++++++++++++++++++++++ previous-versions/spampd-2.40.forceuser.pl | 1592 ++++++++++++++++++++++++++++ previous-versions/spampd-2.40.pl | 1591 +++++++++++++++++++++++++++ previous-versions/spampd.1.0.1.pl | 584 ++++++++++ 14 files changed, 16572 insertions(+) create mode 100644 previous-versions/spampd-0.0.1.pl create mode 100644 previous-versions/spampd-0.0.5.pl create mode 100644 previous-versions/spampd-1.0.2.pl create mode 100644 previous-versions/spampd-2.00.pl create mode 100644 previous-versions/spampd-2.10.pl create mode 100644 previous-versions/spampd-2.11.pl create mode 100644 previous-versions/spampd-2.12.pl create mode 100644 previous-versions/spampd-2.13.pl create mode 100644 previous-versions/spampd-2.2.pl create mode 100644 previous-versions/spampd-2.30.pl create mode 100644 previous-versions/spampd-2.32.pl create mode 100644 previous-versions/spampd-2.40.forceuser.pl create mode 100644 previous-versions/spampd-2.40.pl create mode 100644 previous-versions/spampd.1.0.1.pl 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 <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 + +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 + +Optionally specifies the port number for the server to listen on. + +=item B<-t> I + +Use specified IP address as relay (To) host (default: 127.0.0.1) + +=item B<-v> I + +Use specified port on the relay host specified with -t (default: 10026) + +=item B<-g> I + +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 + +Specify the syslog facility to use (default: mail). + +=item B<-u> I + +Run as the named user. The alternative, default behaviour is to setuid() to +the user running C, if C 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. + +=item B<-P> + +Die on user errors (for the user passed from spamc) instead of falling back to +user I 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 EMPaperno@worldDesign.comE + +=head1 CREDITS + +Justin Mason and Craig Hughes for B +and B + +Ian R. Justman for his B implementation + +Habeeb J. "MacGyver" Dihu for his B code + +Bennett Todd for the perforking code and option-parsing code from his + pacakge, smtpproxy (used via spamproxyd code) + +=head1 PREREQUISITES + +C +C + +=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 < +[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 B<--help> + +=head1 DESCRIPTION + +I 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 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 adds is the B +header. This header will have one of five values: I, I, +I, I and I. 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 also adds a B 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 adds one or more B headers, which +contain a plain-text report of the rules that SpamAssassin used to +assign the message its score. + +I logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +=head1 OPERATION + +I 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 listens on, and I would relay its messages +to port 25 of your internal server. I 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 default +mode of operation. + +=item Running on the firewall with an internal mail server + +I would accept messages on port 25 and forward them to the +mail server that is also listening on port 25. Note that I +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 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 listens on. By default, it listens on +port 2025. + +=item B<--relayhost=hostname[:port]> + +Specifies the hostname where I will relay all +messages. Defaults to I. 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/I. + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 will store any message that +it fails to deliver. The default is F. You should periodically +examine this directory to see if there are any messages that couldn't be +delivered. + +B 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 will not be able to save the +message, and it will be lost. + +=item B<--pid=filename> + +Specifies a filename where I 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 user. The default is +F. + +=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'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, + +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 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 < +[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 B<--help> + +=head1 DESCRIPTION + +I 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 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 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 logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +=head1 REQUIRES + +Perl modules: + +B + +B + +B + +B + + +=head1 OPERATION + +I is meant to operate as an SMTP mail relay which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 listens on, and I would relay its +messages to port 25 of your internal server. I 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 (@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 (@inter.net.host:25) -> + I (@localhost:10025) -> + I (@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. + +=head1 OPTIONS + +=over 5 + +=item B<--port=n> + +Specifies what port I listens on. By default, it listens on +port 10025. + +=item B<--host=ip> + +Specifies what interface/IP I listens on. By default, it listens on +127.0.0.1 (localhost). + +B You should NOT enable I 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 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/I. + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 will store any message that +it fails to deliver. The default is F. You should periodically +examine this directory to see if there are any messages that couldn't be +delivered. + +B 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 will not be able to save the +message, and it will be lost. + +=item B<--pid=filename> + +Specifies a filename where I 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 user. The default is +F. + +=item B<--tagall> + +Tells I to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 by Dave Carrigan, +see http://www.rudedog.org/assassind/ + +Modified and renamed to I (to avoid confusion) by +Maxim Paperno, . 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 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 . +# +# 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 + +# =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 . +# +# 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 + +# =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 < +[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 B<--help> + +=head1 Description + +I 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 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 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 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 + +=item B + +=item B + +=item B + +=back + +=head1 Operation + +I is meant to operate as an SMTP mail proxy which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 U I from the I distribution +in function. You do not need to run I in order for I to function. + +Here are some simple examples (square brackets in the "diagrams" indicate +physical machines): + + +B + +=over 3 + +The firewall/gateway MTA would be configured to forward all of its mail +to the port that I listens on, and I would relay its +messages to port 25 of your internal server. I 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 (@localhost:2025) ] -> + Internal mail (@private.host.ip:25) + +=back + +B + +=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 (@inter.net.host:25) -> + I (@localhost:10025) -> + I (@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. + + +=head1 Installation + +I 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 (a sample script is available on the +I Web page @ http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm). + +Note that I B I from the I distribution +in function. You do not need to run I in order for I 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: + + 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 file. I is the process that listens on +localhost:10025 and then connects to the Postfix listener on localhost:10026. +Note that the C 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 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 listens on. By default, it listens +on 127.0.0.1 (localhost) on port 10025. + +B You should NOT enable I to listen on a +public interface (IP address) unless you know exactly what you're doing! + +=item B<--port=n> + +Specifies what port I 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 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 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/I. + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 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 user. The default is +F. + +=item B<--tagall> + +Tells I to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 is written and maintained by Maxim Paperno . +See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info. + +I 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. See http://bent.latency.net/smtpprox/ . + +I 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 (included with SpamAssassin) and +I (http://www.ijs.si/software/amavisd/) for some tricks. + +=head1 Copyright and Disclaimer + +I 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, +L 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 . +# +# 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 + +# =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 . +# +# 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 + +# =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 < or B<--mc=n> C<(new in v2)> +# +# Maximum number of children to spawn if needed (where n >= --children). When +# I 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 to match this setting (for +# Postfix this is the C 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 +[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 B<--help> + +=head1 Description + +I 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 (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 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 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 logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +The latest version can be found at +L. + +=head1 Requires + +=over 5 + +Perl modules: + +=item B + +=item B + +=item B + +=item B + +=item B (not actually required but recommended) + +=back + +=head1 Operation + +I is meant to operate as an S/LMTP mail proxy which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 + +=over 3 + +The firewall/gateway MTA would be configured to forward all of its mail +to the port that I listens on, and I would relay its +messages to port 25 of your internal server. I 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 + +=over 3 + +Please see the F 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. + +=head1 Upgrading + +Upgrading from version 1 simply involves replacing the F program file +with the latest one. Note that the I 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 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 (a sample script is available on the +I 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 is a memory-hungry +program. Check the L<"Options"> for details on this and all other parameters. + +Note that I B I from the I distribution +in function. You do not need to run I in order for I 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 that came with the Postfix distribution (which you +really need to read): + +F: + + 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 file. I is the process that listens on +localhost:10025 and then connects to the Postfix listener on localhost:10026. +Note that the C 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 like this: + + spampd --host=localhost:10025 --relayhost=localhost:10026 + +F 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 page (smtp, smtpd, lmtp). By default +I 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 listens on. By default, it listens +on 127.0.0.1 (localhost) on port 10025. + +B You should NOT enable I to listen on a +public interface (IP address) unless you know exactly what you're doing! + +=item B<--port=n> + +Specifies what port I 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 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 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/I. + +=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 processes running. + +You may want to set your origination mail server to limit the +number of concurrent connections to I to match this setting (for +Postfix this is the C setting where +'xxxx' is the transport being used, usually 'smtp', and the default is 100). + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 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 user. The default is +F. + +=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 +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 to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 and/or +I settings in your SpamAssassin F 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 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 is written and maintained by Maxim Paperno . +See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info. + +I 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. See http://bent.latency.net/smtpprox/ . + +I v1 was based on code by Dave Carrigan named I. 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 (included with SpamAssassin) and +I (http://www.ijs.si/software/amavisd/) for some tricks. + +=head1 Copyright, License, and Disclaimer + +I 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, +L 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 . +# +# 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 + +# =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 . +# +# 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 + +# =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 < or B<--mc=n> C<(new in v2)> +# +# Maximum number of children to spawn if needed (where n >= --children). When +# I 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 to match this setting (for +# Postfix this is the C 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 +[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 B<--help> + +=head1 Description + +I 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 (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 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 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 logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +The latest version can be found at +L. + +=head1 Requires + +=over 5 + +Perl modules: + +=item B + +=item B + +=item B + +=item B + +=item B (not actually required but recommended) + +=back + +=head1 Operation + +I is meant to operate as an S/LMTP mail proxy which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 + +=over 3 + +The firewall/gateway MTA would be configured to forward all of its mail +to the port that I listens on, and I would relay its +messages to port 25 of your internal server. I 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 + +=over 3 + +Please see the F 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. + +=head1 Upgrading + +Upgrading from version 1 simply involves replacing the F program file +with the latest one. Note that the I 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 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 (a sample script is available on the +I 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 is a memory-hungry +program. Check the L<"Options"> for details on this and all other parameters. + +Note that I B I from the I distribution +in function. You do not need to run I in order for I 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 that came with the Postfix distribution (which you +really need to read): + +F: + + 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 file. I is the process that listens on +localhost:10025 and then connects to the Postfix listener on localhost:10026. +Note that the C 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 like this: + + spampd --host=localhost:10025 --relayhost=localhost:10026 + +F 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 page (smtp, smtpd, lmtp). By default +I 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 listens on. By default, it listens +on 127.0.0.1 (localhost) on port 10025. + +B You should NOT enable I to listen on a +public interface (IP address) unless you know exactly what you're doing! + +=item B<--port=n> + +Specifies what port I 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 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 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/I. + +=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 processes running. + +You may want to set your origination mail server to limit the +number of concurrent connections to I to match this setting (for +Postfix this is the C setting where +'xxxx' is the transport being used, usually 'smtp', and the default is 100). + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 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 user. The default is +F. + +=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 +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 to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 and/or +I settings in your SpamAssassin F 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 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 is written and maintained by Maxim Paperno . +See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info. + +I 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. See http://bent.latency.net/smtpprox/ . + +I v1 was based on code by Dave Carrigan named I. 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 (included with SpamAssassin) and +I (http://www.ijs.si/software/amavisd/) for some tricks. + +=head1 Copyright, License, and Disclaimer + +I 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, +L 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 . +# +# 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 + +# =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 . +# +# 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 + +# =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 < or B<--mc=n> C<(new in v2)> +# +# Maximum number of children to spawn if needed (where n >= --children). When +# I 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 to match this setting (for +# Postfix this is the C 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 +[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 B<--help> + +=head1 Description + +I 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 (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 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 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 logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +The latest version can be found at +L. + +=head1 Requires + +=over 5 + +Perl modules: + +=item B + +=item B + +=item B + +=item B + +=item B (not actually required but recommended) + +=back + +=head1 Operation + +I is meant to operate as an S/LMTP mail proxy which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 + +=over 3 + +The firewall/gateway MTA would be configured to forward all of its mail +to the port that I listens on, and I would relay its +messages to port 25 of your internal server. I 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 + +=over 3 + +Please see the F 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. + +=head1 Upgrading + +Upgrading from version 1 simply involves replacing the F program file +with the latest one. Note that the I 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 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 (a sample script is available on the +I 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 is a memory-hungry +program. Check the L<"Options"> for details on this and all other parameters. + +Note that I B I from the I distribution +in function. You do not need to run I in order for I 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 that came with the Postfix distribution (which you +really need to read): + +F: + + 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 file. I is the process that listens on +localhost:10025 and then connects to the Postfix listener on localhost:10026. +Note that the C 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 like this: + + spampd --host=localhost:10025 --relayhost=localhost:10026 + +F 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 page (smtp, smtpd, lmtp). By default +I 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 listens on. By default, it listens +on 127.0.0.1 (localhost) on port 10025. + +B You should NOT enable I to listen on a +public interface (IP address) unless you know exactly what you're doing! + +=item B<--port=n> + +Specifies what port I 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 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 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/I. + +=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 processes running. + +You may want to set your origination mail server to limit the +number of concurrent connections to I to match this setting (for +Postfix this is the C setting where +'xxxx' is the transport being used, usually 'smtp', and the default is 100). + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 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 user. The default is +F. + +=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 +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 to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 and/or +I settings in your SpamAssassin F 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 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 is written and maintained by Maxim Paperno . +See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info. + +I 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. See http://bent.latency.net/smtpprox/ . + +I v1 was based on code by Dave Carrigan named I. 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 (included with SpamAssassin) and +I (http://www.ijs.si/software/amavisd/) for some tricks. + +=head1 Copyright, License, and Disclaimer + +I 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, +L 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 . +# +# 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 + +# =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 . +# +# 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 + +# =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 < or B<--mc=n> C<(new in v2)> +# +# Maximum number of children to spawn if needed (where n >= --children). When +# I 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 to match this setting (for +# Postfix this is the C 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 +[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 B<--help> + +=head1 Description + +I 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 (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 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 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 logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +The latest version can be found at +L. + +=head1 Requires + +=over 5 + +Perl modules: + +=item B + +=item B + +=item B + +=item B + +=item B (not actually required but recommended) + +=back + +=head1 Operation + +I is meant to operate as an S/LMTP mail proxy which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 + +=over 3 + +The firewall/gateway MTA would be configured to forward all of its mail +to the port that I listens on, and I would relay its +messages to port 25 of your internal server. I 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 + +=over 3 + +Please see the F 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. + +=head1 Upgrading + +Upgrading from version 1 simply involves replacing the F program file +with the latest one. Note that the I 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 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 (a sample script is available on the +I 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 is a memory-hungry +program. Check the L<"Options"> for details on this and all other parameters. + +Note that I B I from the I distribution +in function. You do not need to run I in order for I 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 that came with the Postfix distribution (which you +really need to read): + +F: + + 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 file. I is the process that listens on +localhost:10025 and then connects to the Postfix listener on localhost:10026. +Note that the C 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 like this: + + spampd --host=localhost:10025 --relayhost=localhost:10026 + +F 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 page (smtp, smtpd, lmtp). By default +I 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 listens on. By default, it listens +on 127.0.0.1 (localhost) on port 10025. + +B You should NOT enable I to listen on a +public interface (IP address) unless you know exactly what you're doing! + +=item B<--port=n> + +Specifies what port I 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 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 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/I. + +=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 processes running. + +You may want to set your origination mail server to limit the +number of concurrent connections to I to match this setting (for +Postfix this is the C setting where +'xxxx' is the transport being used, usually 'smtp', and the default is 100). + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 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 user. The default is +F. + +=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 +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 to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 and/or +I settings in your SpamAssassin F 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 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 is written and maintained by Maxim Paperno . +See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info. + +I 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. See http://bent.latency.net/smtpprox/ . + +I v1 was based on code by Dave Carrigan named I. 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 (included with SpamAssassin) and +I (http://www.ijs.si/software/amavisd/) for some tricks. + +=head1 Copyright, License, and Disclaimer + +I 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, +L 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 . +# +# 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 + +# =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 . +# +# 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 + +# =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 < 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 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 to match this setting (for +# Postfix this is the C 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 +[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 B<--help> + +=head1 Description + +I 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 (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 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 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 logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +The latest version can be found at +L. + +=head1 Requires + +=over 5 + +Perl modules: + +=item B + +=item B + +=item B + +=item B + +=item B (not actually required but recommended) + +=back + +=head1 Operation + +I is meant to operate as an S/LMTP mail proxy which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 + +=over 3 + +The firewall/gateway MTA would be configured to forward all of its mail +to the port that I listens on, and I would relay its +messages to port 25 of your internal server. I 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 + +=over 3 + +Please see the F 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. + +=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 program file +with the latest one. Note that the I 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 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 (a sample script is available on the +I 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 is a memory-hungry +program. Check the L<"Options"> for details on this and all other parameters. + +Note that I B I from the I distribution +in function. You do not need to run I in order for I 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 that came with the Postfix distribution (which you +really need to read): + +F: + + 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 file. I is the process that listens on +localhost:10025 and then connects to the Postfix listener on localhost:10026. +Note that the C 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 like this: + + spampd --host=localhost:10025 --relayhost=localhost:10026 + +F 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 page (smtp, smtpd, lmtp). By default +I 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 listens on. By default, it listens +on 127.0.0.1 (localhost) on port 10025. + +B You should NOT enable I to listen on a +public interface (IP address) unless you know exactly what you're doing! + +=item B<--port=n> + +Specifies what port I 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 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 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/I. + +=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 processes running. + +You may want to set your origination mail server to limit the +number of concurrent connections to I to match this setting (for +Postfix this is the C setting where +'xxxx' is the transport being used, usually 'smtp', and the default is 100). + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 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 user. The default is +F. + +=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 +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 to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 and/or +I settings in your SpamAssassin F 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 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 is written and maintained by Maxim Paperno . +See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info. + +I 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. See http://bent.latency.net/smtpprox/ . + +I v1 was based on code by Dave Carrigan named I. 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 (included with SpamAssassin) and +I (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 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, +L 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 . +# +# 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 + +# =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 . +# +# 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 + +# =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 < 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 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 to match this setting (for +# Postfix this is the C 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 +[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 B<--help> + +=head1 Description + +I 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 (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 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 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 logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +The latest version can be found at +L. + +=head1 Requires + +=over 5 + +Perl modules: + +=item B + +=item B + +=item B + +=item B + +=item B (not actually required but recommended) + +=back + +=head1 Operation + +I is meant to operate as an S/LMTP mail proxy which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 + +=over 3 + +The firewall/gateway MTA would be configured to forward all of its mail +to the port that I listens on, and I would relay its +messages to port 25 of your internal server. I 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 + +=over 3 + +Please see the F 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. + +=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 program file +with the latest one. Note that the I 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 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 (a sample script is available on the +I 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 is a memory-hungry +program. Check the L<"Options"> for details on this and all other parameters. + +Note that I B I from the I distribution +in function. You do not need to run I in order for I 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 that came with the Postfix distribution (which you +really need to read): + +F: + + 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 file. I is the process that listens on +localhost:10025 and then connects to the Postfix listener on localhost:10026. +Note that the C 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 like this: + + spampd --host=localhost:10025 --relayhost=localhost:10026 + +F 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 page (smtp, smtpd, lmtp). By default +I 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 listens on. By default, it listens +on 127.0.0.1 (localhost) on port 10025. + +B You should NOT enable I to listen on a +public interface (IP address) unless you know exactly what you're doing! + +=item B<--port=n> + +Specifies what port I 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 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 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/I. + +=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 processes running. + +You may want to set your origination mail server to limit the +number of concurrent connections to I to match this setting (for +Postfix this is the C setting where +'xxxx' is the transport being used, usually 'smtp', and the default is 100). + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 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 user. The default is +F. + +=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 +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 to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 and/or +I settings in your SpamAssassin F 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: 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 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 is written and maintained by Maxim Paperno . +See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info. + +I 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. See http://bent.latency.net/smtpprox/ . + +I v1 was based on code by Dave Carrigan named I. 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 (included with SpamAssassin) and +I (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 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, +L 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 . +# +# 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 + +# =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 . +# +# 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 + +# =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 < 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 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 to match this setting (for +# Postfix this is the C 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 +[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 B<--help> + +=head1 Description + +I 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 (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 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 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 logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +The latest version can be found at +L. + +=head1 Requires + +=over 5 + +Perl modules: + +=item B + +=item B + +=item B + +=item B + +=item B (not actually required but recommended) + +=back + +=head1 Operation + +I is meant to operate as an S/LMTP mail proxy which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 + +=over 3 + +The firewall/gateway MTA would be configured to forward all of its mail +to the port that I listens on, and I would relay its +messages to port 25 of your internal server. I 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 + +=over 3 + +Please see the F 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. + +=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 program file +with the latest one. Note that the I 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 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 (a sample script is available on the +I 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 is a memory-hungry +program. Check the L<"Options"> for details on this and all other parameters. + +Note that I B I from the I distribution +in function. You do not need to run I in order for I 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 that came with the Postfix distribution (which you +really need to read): + +F: + + 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 file. I is the process that listens on +localhost:10025 and then connects to the Postfix listener on localhost:10026. +Note that the C 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 like this: + + spampd --host=localhost:10025 --relayhost=localhost:10026 + +F 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 page (smtp, smtpd, lmtp). By default +I 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 listens on. By default, it listens +on 127.0.0.1 (localhost) on port 10025. + +B You should NOT enable I to listen on a +public interface (IP address) unless you know exactly what you're doing! + +=item B<--port=n> + +Specifies what port I 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 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 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/I. + +=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 processes running. + +You may want to set your origination mail server to limit the +number of concurrent connections to I to match this setting (for +Postfix this is the C setting where +'xxxx' is the transport being used, usually 'smtp', and the default is 100). + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 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 user. The default is +F. + +=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 +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 to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 and/or +I settings in your SpamAssassin F 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: 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 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 is written and maintained by Maxim Paperno . +See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info. + +I 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. See http://bent.latency.net/smtpprox/ . + +I v1 was based on code by Dave Carrigan named I. 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 (included with SpamAssassin) and +I (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 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, +L 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 . +# +# 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 + +# =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 . +# +# 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 + +# =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 < 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 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 to match this setting (for +# Postfix this is the C 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 +[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 B<--help> + +=head1 Description + +I 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 (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 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 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 logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +The latest version can be found at +L. + +=head1 Requires + +=over 5 + +Perl modules: + +=item B + +=item B + +=item B + +=item B + +=item B (not actually required but recommended) + +=back + +=head1 Operation + +I is meant to operate as an S/LMTP mail proxy which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 + +=over 3 + +The firewall/gateway MTA would be configured to forward all of its mail +to the port that I listens on, and I would relay its +messages to port 25 of your internal server. I 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 + +=over 3 + +Please see the F 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. + +=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 program file +with the latest one. Note that the I 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 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 (a sample script is available on the +I 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 is a memory-hungry +program. Check the L<"Options"> for details on this and all other parameters. + +Note that I B I from the I distribution +in function. You do not need to run I in order for I 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 that came with the Postfix distribution (which you +really need to read): + +F: + + 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 file. I is the process that listens on +localhost:10025 and then connects to the Postfix listener on localhost:10026. +Note that the C 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 like this: + + spampd --host=localhost:10025 --relayhost=localhost:10026 + +F 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 page (smtp, smtpd, lmtp). By default +I 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 listens on. By default, it listens +on 127.0.0.1 (localhost) on port 10025. + +B You should NOT enable I to listen on a +public interface (IP address) unless you know exactly what you're doing! + +=item B<--port=n> + +Specifies what port I 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 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 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/I. + +=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 processes running. + +You may want to set your origination mail server to limit the +number of concurrent connections to I to match this setting (for +Postfix this is the C setting where +'xxxx' is the transport being used, usually 'smtp', and the default is 100). + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 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 user. The default is +F. + +=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 +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 to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 and/or +I settings in your SpamAssassin F 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: 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 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 is written and maintained by Maxim Paperno . +See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info. + +I 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. See http://bent.latency.net/smtpprox/ . + +I v1 was based on code by Dave Carrigan named I. 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 (included with SpamAssassin) and +I (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 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, +L 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 . +# +# 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 + +# =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 . +# +# 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 + +# =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 < 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 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 to match this setting (for +# Postfix this is the C 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 +[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 B<--help> + +=head1 Description + +I 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 (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 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 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 logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +The latest version can be found at +L. + +=head1 Requires + +=over 5 + +Perl modules: + +=item B + +=item B + +=item B + +=item B + +=item B (not actually required but recommended) + +=back + +=head1 Operation + +I is meant to operate as an S/LMTP mail proxy which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 + +=over 3 + +The firewall/gateway MTA would be configured to forward all of its mail +to the port that I listens on, and I would relay its +messages to port 25 of your internal server. I 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 + +=over 3 + +Please see the F 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. + +=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 program file +with the latest one. Note that the I 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 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 (a sample script is available on the +I 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 is a memory-hungry +program. Check the L<"Options"> for details on this and all other parameters. + +Note that I B I from the I distribution +in function. You do not need to run I in order for I 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 that came with the Postfix distribution (which you +really need to read): + +F: + + 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 file. I is the process that listens on +localhost:10025 and then connects to the Postfix listener on localhost:10026. +Note that the C 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 like this: + + spampd --host=localhost:10025 --relayhost=localhost:10026 + +F 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 page (smtp, smtpd, lmtp). By default +I 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 listens on. By default, it listens +on 127.0.0.1 (localhost) on port 10025. + +B You should NOT enable I to listen on a +public interface (IP address) unless you know exactly what you're doing! + +=item B<--port=n> + +Specifies what port I 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 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 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/I. + +=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 processes running. + +You may want to set your origination mail server to limit the +number of concurrent connections to I to match this setting (for +Postfix this is the C setting where +'xxxx' is the transport being used, usually 'smtp', and the default is 100). + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 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 user. The default is +F. + +=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 +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 to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 and/or +I settings in your SpamAssassin F 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: 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 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 is written and maintained by Maxim Paperno . +See http://www.WorldDesign.com/index.cfm/rd/mta/spampd.htm for latest info. + +I 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. See http://bent.latency.net/smtpprox/ . + +I v1 was based on code by Dave Carrigan named I. 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 (included with SpamAssassin) and +I (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 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, +L 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 < +[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 B<--help> + +=head1 DESCRIPTION + +I 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 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 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 logs all aspects of its operation to syslog(8), using the +mail syslog facility. + +=head1 REQUIRES + +Perl modules: + +B + +B + +B + +B + + +=head1 OPERATION + +I is meant to operate as an SMTP mail relay which passes +each message through SpamAssassin for analysis. Note that I +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 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 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 listens on, and I would relay its +messages to port 25 of your internal server. I 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 (@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 (@inter.net.host:25) -> + I (@localhost:10025) -> + I (@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. + +=head1 OPTIONS + +=over 5 + +=item B<--port=n> + +Specifies what port I listens on. By default, it listens on +port 10025. + +=item B<--host=ip> + +Specifies what interface/IP I listens on. By default, it listens on +127.0.0.1 (localhost). + +B You should NOT enable I 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 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/I. + +=item B<--maxrequests=n> + +I works by forking child servers to handle each message. The +B 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 will store any message that +it fails to deliver. The default is F. You should periodically +examine this directory to see if there are any messages that couldn't be +delivered. + +B 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 will not be able to save the +message, and it will be lost. + +=item B<--pid=filename> + +Specifies a filename where I 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 user. The default is +F. + +=item B<--tagall> + +Tells I to have SpamAssassin add headers to all scanned mail, +not just spam. By default I 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 listens on port 10025 on the same host as the internal mail server. + + spampd --host=192.168.1.10 + +Same as above but I 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 by Dave Carrigan, +see http://www.rudedog.org/assassind/ + +Modified and renamed to I (to avoid confusion) by +Maxim Paperno, . 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 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/ -- cgit v1.2.3-24-g4f1b