quarantine to a pipe ?
Mark Martinec
Mark.Martinec+amavis at ijs.si
Wed Dec 14 17:14:46 CET 2011
Andreas,
> quarantine to pipe failed.
>
> I put this in amavisd.conf:
> $spam_quarantine_method = 'pipe:/usr/bin/opendkim-spam -f -o
> opendkim-spam.out -v -v -v'; $log_level = 5;
>
> and got that:
> Dec 9 09:59:39 test amavis[12353]: (12353) mail_via_pipe running command:
> /usr/bin/opendkim-spam -N NEVER -f -o opendkim-spam.out -v -v -v
> Dec 9 09:59:39 test amavis[12362]: (12353) (!)run_command_consumer:
> child process [12362]: Insecure dependency in exec
> while running with -T switch at /usr/sbin/amavisd line 3650,
Thanks, a bug indeed. Actually teo of them: the "NEVER" string is tainted,
and the -N command line option should not be used unless the command
is some form aof a 'sendmail' command.
The attached patch should fix it. Thanks!
> I have to pipe all spam to an opendkim statistic collector:
> $ cat spammail | opendkim-spam
> Could it be done direct by amavisd-new as quarantine-method?
Should be possible, e. g. through archive_quarantine:
@archive_quarantine_to_maps = ( 'any' );
$archive_quarantine_method = 'pipe:/usr/local/sbin/yourprog';
Btw, I'm using a custom hook for this purpose, attached as well.
Results are approved by Murray S. Kucherawy to be compatible/usable
with his opendkim milter statistic results / auto-reporting.
Mark
-------------- next part --------------
A non-text attachment was scrubbed...
Name: 0.patch
Type: text/x-patch
Size: 919 bytes
Desc: not available
URL: <http://lists.amavis.org/pipermail/amavis-users/attachments/20111214/c4a2cc2a/attachment.bin>
-------------- next part --------------
package Amavis::Custom;
use strict;
use re 'taint';
use warnings;
sub new {
my($class,$conn,$msginfo) = @_;
bless {}, $class;
}
sub after_send {
my($self,$conn,$msginfo) = @_;
opendkim_stats(@_);
}
use Digest::SHA;
use Digest::MD5 qw(md5_hex);
use Fcntl qw(LOCK_SH LOCK_EX LOCK_UN);
use IO::File qw(O_RDONLY O_WRONLY O_RDWR O_APPEND O_CREAT O_EXCL);
our($stat_filename, $stat_fh, $stat_open_attempted, $stat_dev, $stat_ino);
$stat_filename = '/var/db/opendkim/amavis-dkim-stats.log';
sub opendkim_stats {
my($self,$conn,$m) = @_;
if ($stat_fh) { # check if the file changed
my @status_list = stat($stat_filename);
my($t_stat_dev, $t_stat_ino) = @status_list;
if ($t_stat_dev != $stat_dev || $t_stat_ino != $stat_ino) {
do_log(2, "opendkim_stats: reopening file %s", $stat_filename);
$stat_fh->close or do_log(-1,"opendkim_stats: error closing: %s", $!);
$stat_dev = $stat_ino = $stat_fh = $stat_open_attempted = undef;
}
}
if (!$stat_open_attempted) {
$stat_open_attempted = 1;
eval {
$stat_fh = IO::File->new;
$stat_fh->open($stat_filename, O_CREAT|O_APPEND|O_WRONLY, 0640)
or die "Failed to open stats file $stat_filename: $!";
($stat_dev, $stat_ino) = stat($stat_filename);
binmode($stat_fh,':bytes') or die "Can't cancel :utf8 mode: $!";
$stat_fh->autoflush(1);
1;
} or do {
undef $stat_fh;
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
die "opendkim_stats: $eval_stat";
};
}
return if !$stat_fh;
my(@dkim_all, @dkim_valid, %atps_seen); local($1);
@dkim_all = @{$m->dkim_signatures_all} if $m->dkim_signatures_all;
@dkim_valid = @{$m->dkim_signatures_valid} if $m->dkim_signatures_valid;
@dkim_all = grep(!$_->isa('Mail::DKIM::DkSignature'), @dkim_all);
@dkim_valid = grep(!$_->isa('Mail::DKIM::DkSignature'), @dkim_valid);
my @rfc2822_from = do { my $f = $m->rfc2822_from; ref $f ? @$f : $f };
my @author_domains = map(/\@([^\@]*)\z/s ? lc $1 : (), @rfc2822_from);
my($adsp_found, $adsp_unknown, $adsp_all, $adsp_discard, $adsp_failed) =
(0) x 5;
my $eval_stat;
for my $author_domain (@author_domains) {
next if $author_domain !~ /(.\.[a-z-]{2,})\z/si; # non-FQDN
my $practices; # author domain signing practices object
eval {
$practices = Mail::DKIM::AuthorDomainPolicy->fetch(
Protocol => 'dns', Domain => $author_domain); 1;
} or do {
$eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
undef $practices;
};
my $sp; # ADSP: unknown / all / discardable
($sp) = $practices->policy if $practices;
$adsp_found = 1 if $practices && defined $practices->{tags}->{dkim};
my $atps_y = $adsp_found &&
grep($_ eq 'atps=y', split(' ', $practices->{tags}->{dkim})) ? 1 : 0;
if (!$practices) {
# do_log(2,"HERE ADSP: %s -> none, %s",
# $author_domain, !defined $sp ? 'UNDEF' : "/$sp/");
} elsif (!defined $practices->{tags}->{dkim}) {
do_log(2,"HERE ADSP: %s -> UNDEF, %s",
$author_domain,
!defined $sp ? 'UNDEF' : "/$sp/");
} else {
do_log(2,"HERE ADSP: %s -> /%s/, %s",
$author_domain, $practices->{tags}->{dkim},
!defined $sp ? 'UNDEF' : "/$sp/");
}
if (!defined $sp) { # SERVFAIL or a timeout
} elsif (uc $sp eq 'NXDOMAIN') { # nonexistent domain
} elsif ($sp eq 'all') { $adsp_all = 1;
} elsif ($sp eq 'discardable') { $adsp_discard = 1;
} elsif ($sp eq 'strict') { $adsp_discard = 1;
} elsif ($sp eq 'unknown' && defined $practices->{tags}->{dkim})
{ $adsp_unknown = 1 }
if ($adsp_all || $adsp_discard) {
$adsp_failed = 1 if !grep($author_domain eq lc $_->domain, @dkim_valid);
}
for my $s (@dkim_valid) { # ATPS draft
local $_ = Digest::SHA->new->add(lc $s->domain)->digest; # SHA-1, 160 bit
local $1; $_ = unpack('B*',$_); s/(.{5})/000$1/gs; $_ = pack('B*',$_);
tr/\000-\037/A-Z2-7/; # RFC 4648
$_ .= '._atps.' . $author_domain;
if (!$atps_seen{$_}++) { do_log(2,"HERE ATPS: %d, %s", $atps_y, $_) }
}
last if $eval_stat =~ /^timed out\b/;
last; # currently opendkim stats reflect only the first author
}
my $anon = 0;
my $prefix = '';
my $jobid = $m->queue_id;
$jobid = $1 if !defined $jobid &&
$m->per_recip_data->[0]->recip_remote_mta_smtp_response
=~ /queued as ([0-9A-Z]+)$/;
$jobid = $m->mail_id if !defined $jobid;
my @msg_rec = (
jobid => $jobid, # MTA-provided job/envelope ID
reporter => c('myhostname'), # reporter (defaults to hostname)
fromdomain => # first domain found in the From: h.f.
map($anon ? md5_hex($prefix,$_) : $_, $author_domains[0]),
ipaddr => # SMTP client IP address
map($anon ? md5_hex($prefix,$_) : $_, $m->client_addr),
anonymized => $anon, # anonymized (0=no, 1=yes)
msgtime => int $m->rx_time, # UNIX timestamp of msg receive time
msglen => $m->orig_body_size, # message body size, in bytes
sigcount => scalar @dkim_all, # signature count
adsp_found => $adsp_found, # ADSP record found in DNS (0=no, 1=yes)
adsp_unknown => $adsp_unknown, # ADSP "unknown" policy in DNS (0/1)
adsp_all => $adsp_all, # ADSP "all" policy in DNS (0/1)
adsp_discard => $adsp_discard, # ADSP "discardable" policy in DNS (0/1)
adsp_fail => $adsp_failed, # ADSP failed for this msg (0=no, 1=yes)
mailinglist => # appeared to come from a mailing list (0=no, 1=yes)
$m->is_mlist ? 1 : 0,
receivedcnt => # count of Received: header fields
do { my $j = 0;
while (defined $m->get_header_field('received',$j)) { $j++ }; $j },
contenttype =>
$m->get_header_field_body('content-type')
=~ /^\s*([^;\s]*)/ ? lc $1 : undef,
contentencoding =>
$m->get_header_field_body('content-transfer-encoding')
=~ /^\s*([^;\s]+)/ ? lc $1 : '7bit',
atps_status => -1, # ATPS status (-1=not checked, 0=no, 1=yes)
);
my(@result, $j);
# collect every other element, i.e. just values, no field names
push(@result, 'M' . join("\t", map(defined $_ && $_ ne '' ? $_ : '-',
grep(($j=!$j, !$j), @msg_rec))) . "\n");
for my $s (@dkim_all) {
my @changed_fields;
my $tag_z = $s->get_tag('z');
if (defined $tag_z) {
do_log(2,"HERE Z0: %s", $tag_z);
my($hc,$bc) = $s->canonicalization;
$tag_z =~ tr/ \t\r\n//d;
my @fields = split(m{\|},$tag_z,-1);
for (@fields) {
local($1,$2);
s/=([0-9a-fA-F]{2})/pack('C',hex($1))/egs;
do_log(2,"HERE Z1: %s", $_);
if ($hc eq 'relaxed') {
s/^([^:]*?[ \t]*:)/lc $1/se;
s/\r\n(?=[ \t])//gs; s/[ \t]+/ /gs;
s/[ \t]+\r\n/\r\n/s; s/[ \t]*:[ \t]*/:/s;
}
if (/^([^:]*?)[ \t]*:(.*)\z/s) {
my($hn, $hb) = ($1, $2);
my(@mismatches, $match);
for (my $j=-1; ; $j--) {
my($field_ind, $field_name, $field) = $m->get_header_field($hn,$j);
last if !defined $field_ind;
$field =~ s/\n\z//; $field =~ s/\n/\r\n/gs;
if ($hc eq 'relaxed') {
local $_ = $field;
s/^([^:]*?[ \t]*:)/lc $1/se;
s/\r\n(?=[ \t])//gs; s/[ \t]+/ /gs;
s/[ \t]+\r\n/\r\n/s; s/[ \t]*:[ \t]*/:/s;
$field = $_;
}
if ($field eq $_) { $match = 1; last }
else { push(@mismatches, $field) }
}
if ($match) {
do_log(2,"HERE Z2: match %s", $_);
} else {
do_log(2,"HERE Z2: NO match %s <-> %s", $_,join("\n", at mismatches));
push(@changed_fields, lc $hn);
}
}
}
}
my $pk = eval { $s->get_public_key }; # contain failures/exceptions
my $id = $s->get_tag('i');
my $rdetail = $m->originating && !defined $s->result ?
'pass, just generated, assumed good'
: $s->result_detail;
my $is_dk = $s->isa('Mail::DKIM::DkSignature');
# error codes according to libopendkim/dkim.h
my $sigerr =
!defined $s->get_tag('d') ? 15 # MISSING_D
: $s->get_tag('d') eq '' ? 16 # EMPTY_D
: !defined $s->get_tag('s') ? 17 # MISSING_S
: $s->get_tag('s') eq '' ? 18 # EMPTY_S
: !$is_dk && !defined $s->get_tag('v') ? 44 # MISSING_V
: defined $s->get_tag('v') && $s->get_tag('v') eq '' ? 45 # EMPTY_V
: defined $s->get_tag('v') && $s->get_tag('v') ne '1' ? 1 # VERSION
: $rdetail =~ /invalid domain in d tag/ ? 2 # DOMAIN
: $rdetail =~ /bad identity/ ? 2 # DOMAIN
: $rdetail =~ /signature is expired/ ? 3 # EXPIRED
: !$is_dk && $s->timestamp && $s->timestamp > $m->rx_time+1 ? 4 # FUTURE
: defined $s->get_tag('t') && $s->get_tag('t') !~ /^\d{1,10}\z/? 5
: defined $s->get_tag('x') && $s->get_tag('x') !~ /^\d{1,10}\z/? 5
: defined $s->get_tag('t') && defined $s->get_tag('x') &&
$s->get_tag('t') > $s->get_tag('x') ? 5 # TIMESTAMPS
# : !defined $s->get_tag('c') ? 6 # MISSING_C
: !$is_dk && defined $s->get_tag('c') &&
$s->get_tag('c') !~ m{^(simple|relaxed)(/|\z)} ? 7 # INVALID_HC
: !$is_dk && defined $s->get_tag('c') &&
$s->get_tag('c') !~ m{^(simple|relaxed)(/(simple|relaxed))?\z}
? 8 # INVALID_BC
: $is_dk && defined $s->get_tag('c') &&
$s->get_tag('c') !~ m{(simple|nofws)$} ? 8 # INVALID_HC
: !$is_dk && !defined $s->get_tag('a') ? 9 # MISSING_A
: $rdetail =~ /unsupported algorithm/ ? 10 # INVALID_A
: !$is_dk && !defined $s->get_tag('h') ? 11 # MISSING_H
: defined $s->get_tag('h') && $s->get_tag('h') eq ''? 31 # EMPTY_H
# : !@author_domains ? 32 # INVALID_H #***
: defined $s->get_tag('l') && $s->get_tag('l') !~ /^\d{1,10}\z/ ? 12
# INVALID_L
### 33 # TOOLARGE_L
: $rdetail =~ /unsupported query protocol/ ? 13 # INVALID_Q
### 14 # INVALID_QO
: !$is_dk && !defined $s->get_tag('bh') ? 25 # MISSING_BH
: defined $s->get_tag('bh') && $s->get_tag('bh') eq '' ? 26 # EMPTY_BH
: defined $s->get_tag('bh') &&
do { local $_ = $s->get_tag('bh'); tr/ \t\r\n//d;
!m{^ [a-z0-9+/]+ ={0,2} \z}xsi } ? 27 # CORRUPT_BH
: !defined $s->get_tag('b') ? 19 # MISSING_B
: $s->get_tag('b') eq '' ? 20 # EMPTY_B
: do { local $_ = $s->get_tag('b'); tr/ \t\r\n//d;
!m{^ [a-z0-9+/]+ ={0,2} \z}xsi } ? 21 # CORRUPT_B
: $pk && defined $pk->get_tag('v') && $pk->get_tag('v') ne 'DKIM1'
? 35 # KEYVERSION
: $pk && defined $pk->get_tag('h') &&
$pk->get_tag('h') !~ /^([a-z][a-z0-9-]*)(:([a-z][a-z0-9-]*))*\z/si
? 36 # KEYUNKNOWNHASH
: $pk && defined $pk->get_tag('h') &&
!grep($_ eq $s->hash_algorithm, split(/:/,$pk->get_tag('h')))
? 37 # KEYHASHMISMATCH
: $pk && defined $pk->get_tag('s') &&
!grep($_ eq '*' || $_ eq 'email' , split(/:/,$pk->get_tag('s')))
? 38 # NOTEMAILKEY
: $rdetail =~ /public key: unsupported version/ ? 35 # KEYVERSION
: $rdetail =~ /public key: granularity/ ? 39 # GRANULARITY
: $rdetail =~ /public key: DNS query timeout/ ? 24 # KEYFAIL
: $rdetail =~ /public key: not available/ ? 22 # NOKEY
: $rdetail =~ /public key: unknown query type/ ? 22 # NOKEY
# (key type tag is optional) ### 40 # KEYTYPEMISSING
: $rdetail =~ /public key: unsupported key type/ ? 41 # KEYTYPEUNKNOWN
: $rdetail =~ /public key: revoked/ ? 42 # KEYREVOKED
: $rdetail =~ /public key: invalid data/ ? 43 # KEYDECODE
: $rdetail =~ /public key: OpenSSL error/ ? 43 # KEYDECODE
: $rdetail =~ /public key:/ ? 43 # KEYDECODE
: !$pk ? 22 # NOKEY
: $rdetail =~ /bad RSA signature/ ? 28 # BADSIG
: $rdetail =~ /has been altered/ ? 28 # BADSIG
: $rdetail =~ /public key: does not support signing subdomains/
? 29 # SUBDOMAIN
### 23 # DNSSYNTAX
### 30 # MULTIREPLY
### 34 # MBSFAILED
: $s->result eq 'pass' || ($m->originating && !defined $s->result)
? 0 # OK
: -1;
if ($sigerr != 0 &&
($s->result eq 'pass' || ($m->originating && !defined $s->result))) {
do_log(2, "opendkim_stats: status mismatch: %s, %s", $sigerr,$rdetail);
# $sigerr = 0;
}
my @sig_rec = (
domain => # domain of the signature
map($anon ? md5_hex($prefix,$_) : $_, lc $s->domain),
algorithm => # algorithm (0=rsa-sha1, 1=rsa-sha256)
lc $s->algorithm eq 'rsa-sha256' ? 1 : 0,
header_canon => # header canonicalization (0=simple, 1=relaxed)
do { my($hc,$bc) = $s->canonicalization; $hc eq 'relaxed' ? 1 : 0 },
body_canon => # body canonicalization (0=simple, 1=relaxed)
do { my($hc,$bc) = $s->canonicalization; $bc eq 'relaxed' ? 1 : 0 },
ignore => 0, # tagged for ignore (0=no, 1=yes)
pass => $s->result eq 'pass' || # pass (0=no, 1=yes)
($m->originating && !defined $s->result) ? 1 : 0,
fail_body => # failed due to "bh" mismatch (0=no, 1=yes)
$rdetail =~ /body has been altered/ ? 1 : 0,
siglength => # $s->body_count, # "l=" tag value (-1=not present)
!defined $s->get_tag('l') ? -1 : $s->get_tag('l'),
key_t => # "t=" present in key (0=no, 1=yes)
$pk && defined $pk->get_tag('t') ? 1 : 0,
key_g => # "g=" present in key (0=no, 1=yes)
$pk && defined $pk->get_tag('g') ? 1 : 0,
key_g_name => # "g=" present in key with a value other than "*" (0=n,1=y)
$pk && defined $pk->get_tag('g') && $pk->get_tag('g') ne '*' ? 1 : 0,
key_dk_compat => # key was DK-compatible (0=no, 1=yes)
$pk && !defined $pk->get_tag('v') &&
defined $pk->get_tag('g') && $pk->get_tag('g') eq '' ? 1 : 0,
sigerror => $sigerr, # error code from signature
sig_t => $s->get_tag('t') ? 1 : 0, # "t=" present (0=no, 1=yes)
sig_x => $s->get_tag('x') ? 1 : 0, # "x=" present (0=no, 1=yes)
sig_z => $s->get_tag('z') ? 1 : 0, # "z=" present (0=no, 1=yes)
dnssec => -1, # DNSSEC value (see DKIM_DNSSEC_* const from dkim.h)
signed_fields => # colon-separated list of header fields signed
!defined $s->get_tag('h') ? undef : scalar $s->headerlist,
changed_fields =>
# colon-separated list of header fields known to have changed
!@changed_fields ? '' : join(':', at changed_fields),
sig_i => # "i=" domain (0=absent, 1=matches From:, 2=other)
!defined $id ? 0 : lc $id eq '@'.lc($s->domain) ? 1 : 2,
sig_i_user => # "i=" user (0=absent, 1=empty, 2=matches From:, 3=other)
!defined $id ? 0 : $id =~ /^\@/ ? 1 :
do { local $1; $id =~ /^(.*)\@/; my $ilp = $1;
$rfc2822_from[0] =~ /^(.*)\@/; $ilp eq $1 } ? 2 : 3,
key_s => # "s=" present in key (0=absent, 1="*", 2="email", 3=other)
!$pk || !defined $pk->get_tag('s') ? 0
: $pk->get_tag('s') eq '*' ? 1
: $pk->get_tag('s') eq 'email' ? 2 : 3,
keysize => # key size, in bits
$pk && $pk->cork ? $pk->cork->size * 8 : 0,
);
my $j;
# collect every other element, i.e. just values, no field names
push(@result, 'S' . join("\t", map(defined $_ && $_ ne '' ? $_ : '-',
grep(($j=!$j, !$j), @sig_rec))) . "\n");
}
if ($stat_fh && !$m->originating) {
flock($stat_fh,LOCK_EX) or die "Can't lock a stats file: $!";
seek($stat_fh,0,2) or warn "Can't position stats file to its tail: $!";
$stat_fh->print(join('', at result)) or warn "Error writing to stats file: $!";
flock($stat_fh,LOCK_UN) or die "Can't unlock a stats file: $!";
}
s/\t/, /g for @result;
do_log(2, "HERE STAT: %s", $_) for @result;
if (defined $eval_stat && $eval_stat =~ /^timed out\b/) {
die $eval_stat # resignal timeout
}
}
1; # insure a defined return
More information about the amavis-users
mailing list