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