#!perl -w

=head1 NAME

rhsbl - handle RHSBL lookups

=head1 DESCRIPTION

Pluging that checks the host part of the sender's address against a
configurable set of RBL services.

=head1 CONFIGURATION

This plugin reads the lists to use from the rhsbl_zones configuration
file. Normal domain based dns blocking lists ("RBLs") which contain TXT
records are specified simply as:

  dsn.rfc-ignorant.org

To configure RBL services which do not contain TXT records in the DNS,
but only A records, specify, after a whitespace, your own error message
to return in the SMTP conversation e.g.

  abuse.rfc-ignorant.org does not support abuse@domain

=cut


sub register {
  my ($self, $qp, $denial ) = @_;
  if ( defined $denial and $denial =~ /^disconnect$/i ) {
    $self->{_rhsbl}->{DENY} = DENY_DISCONNECT;
  }
  else {
    $self->{_rhsbl}->{DENY} = DENY;
  }

}

sub hook_mail {
  my ($self, $transaction, $sender, %param) = @_;

  my $res = new Net::DNS::Resolver;
  my $sel = IO::Select->new();
  my %rhsbl_zones_map = ();

  # Perform any RHS lookups in the background. We just send the query packets
  # here and pick up any results in the RCPT handler.
  # MTAs gets confused when you reject mail during MAIL FROM:

    my %rhsbl_zones = map { (split /\s+/, $_, 2)[0,1] } $self->qp->config('rhsbl_zones');

  if ($sender->format ne '<>' and %rhsbl_zones) {
    push(my @hosts, $sender->host);
    #my $helo = $self->qp->connection->hello_host;
    #push(@hosts, $helo) if $helo && $helo ne $sender->host;
    for my $host (@hosts) {
    for my $rhsbl (keys %rhsbl_zones) {
      # fix to find TXT records, if the rhsbl_zones line doesn't have second field
      if (defined($rhsbl_zones{$rhsbl})) {
        $self->log(LOGDEBUG, "Checking $host.$rhsbl for A record in the background");
        $sel->add($res->bgsend("$host.$rhsbl"));
      } else {
        $self->log(LOGDEBUG, "Checking $host.$rhsbl for TXT record in the background");
        $sel->add($res->bgsend("$host.$rhsbl", "TXT"));
      }
        $rhsbl_zones_map{"$host.$rhsbl"} = $rhsbl_zones{$rhsbl};
    }
  }

    %{$self->{_rhsbl_zones_map}} = %rhsbl_zones_map;
    $transaction->notes('rhsbl_sockets', $sel);
  } else {
    $self->log(LOGDEBUG, 'no RHS checks necessary');
  }

  return DECLINED;
}

sub hook_rcpt {
  my ($self, $transaction, $rcpt) = @_;
  my $host = $transaction->sender->host;
  my $hello = $self->qp->connection->hello_host;

  my $result = $self->process_sockets;
  if ($result && defined($self->{_rhsbl_zones_map}{$result})) {
    if ($result =~ /^$host\./ ) {
      return ($self->{_rhsbl}->{DENY}, "Mail from $host rejected because it " . $self->{_rhsbl_zones_map}{$result});
    } else {
      return ($self->{_rhsbl}->{DENY}, "Mail from HELO $hello rejected because it " . $self->{_rhsbl_zones_map}{$result});
    }
  }
  return ($self->{_rhsbl}->{DENY}, $result) if $result;
  return DECLINED;
}

sub process_sockets {
  my ($self) = @_;
  my $trans = $self->transaction;
  my $result = '';

  return $trans->notes('rhsbl') if $trans->notes('rhsbl');

  my $res = new Net::DNS::Resolver;
  my $sel = $trans->notes('rhsbl_sockets') or return '';

  $self->log(LOGDEBUG, 'waiting for rhsbl dns');

  # don't wait more than 8 seconds here
  my @ready = $sel->can_read(8);

  $self->log(LOGDEBUG, 'DONE waiting for rhsbl dns, got ' , scalar @ready, ' answers ...') ;
  return '' unless @ready;

  for my $socket (@ready) {
    my $query = $res->bgread($socket);
    $sel->remove($socket);
    undef $socket;

    if ($query) {
      foreach my $rr ($query->answer) {
        $self->log(LOGDEBUG, 'got an ' . $rr->type . ' record ' . $rr->name);
        if ($rr->type eq 'A') {
          $result = $rr->name;
          $self->log(LOGDEBUG, "A record found for $result with IP " . $rr->address);
          last;
        } elsif ($rr->type eq 'TXT') {
          $result = $rr->txtdata;
          $self->log(LOGDEBUG, "TXT record found: " . $rr->txtdata);
          last;
        }
      }
    } else {
      $self->log(LOGCRIT, "query failed: ", $res->errorstring) unless $res->errorstring eq 'NXDOMAIN';
    }

    if ($result) {
      #kill any other pending I/O
      $trans->notes('rhsbl_sockets', undef);
      return $trans->notes('rhsbl', $result);
    }
  }

  if ($sel->count) {
    # loop around if we have dns results left
    return $self->process_sockets();
  }

  # if there was more to read; then forget it
  $trans->notes('rhsbl_sockets', undef);

  return $trans->notes('rhsbl', $result);
}

sub hook_disconnect {
  my ($self, $transaction) = @_;

  $transaction->notes('rhsbl_sockets', undef);
  return DECLINED;
}