Re: [Exim] transport_filter/temp_errors problems; doing spam…

Startseite
Nachricht löschen
Nachricht beantworten
Autor: Steve Haslam
Datum:  
To: exim-users
Alte Treads: Re: [Exim] transport_filter/temp_errors problems, [Exim] transport_filter/temp_errors problems
Betreff: Re: [Exim] transport_filter/temp_errors problems; doing spam-checking without transport_filter
On Mon, Aug 26, 2002 at 01:02:41PM -0400, Paul Fisher wrote:
>     2002-08-26 07:15:05 17jHkz-0004mI-00 ** foobar@???
>     R=spamassassin_router T=spamassassin_transport: Child process of
>     spamassassin_transport transport returned 2 from command:
>     /usr/sbin/exim

[...]
> On a related note, does temp_errors have any effect for the the
> transport_filter program? If not, maybe there should be a
> transport_filter_temp_errors option?


On Tue, Aug 27, 2002 at 09:48:47AM +0100, Philip Hazel wrote:
> No effect, because temp_errors is local to the pipe transport, whereas
> you can have a transport filter for any transport.
>
> As it happens, I already have an item on my infelicities list about
> transport filters. Three processes are involved in a transport filter,
> and if something goes wrong, it is not clear which process' error should
> be the one that is reported (all 3 processes notice a problem). I'll
> think about temporary errors when I get to that work item.


Erk.... ok, I'm glad I found a way around using transport_filter to do
spamassassin now. I've written a Perl script (see end) that acts as a
delivery process taking BSMTP-- it forks a spamc process to check the mail
and an exim -oMr process to reinject the mail, and passes the BSMTP around
between the processes so that a spam-checked BSMTP message gets reinjected.

The script needs to be setuid root so that it can change user ids, which is
nasty. If you set "user = root" in a transport it gets executed as
"nobody"-- presumably do to the never_users setting? Maybe allow overriding
never_users on a per-transport basis... (e.g. "override_never_users", or
"user = root!").

Anyway, this is an alternative approach that I'd be interested in hearing
feedback about.

The script:

#!/usr/bin/suidperl -wT

require 5;
use strict;
use IO::Pipe;
use Errno;
use POSIX;
use Sys::Syslog qw(:DEFAULT setlogsock);
use vars qw($spamc_user $spamc_group $spamc_pid $spamc_status
        $exim_user $exim_group $exim_pid $exim_status
        $progname $to_spamc_pipe $from_spamc_pipe $to_exim_pipe
        @addresses);


($progname = $0) =~ s|^.*/||;

setlogsock 'unix';
openlog $progname, '', 'mail';

sub tempfail($) {
    my $m = shift;
    syslog 'err', $m;
    $m .= "\n" unless ($m =~ /\n$/);
    print STDERR $m;
    exit(75); # EX_TEMPFAIL
}


sub decodestatus {
    my $status = shift;
    if (WIFEXITED($status)) {
    return "exited with status ".WEXITSTATUS($status);
    }
    elsif (WIFSIGNALED($status)) {
    return "killed by signal ".WTERMSIG($status);
    }
    elsif (WIFSTOPPED($status)) {
    return "stopped by signal ".WSTOPSIG($status);
    }
    elsif ($status == 0) {
    return "finished successfully";
    }
    else {
    return "returned unknown status $status";
    }
}


$SIG{__DIE__} = \&tempfail;

if ($>) {
    my $eunam = (getpwuid($>))[0] || $>;
    my $egnam = (getgrgid($)))[0] || $);
    my $runam = (getpwuid($<))[0] || $<;
    my $rgnam = (getgrgid($())[0] || $(;
    tempfail "$progname: need root privs, executed as (rusr=$runam,rgrp=$rgnam eusr=$eunam,egrp=$egnam)";
}
else {
    $< = $>;
}


tempfail "Syntax: $progname target-user-id exim-user-id\n" unless (@ARGV == 2);

if ($ARGV[0] =~ /^([a-z][a-z0-9-]+)$/) {
    push @addresses, $1;
    my($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell,$expire)=getpwnam($1);
    if (!$uid) {
    tempfail "$progname: Unknown user-id '$1'\n";
    }
    ($spamc_user,$spamc_group) = ($uid,$gid);
}
else {
    tempfail "$progname: Bad user-id '$ARGV[0]'\n";
}


if ($ARGV[1] =~ /^([a-z][a-z0-9-]+)$/) {
    my($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell,$expire)=getpwnam($1);
    if (!$uid) {
    tempfail "$progname: Unknown user-id '$1'\n";
    }
    ($exim_user,$exim_group) = ($uid,$gid);
}
else {
    tempfail "$progname: Bad user-id '$ARGV[1]'\n";
}


$ENV{PATH}="/bin:/usr/bin:/usr/local/bin";

$to_spamc_pipe = IO::Pipe->new;
$to_exim_pipe = IO::Pipe->new;
$from_spamc_pipe = IO::Pipe->new;

if (!defined($spamc_pid = fork)) {
    tempfail "fork failed: $!\n";
}
elsif ($spamc_pid == 0) {
    # Child process: exec spamc with stdout redirected to pipe
    if ($> == 0) {
    # Change user-id to the target user
    ($(, $)) = ($spamc_group, $spamc_group);
    ($<, $>) = ($spamc_user, $spamc_user);
    }
    $from_spamc_pipe->writer;
    $to_spamc_pipe->reader;
    open(STDOUT, ">&=".$from_spamc_pipe->fileno) or tempfail "Unable to redirect STDOUT: $!\n";
    open(STDIN, "<&=".$to_spamc_pipe->fileno) or tempfail "Unable to redirect STDIN: $!\n";
    exec("/usr/bin/spamc") or tempfail "Unable to exec spamc: $!\n";
}


if (!defined($exim_pid = fork)) {
    tempfail "fork failed: $!\n";
}
elsif ($exim_pid == 0) {
    # Child process: exec exim with stdin redirected from pipe
    if ($> == 0) {
    # Change user-id to the exim user
    ($(, $)) = ($exim_group, $exim_group);
    ($<, $>) = ($exim_user, $exim_user);
    }
    $to_exim_pipe->reader;
    open(STDIN, "<&=".$to_exim_pipe->fileno) or tempfail "Unable to redirect STDIN: $!\n";
    exec("/usr/sbin/exim", "-bS", "-oMr", "spamassassin") or tempfail "Unable to exec exim: $!\n";
}


$to_exim_pipe->writer;
$to_spamc_pipe->writer;
$from_spamc_pipe->reader;

# Now that we've exec'd the subcommands, drop root privs
if ($> == 0) {
    ($(, $)) = ($exim_group, $exim_group);
    ($<, $>) = ($exim_user, $exim_user);
}


# Forward standard input to Exim until we get a DATA command
while (1) {
    my $line = <STDIN>;
    tempfail "EOF reading BSMTP header" if (!defined($line));
    $to_exim_pipe->print($line);
    syslog 'debug', "BSMTP-HDR $line";
    last if ($line =~ /^DATA\r?\n/m);
}
$to_exim_pipe->flush;


# Now forward standard input to SpamAssassin until we get an end-of-message
# Handle SMTP escaping along the way
while (1) {
    my $line = <STDIN>;
    tempfail "EOF reading BSMTP message" if (!defined($line));
    last if ($line =~ /^\.\r?\n/m);
    #syslog 'debug', "BSMTP-BODY $line";
    $line =~ s/^\./../;
    $to_spamc_pipe->print($line);
}
$to_spamc_pipe->close;


# Read the remainder of stdin (should be just a QUIT command)
while (1) {
    my $line = <STDIN>;
    #tempfail "EOF during BSMTP footer" if (!defined($line));
    last if (!defined($line));
    syslog 'debug', "BSMTP-FTR $line";
    tempfail "Exepected QUIT in BSMTP, got \"$line\"" if ($line !~ /^QUIT\r?\n/);
    last;
}
close(STDIN);


# Now forward spamc's output to Exim, doing SMTP escaping along the way
# Deal with the first line being a "From ...." mailbox header line
my $firstline = 1;
while (1) {
    my $line = $from_spamc_pipe->getline;
    last if (!defined($line));
    if ($firstline) {
    if ($line =~ /^From /) {
        syslog 'debug', "Junking mailbox header \"%s\"", $line;
        next;
    }
    $firstline = 0;
    }
    $line =~ s/^\.\././;
    $to_exim_pipe->print($line);
}
$from_spamc_pipe->close; # spamc should exit now


# Finally, write an end-of message string and a QUIT command
$to_exim_pipe->print(".\r\n");
#$to_exim_pipe->print("QUIT\r\n");
# .. and close the pipe
$to_exim_pipe->close;

# Wait for spamc to return
waitpid($spamc_pid,0) or tempfail "Unable to waitpid($spamc_pid,0): $!\n";
$spamc_status = $?;

# Wait for exim to return
waitpid($exim_pid,0) or tempfail "Unable to waitpid($exim_pid,0): $!\n";
$exim_status = $?;

# Report any errors
exit(0) unless ($spamc_status || $exim_status);
tempfail "Delivery failed; spamc: ".decodestatus($spamc_status)."; exim: ".decodestatus($exim_status);

__END__

I use this to run spamassassin on incoming mail once for each user, so you
get the user's SA preferences. But you just pass the username to run spamc
as as a command line parameter, so you could do it for all local users on a
message as the "nobody" user.

I use this transport (Exim 3):

spamassassin_userdivert:
driver = pipe
path = /bin:/usr/bin:/usr/local/bin
command = /usr/local/lib/exim/reinject_via_spamc ${local_part} mail
bsmtp = one
log_output
from_hack # I'm not sure why I've got this set.
user = mail

and this director:

spamassassin_rp:
driver = localuser
transport = spamassassin_userdivert
debug_print = "spamassassin_rp for ${local_part}, rcvd via ${received_protocol}"
no_verify
condition = "${if or{ {eq{${received_protocol}} {spamassassin}} {eq{${received_protocol}} {}} {eq{${received_protocol}} {local}} } {no}{yes}}"
suffix = "-*"
suffix_optional

SRH
--
Steve Haslam      Reading, UK                           araqnid@???
Debian GNU/Linux Maintainer                               araqnid@???
                               maybe the human race deserves to be wiped out