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