Hello,

Here is a small patch to spamproxyd. (current cvs version)
(i joined the patched spamproxyd as well, as it is small enought)

I changed spamproxyd  

 - to correct a call to split which was removing trailing empty lines 
        ( which could induce message with no body, thus bouncing the mail with 
          intolerant mdas (cyrus in my case))

        relay=/data/mail/imap/socket/lmtp[/data/mail/imap/socket/lmtp],
         delay=2, status=bounced (host 
/data/mail/imap/socket/lmtp[/data/mail/imap/socket/lmtp] 
        said: 554 5.6.0 Message has no header/body separator)

 - to add the hits in headers even if it isn't a spam

 - to add scrictiness, and perl warnings 

 - to add a possibility to put the spam into a mailbox called shared+user.$user.spam
   (usefull for imap users (i'm using cyrus))

 - to add a debug option to print the recipient inside of spamproxyd



I hope it's good enough to integrate it.

Tell me what you think,

Thanks

xavier


ps : info to users : spamproxyd used Mail::Spamassassin, which loads local.cf (in 
rules directory) as site-wide preferences. You may want to add/modify it.

ps2 : here is how i use it : 

I added in :

postfix main.cf : 

content_filter = smtp:localhost:10025


postfix master.cf :

localhost:10026     inet  n      -      n      -      10      smtpd
      -o content_filter= 
      -o local_recipient_maps=
      -o myhostname=localhost.hansonpublications.com

and i start spamproxyd via :

 ./spamproxyd.pl --debug 127.0.0.1:10025 127.0.0.1:10026


ps3 : i should work on it to daemonize it.
--- spamproxyd.raw      Mon Apr 29 10:54:28 2002
+++ spamproxyd.raw.new  Fri May 10 11:53:18 2002
@@ -1,4 +1,5 @@
-#!/usr/bin/perl
+#!/usr/bin/perl -w
+
 
 eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
     if 0; # not running under some shell
@@ -13,6 +14,8 @@
 my $children = 4;
 my $minperchild = 5;
 my $maxperchild = 10;
+my $shared=0;
+my $debug=0;
 
 # This file is based largely on example code bundled with MacGyver's
 # Net::SMTP::Server kit, but with some additional stuff to use
@@ -38,6 +41,7 @@
 use Mail::SpamAssassin;
 use Net::DNS;
 use Getopt::Long;
+use strict;
 
 my $spamtest = Mail::SpamAssassin->new();
 $spamtest->compile_now();       # ensure all modules etc. are preloaded
@@ -56,10 +60,12 @@
 # --irj
 
 my $syntax = "syntax: $0 [--children=$children] [--minperchild=$minperchild] ".
-             "[--maxperchild=$maxperchild] ".
+             "[--maxperchild=$maxperchild] [--shared] [--debug]".
              "listen.addr:port talk.addr:port spamaddr\@example.com\n";
 
 GetOptions("children=n" => \$children,
+          "shared" => \$shared,
+          "debug" => \$debug,
            "minperchild=n" => \$minperchild,
            "maxperchild=n" => \$maxperchild) or die $syntax;
 
@@ -76,11 +82,13 @@
 print "Trying to start using source $srcaddr port $srcport, " .
       "destination $dstaddr port $dstport, " . 
       "reporting e-mail address $spamaddr.\n";
+print "shared : $shared\n";
+print "debug : $debug\n";
 
 die $syntax unless defined($srcport) and defined($dstport)
   and defined($spamaddr);
 
-$smarthost=$dstaddr . ":" . $dstport;
+my $smarthost=$dstaddr . ":" . $dstport;
 
 # Set up the server using the IP address and port specified on the command
 # line by the user.
@@ -89,11 +97,11 @@
 # I'll spare everyone those details here as that info is in his code. 
 # Instead,  I'll be concentrating on the message-handling portion. --irj
 
-$server = new Net::SMTP::Server($srcaddr, $srcport) ||
+my $server = new Net::SMTP::Server($srcaddr, $srcport) ||
   croak("Unable to create server: $!\n");
 
 print "Server started on address $srcaddr port $srcport " .
-      "with destination address $dstaddr port $dstport";
+      "with destination address $dstaddr port $dstport\n";
 
 # [MSDW]
 # This should allow a kill on the parent to also blow away the
@@ -130,7 +138,7 @@
 my $lives = $minperchild + (rand($maxperchild - $minperchild));
 my %opts;
 
-while($conn = $server->accept()) {
+while(my $conn = $server->accept()) {
     my $client = new Net::SMTP::Server::Client($conn) ||
       croak("Unable to handle client connection: $!\n");
 
@@ -147,11 +155,19 @@
     # handed to a new Mail::SpamAssassin::NoMailAudit object.
     # --irj
 
-    $message = $client->{MSG};
-    @msg = split ("\r\n", $message);
-    $arraycont = @msg; for(0..$arraycont) { $msg[$_] .= "\r\n"; }
-    %args = (data => \@msg);
-    $mail = Mail::SpamAssassin::NoMailAudit->new(%args);
+    # perldoc -f split
+    #split   Splits a string into a list of strings and returns
+    #           that list.  By default, empty leading fields are
+    #           preserved, and empty trailing ones are deleted.
+    #
+    # so, it removes last empty lines !!! -> hence the last argument, -1
+    
+
+    my $message = $client->{MSG};
+    my @msg = split ("\r\n", $message,'-1');
+    my $arraycont = @msg; for(0..$arraycont) { $msg[$_] .= "\r\n"; }
+    my %args = (data => \@msg);
+    my $mail = Mail::SpamAssassin::NoMailAudit->new(%args);
 
     # At some point, I may also put some other code so I can go grab
     # preferences, e.g. via MySQL, e.g. scoring parameters, or even whether to
@@ -167,25 +183,55 @@
     # instead, pass the original message to the smarthost code below.
     # --irj
 
+    my $recips;
+    my $msg;
     my $status = $spamtest->check($mail);
+    my ($msg_debug1,$msg_debug2);
+
     if ($status->is_spam ()) {
         $msg = sprintf("    SPAM[%6.1f]: %s", $status->get_hits(), 
$status->get_names_of_tests_hit());
+       # add headers
         $status->rewrite_mail ();
         $message = join ("",$mail->header(),@{$mail->body()});
+
+       # check if the mail goes to one address
         if($spamaddr ne "recipient") {
-          @recipients = ("$spamaddr");
+          my @recipients = ("$spamaddr");
           $recips = \@recipients;
         } else {
-          $recips = $client->{TO};
+           $recips = $client->{TO};
+         
+
+           foreach (@{$client->{TO}})
+           {
+               $msg_debug1="msg going to be delivered in      : -$_- ";
+                if ($shared)
+                {
+
+                    # rewrite in case of imap delivery
+                    # shared+user.$user.spam
+                    s/<(.*?)(\@.*)/<shared+user.$1.spam$2/g;
+                    $msg_debug2="msg going to be delivered in spam : -$_- ";
+                }
+           }
         }
     } else {
         $msg = sprintf("NOT_SPAM[%6.1f]: %s", $status->get_hits(), 
$status->get_names_of_tests_hit());
+
+       # added the next 2 lines so that even if it's not spam we got statistics 
+(xavier renaut)
+
+       $status->rewrite_mail ();
+        $message = join ("",$mail->header(),@{$mail->body()}); #original
+       
+       # end of addition
+
         $recips = $client->{TO};
     }
 
     setlogsock 'unix';
     openlog('spamassassin', 'nowait', 'local3');
     syslog('notice', $msg);
+    if ($debug && $msg_debug1 ){print $msg_debug1,"\n";print $msg_debug2,"\n" if 
+$msg_debug2; syslog('notice', $msg_debug1); syslog('notice', $msg_debug2) if 
+$msg_debug2;}
     closelog();
 
     $status->finish();
#!/usr/bin/perl -w


eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
    if 0; # not running under some shell

# Some configurable stuff here.  This may get offloaded to a file in the
# future.

# $smarthost="localhost:10026";

# Some stuff from MSDW's smtpprox for preforking stuff

my $children = 4;
my $minperchild = 5;
my $maxperchild = 10;
my $shared=0;
my $debug=0;

# This file is based largely on example code bundled with MacGyver's
# Net::SMTP::Server kit, but with some additional stuff to use
# Mail::SpamAsssassin and a modified version of Net::SMTP::Server::Relay so
# then it becomes Net::SMTP::Server::SmartHost.  This way I can direct mail
# to a specific mailserver specified.  ::Relay does MX lookups which isn't
# what we want, but instead, reinject the message back into the system via
# an unfiltered version of SMTP server
#
# This was written with Postfix in mind, but nothing says you cannot use it
# for another MTA.  Be sure to read FILTER_README for a bit more background
# on how to integrate an SMTP-based filter (considered an "advanced" method).
#
# --Ian R. Justman <[EMAIL PROTECTED]>, 11/21/2001

use Sys::Syslog qw(:DEFAULT setlogsock);

use Carp;
use Net::SMTP::Server;
use Net::SMTP::Server::Client;
use Mail::SpamAssassin::SMTP::SmartHost;
use Mail::SpamAssassin::NoMailAudit;
use Mail::SpamAssassin;
use Net::DNS;
use Getopt::Long;
use strict;

my $spamtest = Mail::SpamAssassin->new();
$spamtest->compile_now();       # ensure all modules etc. are preloaded
$/ = "\n";                      # argh, Razor resets this!  Bad Razor!

# 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

my $syntax = "syntax: $0 [--children=$children] [--minperchild=$minperchild] ".
             "[--maxperchild=$maxperchild] [--shared] [--debug]".
             "listen.addr:port talk.addr:port spamaddr\@example.com\n";

GetOptions("children=n" => \$children,
           "shared" => \$shared,
           "debug" => \$debug,
           "minperchild=n" => \$minperchild,
           "maxperchild=n" => \$maxperchild) or die $syntax;

die $syntax unless @ARGV == 2;
my ($srcaddr, $srcport) = split /:/, $ARGV[0];
my ($dstaddr, $dstport) = split /:/, $ARGV[1];
my $spamaddr;
if(@ARGV == 3) {
  $spamaddr = $ARGV[2];
} else {
  $spamaddr = "recipient";
}

print "Trying to start using source $srcaddr port $srcport, " .
      "destination $dstaddr port $dstport, " . 
      "reporting e-mail address $spamaddr.\n";
print "shared : $shared\n";
print "debug : $debug\n";

die $syntax unless defined($srcport) and defined($dstport)
  and defined($spamaddr);

my $smarthost=$dstaddr . ":" . $dstport;

# Set up the server using the IP address and port specified on the command
# line by the user.
#
# Since a vast majority of the SMTP code is based on MacGyver's sample code,
# I'll spare everyone those details here as that info is in his code. 
# Instead,  I'll be concentrating on the message-handling portion. --irj

my $server = new Net::SMTP::Server($srcaddr, $srcport) ||
  croak("Unable to create server: $!\n");

print "Server started on address $srcaddr port $srcport " .
      "with destination address $dstaddr port $dstport\n";

# [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{TERM} = sub { $please_die = 1; };

# [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));
my %opts;

while(my $conn = $server->accept()) {
    my $client = new Net::SMTP::Server::Client($conn) ||
      croak("Unable to handle client connection: $!\n");

    # [MSDW]
    # Process the client.  This command will block until
    # the connecting client completes the SMTP transaction.
    $client->process || next;

    # Mail::SpamAssassin::NoMailAudit wants an array of lines, while the
    # server returns a huge string.  Since I am unsure whether it needs to
    # have the CR/LF pair for each line for use with Razor, after splitting
    # it, using the CR/LF pairs as delimiters, I walk over the message again
    # to re-add them.  Once the array is populated and tweaked, it is then
    # handed to a new Mail::SpamAssassin::NoMailAudit object.
    # --irj

    # perldoc -f split
    #split   Splits a string into a list of strings and returns
    #           that list.  By default, empty leading fields are
    #           preserved, and empty trailing ones are deleted.
    #
    # so, it removes last empty lines !!! -> hence the last argument, -1
    

    my $message = $client->{MSG};
    my @msg = split ("\r\n", $message,'-1');
    my $arraycont = @msg; for(0..$arraycont) { $msg[$_] .= "\r\n"; }
    my %args = (data => \@msg);
    my $mail = Mail::SpamAssassin::NoMailAudit->new(%args);

    # At some point, I may also put some other code so I can go grab
    # preferences, e.g. via MySQL, e.g. scoring parameters, or even whether to
    # filter at all (hey, with Perl + MySQL, your imagination is the
    # limit).
    #
    # This is where the testing actually happens.  In this example, which I
    # have in an actual production environment (save the address), I have it
    # rewriting the message then forwarding to a collection account for
    # examination.  The addresses have been changed to protect the innocent.
    #
    # If the message is OK, we skip doing anything with the object and
    # instead, pass the original message to the smarthost code below.
    # --irj

    my $recips;
    my $msg;
    my $status = $spamtest->check($mail);
    my ($msg_debug1,$msg_debug2);

    if ($status->is_spam ()) {
        $msg = sprintf("    SPAM[%6.1f]: %s", $status->get_hits(), 
$status->get_names_of_tests_hit());
        # add headers
        $status->rewrite_mail ();
        $message = join ("",$mail->header(),@{$mail->body()});

        # check if the mail goes to one address
        if($spamaddr ne "recipient") {
          my @recipients = ("$spamaddr");
          $recips = \@recipients;
        } else {
            $recips = $client->{TO};
          

            foreach (@{$client->{TO}})
            {
                $msg_debug1="msg going to be delivered in      : -$_- ";
                 if ($shared)
                 {

                     # rewrite in case of imap delivery
                     # shared+user.$user.spam
                     s/<(.*?)(\@.*)/<shared+user.$1.spam$2/g;
                     $msg_debug2="msg going to be delivered in spam : -$_- ";
                 }
            }
        }
    } else {
        $msg = sprintf("NOT_SPAM[%6.1f]: %s", $status->get_hits(), 
$status->get_names_of_tests_hit());

        # added the next 2 lines so that even if it's not spam we got statistics 
(xavier renaut)

        $status->rewrite_mail ();
        $message = join ("",$mail->header(),@{$mail->body()}); #original
        
        # end of addition

        $recips = $client->{TO};
    }

    setlogsock 'unix';
    openlog('spamassassin', 'nowait', 'local3');
    syslog('notice', $msg);
    if ($debug && $msg_debug1 ){print $msg_debug1,"\n";print $msg_debug2,"\n" if 
$msg_debug2; syslog('notice', $msg_debug1); syslog('notice', $msg_debug2) if 
$msg_debug2;}
    closelog();

    $status->finish();

    # Here is where we actually connect back into Postfix or wherever.  As
    # has been mentioned before, more detailed information on how to set
    # Postfix up to use an "advanced" filter setup, directly upon this
    # documentation this implementation is based.
    #
    # Here, we need to use a hacked version of Net::SMTP::Server::Relay to
    # make this work, which I will bundle in along with the script.  I made
    # no other modifications to the rest of the distribution (which is
    # required to make this work and is in CPAN).
    # --irj

    my $relay = new Mail::SpamAssassin::SMTP::SmartHost($client->{FROM},
                                                 $recips,
                                                 $message,
                                                 "$smarthost");

    # Zap this instance if this child's processing limit has been reached.
    # --irj
    
    delete $server->{"s"};
    exit 0 if $lives-- <= 0;
}


=head1 NAME

spamproxyd - mail filter to identify spam using text analysis

=head1 SYNOPSIS

=over

=item spamproxyd

=back

=head1 OPTIONS

=over 4

F<spamproxyd> currently has no options.

=back

=head1 DESCRIPTION

IMPORTANT!  PLEASE read CHANGES.spamproxy before continuing!

This is a prototype for an SMTP filter based on Mail::SpamAssassin
(http://spamassassin.org, http://spamassassin.sourceforge.net).

This was originally written with Postfix's filering in mind, based on the
"advanced" example detailed in the FILTER_README file in the Postfix
distribution, but there's no reason why it couldn't be used with other
servers.

This script is just proof of concept right now; it may more than likely not
be usable in a larger-scale environment where there's high volumes of mail
being transferred.  However, it's currently good enough for a small-scale
environment, like the IRC network for which I serve as postmaster, along
with several other people I service on a small machine.

This script requires Mail::Assassin (see above) and Net::SMTP::Server
(http://www.macgyver.org/software/perl/, plus it is also in CPAN).  You also
need a modified version of one of the modules in order to connect to a
specific SMTP server, which I include in the package.

Right now, this script has a couple of shortcomings:

1.  Configurability, configurability!  This is especially true if this will
    filter for multiple people whose needs may be quite different, including
    per-user weighting of the "suspicious stuff", white-lists, etc, and of
    course, whether to tag spam then deliver (if wanted), even whether to
    filter at all.

2.  What do YOU want? Who knows?  With Perl, your imagination's the limit. 

So far, I've managed to zap quite a bit of spam that'd normally go right
through the server.  With Vipul's Razor, this can go up quite a bit.  If
anyone has any ideas about Vipul's Razor and how I populate my arrays,
please let me know.

=head1 SEE ALSO

Mail::SpamAssassin(3)
Net::SMTP::Server(3)

=head1 AUTHOR

Ian R. Justman E<lt>[EMAIL PROTECTED]<gt>

=head1 CREDITS

Justin Mason and Craig Hughes for B<Mail::SpamAssassin>

Habeeb J. "MacGyver" Dihu for his B<Net::SMTP::Server> code

Bennett Todd for the perforking code and option-parsing code from his
    pacakge, smtpproxy

Special thanks go out to the crew at my usual IRC hangout, notably Barry
Hughes, Matti Koskimies, plus a number of others whom I may have not given
appropriate credit, but you still deserve it.  You've been a big help.  :)

=head1 PREREQUISITES

C<Mail::SpamAssassin>
C<Net::SMTP::Server>

=cut

Reply via email to