#!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 use strict; use warnings; use Qpsmtpd::Constants; sub register { my ($self, $qp) = (shift, shift); if (@_ == 1) { $self->legacy_positional_args(@_); } else { $self->{_args} = {@_}; } $self->{_args}{reject} = 1 if !defined $self->{_args}{reject}; $self->{_args}{reject_type} ||= 'perm'; } sub legacy_positional_args { my ($self, $denial) = @_; if (defined $denial && $denial =~ /^disconnect$/i) { $self->{_args}{reject_type} = 'disconnect'; } else { $self->{_args}{reject_type} = 'perm'; } } sub hook_mail { my ($self, $transaction, $sender, %param) = @_; return DECLINED if $self->is_immune(); if ($sender->format eq '<>') { $self->log(LOGINFO, 'pass, null sender'); return DECLINED; } my %rhsbl_zones = $self->populate_zones() or return DECLINED; my $res = $self->get_resolver(); my @hosts = $sender->host; for my $host (@hosts) { for my $rhsbl (keys %rhsbl_zones) { my $query; # 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"); $query = $res->query("$host.$rhsbl"); } else { $self->log(LOGDEBUG, "Checking $host.$rhsbl for TXT record"); $query = $res->query("$host.$rhsbl", 'TXT'); } if (!$query) { if ($res->errorstring ne 'NXDOMAIN') { $self->log(LOGCRIT, "query failed: ", $res->errorstring); } next; } my $result; foreach my $rr ($query->answer) { $self->log(LOGDEBUG, 'got an ' . $rr->type . ' record ' . $rr->name); if ($rr->type eq 'A') { $self->log(LOGDEBUG, "A record found for $result with IP " . $rr->address); $result = $rr->name; } elsif ($rr->type eq 'TXT') { $result = $rr->txtdata; $self->log(LOGDEBUG, "TXT record found: " . $rr->txtdata); } next if !$result; $self->log(LOGINFO, "fail, $result"); if ($transaction->sender) { my $host = $transaction->sender->host; if ($result =~ /^$host\./) { return $self->get_reject( "Mail from $host rejected because it $result"); } } my $hello = $self->qp->connection->hello_host; return $self->get_reject( "Mail from HELO $hello rejected because it $result"); } } } $self->log(LOGINFO, "pass"); return DECLINED; } sub populate_zones { my $self = shift; my %rhsbl_zones = map { (split /\s+/, $_, 2)[0, 1] } $self->qp->config('rhsbl_zones'); if (!keys %rhsbl_zones) { $self->log(LOGINFO, 'pass, no zones'); return; } return %rhsbl_zones; }