Rewritten URIBL plugin take two

[email protected] ("Jared Johnson")
Newsgroups perl.qpsmtpd
Message-ID <[email protected]>
Attached:

uribl: the new URIBL plugin with some changes suggested on the list

uribl.diff: diff from the first copy of this plugin I posted to the list
            for anyone who's been following it and prefers that.  *not* a
            diff from the original plugin, that would be too noisy to be
            useful :)

parse_mime: new plugin that can optionally be added in order to decode MIME;
            the uribl plugin will use the decoded MIME if it's there, else
            it will use the old method.  I didn't try to do anything cooler
            with the old method yet as I haven't had time to set up a test

async.diff: diff necessary for the async plugin to work with this plugin.
            UNTESTED!

gen_tld_lists: the script I've been using to generate tld_lists.pl, FTR

Feel free to reiterate any old requests, I may have had a reason not to do
it or I may have just forgotten :)  Highlights:

- new config format (the old method is still supported).  $zones is now
  more genuinely a structure for 'service specific defaults'
- shifted things around to support async and patched the async plugin and
  Q::Plugin::Async::DNSBLBase to work with the new plugin.  I tried very
  hard not to touch these things much because I have no way to test, but it
  was necessary to make quite a lot of changes to support the new plugin,
  especially to support lookup_a_record and lookup_ns_record.  I am *sure*
  there are serious bugs in this but I'm fairly convinced they will be
  trivially found and fixed.
- some more entity un-munging and fixed the &amp; thing
- a handful of other little things I did in the course of my own work

I haven't yet pulled the trigger on moving tld_lists.pl into an API, see
the thread on why.

I am somewhat interested in switching the non-async plugin to still use
ParaDNS, but it's not in my immediate plans; it doesn't make much
difference for performance of the non-async version, and it would mean a
new dependency on a module that apparently is not packaged for Debian in
our own Debian-based software.  Plus ParaDNS or at least the DNSBLBase
wrapper is not quite as flexible and easy to work with as N::D::A.

This code is a bit fresher so there may be further updates as I get
further into testing.  I'll probably wait to submit those until it's time
for hopefully one final shot.  That seems easiest anyway considering that
I still don't have a timeline on getting my git on again.  My apologies
once again for the lack of git.  Happy reviewing!

-Jared
uribl (application/octet-stream, 22.6 KB) - not displayed
uribl.diff (text/x-patch, 16.3 KB)
--- dc-smtpd//plugins/dc/uribl.qp	2010-07-26 15:50:21.425571890 -0500
+++ dc-smtpd//plugins/dc/uribl	2010-07-27 04:59:27.685571284 -0500
@@ -15,13 +15,14 @@
 
 To enable the plugin, add it to I<~qpsmtpd/config/plugins>.  The list of
 URIBLs to check should be placed in I<uribl_zones> in the config directory
-(typically I<~qpsmtpd/config>).
+(typically I<~qpsmtpd/config>).  If no zones appear in this file, a default
+set of services is checked.
 
 The format of the I<uribl_zones> file is a list of URIBL DNS zones, one per
 line, consisting of one or more columns separated by whitespace.  The first
 column (the only mandatoy one) should consist of the URIBL zone.
 
-The second column may contain a comma-delimited list of integers selecting
+The second column may optionally contain a comma-delimited list of integers selecting
 mask values to be applied to the A record(s) returned from a URIBL.  This
 enables the use of composite DNSBLs, such as multi.surbl.org, where several
 lists are combined so they may be accessed with a single query; any returns
@@ -30,15 +31,74 @@
 checked.  URIBL operators prefer that you use the composite lists to reduce
 their own query load, and it's more efficient for qpsmtpd as well.
 
+The next two columns may optionally contain a configuration directive and
+corresponding value as described below, to be applied either to the entire
+zone, or to the masks specified in the second column.
 The third column specifies an action, which overrides the default action
 configured with the I<action> setting discussed below.
 
+Any masks and parameters not listed will inherit default parameters recommended
+by the zone in question, if known.
+
 For example:
 
 =over 4
 
-    multi.surbl.org    2,8    deny
-    ob.surbl.org    1    add-header
+    dbl.spamhaus.org
+
+    multi.uribl.com           action denysoft
+    multi.uribl.com    2,4    action deny
+
+    multi.surbl.org
+    multi.surbl.org    2,8    action deny
+    multi.surbl.org    4,16
+
+=back
+
+You may specify the following config option(s) in the I<uribl_zones> file:
+
+=over 4
+
+=item action
+
+Overrides the default action configured with the I<action> setting discussed
+below.  This can be set either for the entire zone or for a specific set of
+masks.
+
+=item check_ip
+
+Submit IP addresses found in URIs to the URIBL service.  Some services
+discourage sending IP addresses and some warn that they will result in false
+positives.  SpamHaus DBL, for instance, strongly discourages it and will in fact
+always give a positive result when sent an IP address.  Defaults to enabled
+unless service-specific defaults exists.
+
+=item lookup_a_record
+
+Look up A records on hostnames found in URIs and check the results against the
+zone in question.  SBL-XBL is one service that specifically recommends doing
+this; others discourage it and may generate false positives if you try it.
+Defaults to disabled unless service-specific defaults exist.
+
+=item lookup_ns_record
+
+Look up NS records on hostnames found in URIs and check the results against the
+zone in question.  Like lookup_a_records, this is recommended by SBL-XBL but
+could generate false positives on other services.  Defaults to disabled unless
+service-specific defaults exist.
+
+=item check_host
+
+Submit hostnames found in URIs to the URIBL service.  Some services, especially
+'RBL' services, only list IP addresses.  SBL-XBL is an example of this; SpamHaus
+recommends checking IP addresses from URIs to this list.  Defaults to enabled
+unless service-specific defaults exist.
+
+=item host_wildcards_supported
+
+The zone supports wildcards -- e.g. checking a.b.c.com will generate a hit if
+a.b.c.com, b.c.com, or c.com is listed.  Spamhaus DBL supports wildcard; most
+services do not.  Defaults to disabled unless service-specific defaults exist.
 
 =back
 
@@ -105,27 +165,27 @@
 my $zones = {
     'dbl.spamhaus.org' => {
         label => 'Spamhaus DBL',
-        action => 'log',
+        # action => 'log'
         check_ip => 0,
         check_host => 1,
         host_wildcards_supported => 1,
     },
     'sbl-xbl.spamhaus.org' => {
         label => 'Spamhaus SBL/XBL',
-        action => 'log',
         check_ip => 1,
         lookup_a_record => 1,
         lookup_ns_record => 1,
         check_host => 0,
+        host_wildcards_supported => 0,
     },
     'multi.surbl.org' => {
         label => 'SURBL',
-        masks => { 2 => { label => 'SpamCop',              action => 'log' },
-                   4 => { label => 'sa-blacklist',         action => 'log' },
-                   8 => { label => 'Phishing and Malware', action => 'log' },
-                  16 => { label => 'Outblaze',             action => 'log' },
-                  32 => { label => 'AbuseButler',          action => 'log' },
-                  64 => { label => 'jwSpamSpy',            action => 'log' } },
+        masks => { 2 => { label => 'SpamCop' }, # , action => 'log' },
+                   4 => { label => 'sa-blacklist' },
+                   8 => { label => 'Phishing and Malware' },
+                  16 => { label => 'Outblaze' },
+                  32 => { label => 'AbuseButler' },
+                  64 => { label => 'jwSpamSpy' } },
         check_ip => 1,
         check_host => 1,
         host_wildcards_supported => 0,
@@ -134,10 +194,9 @@
     },
     'multi.uribl.com' => {
         label => 'URIBL',
-        masks => { 2 => { label => 'Black', action => 'log' },
-                   4 => { label => 'Grey',  action => 'log' },
-                   8 => { label => 'Red',   action => 'log' },
-                  16 => { label => 'Gold',  action => 'log' } },
+        masks => { 2 => { label => 'Black' },
+                   4 => { label => 'Grey' },
+                   8 => { label => 'Red' } },
         check_ip => 1,
         check_host => 1,
         host_wildcards_supported => 0,
@@ -146,9 +205,10 @@
     },
 };
 
-# Derive individual lists from their 'multi' counterparts
+# Derive individual list defaults from their 'multi' counterparts
 my $surbl = $zones->{'multi.surbl.org'};
-for ( [ 2 => 'sc' ], [ 4 => 'ws' ], [ 8 => 'ph' ], [ 16 => 'ob' ], [ 32 => 'ab' ], [ 64 => 'jp' ] ) {
+for ( [ 2  => 'sc' ], [ 4  => 'ws' ], [ 8  => 'ph' ],
+      [ 16 => 'ob' ], [ 32 => 'ab' ], [ 64 => 'jp' ] ) {
     my $z = $zones->{ "$_->[1].surbl.org" } = { %$surbl };
     my $descr = $z->{masks}->{ $_->[0] }->{label};
     $z->{label} = "$z->{label} ($descr)";
@@ -164,27 +224,51 @@
 
 
 
-sub init {
+sub init { # used by async plugin
     my ( $self, $qp, %args ) = @_;
-    $self->{timeout} = $args{timeout} || 30;
+    $self->{timeout} = $args{timeout} || $qp->config('dns_timeout') || 30;
     $self->{whitelist_zones} = { map { $_ => undef } $qp->config('uribl_whitelist_domains') };
+    # Includes support for legacy arg 'scan-headers'
     ( $self->{check_headers} ) = grep { defined } ( @args{qw( check-headers scan-headers )}, 0 );
     my $default_action = $args{action} || 'add-header';
     unless ( $qp->config('uribl_zones') ) {
-        $self->{zones} = $zones;
+        $self->{zones} = { map { $_ => $zones->{$_} }
+                           qw( dbl.spamhaus.org sbl-xbl.spamhaus.org
+                               multi.surbl.org multi.uribl.com ) };
         $self->init_masks;
         return;
     }
     my $localzones;
     for ( $self->qp->config('uribl_zones') ) {
-        my ( $zone, $masks, $action ) = split /\s+/;
+        my ( $zone, @arg ) = split /\s+/;
         my $z = $localzones->{$zone} ||= {};
-        $z->{action} ||= $action if $action;
-        for ( split /,/, $masks || '' ) {
-            $z->{masks}->{$_}->{action} = $action if $action;
-            next unless exists $zones->{$zone} and exists $zones->{$zone}->{masks};
-            $z->{masks}->{$_}->{action} ||= $zones->{$zone}->{masks}->{$_}->{action} || $action;;
-            $z->{masks}->{$_}->{label} = $zones->{$zone}->{masks}->{$_}->{label} || $_;
+        my @c = qw( action check_ip lookup_a_record lookup_ns_record
+                    check_host host_wildcards_supported );
+        if ( grep { $arg[0] eq $_ } @c ) {
+            # Config directive for entire zone
+            my ( $key, $value ) = @arg;
+            die "Could not parse config line: $_" unless defined $value;
+            $z->{$_} = $value if defined $value;
+            next;
+        } elsif ( $arg[0] !~ /[\d,]/ ) {
+            die "Could not parse config line: $_";
+        }
+        # mask definitions / settings
+        my ( $masks, $key, $value ) = @arg;
+        $z->{masks}->{$_} ||= {} for split /,/, $masks;
+        next unless defined $key; # Mask definition with no additional prefs
+        if ( grep { $key eq $_ } qw( log add-header deny denysoft ) ) { # legacy definition
+            ( $key, $value ) = ( 'action', $key );
+            $z->{action} ||= $value;
+        }
+        die "Could not parse config line: $_" if ! defined $value or ! grep { $key eq $_ } @c;
+        for my $m ( split /,/, $masks ) {
+            $z->{masks}->{$m}->{$key} = $value;
+            next if ! exists $zones->{$zone}
+                 or ! exists $zones->{$zone}->{masks}
+                 or ! exists $zones->{$zone}->{masks}->{$m};
+            $z->{masks}->{$m}->{label}  ||= $zones->{$zone}->{masks}->{$m}->{label} || $m;
+            $z->{masks}->{$m}->{action} ||= $zones->{$zone}->{masks}->{$m}->{action};
         }
     }
     for my $zone ( keys %$localzones ) {
@@ -206,6 +290,8 @@
     $self->init_masks;
 }
 
+sub register { shift->register_hook( data_post => 'data_handler' ) }
+
 sub init_masks {
     for my $zone ( values %{ shift->{zones} } ) {
         my $mask = 0;
@@ -216,27 +302,29 @@
     }
 }
 
-sub send_query {
+sub send_queries { # used async plugin
+    # Determine which queries to send to which zones and ship them off to the appropriate sub
     my ( $self, $addr, $arg ) = @_;
     return unless $addr;
-    $self->{dns} ||= Net::DNS::Async->new( Timeout => $self->{timeout} ) or return;
+    # Allow the async version to use ParaDNS rather than Net::DNS::Async
+    my $sendq = $self->{async_version} ? 'queue_pdns_query' : 'send_query';
     for my $z ( $arg->{zone} or keys %{ $self->{zones} } ) {
         my $q = { zone => $z, name => $arg->{forward_ip} || $addr };
         $self->{start_time} ||= time();
         if ( $self->{zones}->{$z}->{lookup_ns_record} and ! $arg->{from_cb} ) {
-            $self->add_lookup( $q, sub { $self->ns_record_cb( $q, @_ ) }, $addr, 'NS' );
+            $self->$sendq( $q, 'ns_record', $addr, 'NS' );
         }
         my $hostname;
         if ( ! $arg->{forward_ip} ) { # hostname
             next if $self->host_whitelisted( $addr );
-            $self->add_lookup( $q, sub { $self->a_record_cb( $q, @_ ) }, $addr, 'A' )
+            $self->$sendq( $q, 'a_record', $addr, 'A' )
                 if $self->{zones}->{$z}->{lookup_a_record};
             next unless $self->{zones}->{$z}->{check_host};
             $hostname = $self->shorten_host( $addr, $z );
         }
         next if $arg->{forward_ip} and ! $self->{zones}->{$z}->{check_ip};
         my $querystring = $hostname ? "$hostname.$z" : "$addr.$z";
-        $self->add_lookup( $q, sub { $self->uri_cb( $q, @_ ) }, $querystring, $_ ) for qw( A TXT );
+        $self->$sendq( $q, 'uri', $querystring, $_ ) for qw( A TXT );
     }
 }
 
@@ -267,8 +355,11 @@
     return join '.',@h[-2..-1];
 }
 
-sub add_lookup {
-    my ( $self, $q, $cb, $querystring, $type ) = @_;
+sub send_query { # NOT used by async plugin
+    my ( $self, $q, $cbtype, $querystring, $type ) = @_;
+    $cbtype = $cbtype . '_cb';
+    my $cb = sub { $self->$cbtype( $q, @_ ) };
+    $self->{dns} ||= Net::DNS::Async->new( Timeout => $self->{timeout} ) or return;
     my $key = "$q->{zone}|$querystring|$type";
     return if exists $self->{queries}->{$key};
     $self->{queries}->{$key} = undef;
@@ -310,7 +401,7 @@
         my $fwd = join '.', @fwd;
         my $rev = join '.', reverse @fwd;
         $self->log(LOGDEBUG,"uribl: looking up $fwd (A record of $q->{name})");
-        $self->send_query( $rev, { forward_ip => $fwd, zone => $q->{zone}, from_cb => 1 } );
+        $self->send_queries( $rev, { forward_ip => $fwd, zone => $q->{zone}, from_cb => 1 } );
     }
 }
 
@@ -322,23 +413,24 @@
         my $address = $answer->address or next;
         if ( $address =~ /[^\d.]/ ) { # hostname
             $self->log(LOGDEBUG,"uribl: looking up $address (NS record of $q->{name})");
-            $self->send_query( $address, { zone => $q->{zone}, from_cb => 1 } );
+            $self->send_queries( $address, { zone => $q->{zone}, from_cb => 1 } );
         } else { # IP address
             my @fwd = split /\./, $address or next;
             my $fwd = join '.', @fwd;
             my $rev = join '.', reverse @fwd;
             $self->log(LOGDEBUG,"uribl: looking up $fwd (NS record of $q->{name})");
-            $self->send_query( $rev, { forward_ip => $fwd, zone => $q->zone, from_cb => 1 } );
+            $self->send_queries( $rev, { forward_ip => $fwd, zone => $q->{zone}, from_cb => 1 } );
         }
     }
 }
 
-sub cleanup {
+sub cleanup { # used by async plugin
     my ( $self ) = @_;
-    delete @$self{qw( dns matched start_time queries_completed queries started_queries)};
+    delete @$self{qw( dns matched start_time queries_completed queries
+                      started_queries a_lookups ns_lookups pdns_lookups )};
 }
 
-sub evaluate {
+sub evaluate { # used by async plugin
     my ( $self, $zone, $a ) = @_;
     return unless $zone and $a;
     my $z = $self->{zones}->{$zone};
@@ -376,7 +468,7 @@
     $self->find_uris($line) while ( $line = $txn->body_getline );
 }
 
-sub find_uris {
+sub find_uris { # used by async plugin
     my $self = shift;
     $_ = shift;
     chomp;
@@ -384,11 +476,13 @@
     s/[=%]([0-9A-Fa-f]{2,2})/chr(hex($1))/ge;
     # Undo HTML entity munging (e.g. in parameterized redirects)
     s/&#(\d{2,3});?/chr($1)/ge;
+    # Undo more entity munging
+    s/&#?x0*([0-9A-Fa-f]{2});?/chr(hex($1))/ge;
     # Un-encode a few common important named entities and discard the rest
     s/&nbsp;/ /go;
-    s/&amp;/&/go;
     s/&gt;/>/go;
     s/&lt;/</go;
+    s/&amp;/&/go;
     s/&\w{2,6};//go;
 
     while ( m{
@@ -408,7 +502,7 @@
         my $rev = join('.', reverse @octets);
         $self->log(LOGDEBUG, "uribl: matched pure-integer ipaddr $1 ($fwd)");
         unless (exists $self->{pending}->{$rev}) {
-            $self->send_query($rev, { forward_ip => $fwd });
+            $self->send_queries($rev, { forward_ip => $fwd });
             $self->{pending}->{$rev} = undef;
         }
     }
@@ -431,7 +525,7 @@
         my $rev = join('.', reverse @octets);
         $self->log(LOGDEBUG, "uribl: matched URI ipaddr $fwd");
         unless (exists $self->{pending}->{$rev}) {
-            $self->send_query($rev, { forward_ip => $fwd });
+            $self->send_queries($rev, { forward_ip => $fwd });
             $self->{pending}->{$rev} = undef;
         }
     }
@@ -471,13 +565,12 @@
         }
         next if exists $self->{pending}->{$host};
         $self->log(LOGDEBUG, "uribl: matched hostname $host");
-        $self->send_query( $host );
+        $self->send_queries( $host );
     }
 }
 
-sub hook_data_post {
-    my ($self, $txn) = @_;
-    $self->cleanup();
+sub scan_message { # used by async plugin
+    my ( $self, $txn ) = @_;
     if ( $self->{check_headers} ) {
         $self->find_uris($_) for map { $txn->header->get($_) } $txn->header->tags;
     }
@@ -485,15 +578,26 @@
                              : $self->scan_body( $txn );
     unless ( $self->{started_queries} ) {
         $self->log(LOGINFO, "No URIs found in mail");
-        return DECLINED;
+        return 0;
     }
+    return 1;
+}
+
+sub data_handler { # NOT used by async plugin
+    my ( $self, $txn ) = @_;
+    $self->cleanup();
+    return DECLINED unless $self->scan_message( $txn );
     $self->{dns}->await() if $self->{dns};
     $self->log(LOGINFO, sprintf( "lookups: %2d   matches: %2d   time: %.6f",
                                  $self->{queries_completed},
                                  scalar @{ $self->{matches} || [] },
                                  time() - $self->{start_time} ));
+    return $self->check_results( $txn, $self->{matches} );
+}
 
-    for my $q ( @{ $self->{matches} || [] } ) {
+sub check_results { # used by async plugin
+    my ( $self, $txn, $matches ) = @_;
+    for my $q ( @{ $matches || [] } ) {
         $self->log(LOGDEBUG, $q->{desc});
         for ( @{ $q->{lists} } ) {
             $self->log(LOGWARN, "$q->{name} FOUND IN $_->{label}");
parse_mime (application/octet-stream, 1 KB) - not displayed
async.diff (text/x-patch, 8.8 KB)
Index: plugins/async/uribl
===================================================================
--- plugins/async/uribl	(revision 12753)
+++ plugins/async/uribl	(working copy)
@@ -27,55 +27,58 @@
 
 sub start_data_post {
     my ($self, $transaction) = @_;
-    my $class = ref $self;
-
-    my @names;
-
-    my $queries = $self->lookup_start($transaction, sub {
-        my ($self, $name) = @_;
-        push @names, $name;
-    });
-
-    my @hosts;
-    foreach my $z (keys %{$self->{uribl_zones}}) {
-        push @hosts, map { "$_.$z" } @names;
-    }
-
+    $self->cleanup();
     $transaction->notes(uribl_results => {});
-    $transaction->notes(uribl_zones => $self->{uribl_zones});
-
-    return DECLINED
-        unless @hosts && $class->lookup($self->qp, [ @hosts ], [ @hosts ]);
-
+    $transaction->notes(uribl_zones => $self->{zones});
+    $self->scan_message( $txn );
+    return DECLINED unless $self->{pdns_lookups}
+                       and $self->lookup( $self->qp, @{ $self->{pdns_lookups} } );
     return YIELD;
 }
 
 sub finish_data_post {
     my ($self, $transaction) = @_;
+    my $matches = $self->collect_results($transaction);
+    return $self->check_results($matches);
+}
 
-    my $matches = $self->collect_results($transaction);
-    for (@$matches) {
-        $self->log(LOGWARN, $_->{desc});
-        if ($_->{action} eq 'add-header') {
-            $transaction->header->add('X-URIBL-Match', $_->{desc});
-        } elsif ($_->{action} eq 'deny') {
-            return (DENY, $_->{desc});
-        } elsif ($_->{action} eq 'denysoft') {
-            return (DENYSOFT, $_->{desc});
-        }
+sub queue_pdns_query {
+    my ( $self, $q, $cbtype, $querystring, $type ) = @_;
+    my $key = "$q->{zone}|$querystring|$type";
+    return if exists $self->{queries}->{$key};
+    $self->{queries}->{$key} = undef;
+    $self->log(LOGDEBUG,"Looking up $type record for '$querystring'");
+    $self->{started_queries}++;
+    if ( $cbtype eq 'uri' ) {
+        push @{ $self->{pdns_lookups}->[0] }, $querystring;
+        push @{ $self->{pdns_lookups}->[1] }, $querystring;
+    } elsif ( $cbtype eq 'a_record' ) {
+        push @{ $self->{pdns_lookups}->[0] }, $querystring;
+        $self->{a_lookups}->{$querystring}->{ $q->{zone} = $q;
+    } elsif ( $cbtype eq 'ns_record' ) {
+        push @{ $self->{pdns_lookups}->[2] }, $querystring;
+        $self->{ns_lookups}->{$querystring}->{ $q->{zone} = $q;
     }
-    return DECLINED;
 }
 
 sub init_resolver { }
 
 sub process_a_result {
-    my ($class, $qp, $result, $query) = @_;
-
+    my ( $self, $qp, $result, $query ) = @_;
     my $transaction = $qp->transaction;
     my $results = $transaction->notes('uribl_results');
     my $zones = $transaction->notes('uribl_zones');
-
+    $self->{queries_completed}++;
+    if ( exists $self->{a_lookups}->{$query} ) {
+        my @fwd = split /\./, $result or next;
+        my $fwd = join '.', @fwd;
+        my $rev = join '.', reverse @fwd;
+        for my $q ( values %{ $self->{a_lookups}->{$query} || {} } ) {
+            $self->log(LOGDEBUG,"uribl: looking up $fwd (A record of $q->{name})");
+            $self->send_queries( $rev, { forward_ip => $fwd, zone => $q->{zone},
+                                         from_cb => 1 } );
+        }
+    }
     foreach my $z (keys %$zones) {
         if ($query =~ /^(.*)\.$z$/) {
             my $name = $1;
@@ -85,7 +88,8 @@
 }
 
 sub process_txt_result {
-    my ($class, $qp, $result, $query) = @_;
+    my ( $self, $qp, $result, $query ) = @_;
+    $self->{queries_completed}++;
 
     my $transaction = $qp->transaction;
     my $results = $transaction->notes('uribl_results');
@@ -99,6 +103,26 @@
     }
 }
 
+sub process_ns_result {
+    my ( $self, $qp, $result, $query ) = @_;
+    $self->{queries_completed}++;
+    if ( $result =~ /[^\d.]/ ) { # hostname
+        for my $q ( values %{ $self->{ns_lookups}->{$query} || {} } ) {
+            $self->log(LOGDEBUG,"uribl: looking up $result (NS record of $q->{name})");
+            $self->send_queries( $result, { zone => $q->{zone}, from_cb => 1 } );
+        }
+    } else { # IP address
+        my @fwd = split /\./, $result or next;
+        my $fwd = join '.', @fwd;
+        my $rev = join '.', reverse @fwd;
+        for my $q ( values %{ $self->{ns_lookups}->{$query} || {} } ) {
+            $self->log(LOGDEBUG,"uribl: looking up $fwd (NS record of $q->{name})");
+            $self->send_queries( $rev, { forward_ip => $fwd, zone => $q->{zone},
+                                         from_cb => 1 } );
+        }
+    }
+}
+
 sub collect_results {
     my ($self, $transaction) = @_;
 
@@ -108,12 +132,14 @@
     foreach my $z (keys %$results) {
         foreach my $n (keys %{$results->{$z}}) {
             if (exists $results->{$z}->{$n}->{a}) {
-                if ($self->evaluate($z, $results->{$z}->{$n}->{a})) {
+                my @lists = $self->evaluate($z, $results->{$z}->{$n}->{a}) or next;
                     $self->log(LOGDEBUG, "match $n in $z");
                     push @matches, {
-                        action => $self->{uribl_zones}->{$z}->{action},
-                        desc => "$n in $z: " .
+                        zone => $z,
+                        name => $n,
+                        desc => "$n in $z" . 
                             ($results->{$z}->{$n}->{txt} || $results->{$z}->{$n}->{a}),
+                        lists => \@lists,
                     };
                 }
             }
Index: lib/Qpsmtpd/Plugin/Async/DNSBLBase.pm
===================================================================
--- lib/Qpsmtpd/Plugin/Async/DNSBLBase.pm	(revision 12753)
+++ lib/Qpsmtpd/Plugin/Async/DNSBLBase.pm	(working copy)
@@ -8,64 +8,43 @@
 use ParaDNS;
 
 sub lookup {
-    my ($class, $qp, $A_lookups, $TXT_lookups) = @_;
+    my ($class, $qp, $A_lookups, $TXT_lookups, $NS_lookups) = @_;
 
-    my $total_zones = @$A_lookups + @$TXT_lookups;
+    my $total_zones = @$A_lookups + @$TXT_lookups + @$NS_lookups;
 
-    my ($A_pdns, $TXT_pdns);
-
-    if (@$A_lookups) {
+    my $pdns;
+    for my $type ( qw( A TXT NS ) ) {
+        my $lookups = { A   => $A_lookups,
+                        TXT => $TXT_lookups,
+                        NS  => $NS_lookups   }->{$type};
+        next unless @$lookups;
         $qp->log(LOGDEBUG, "Checking ",
-                 join(", ", @$A_lookups),
-                 " for A record in the background");
-
-        $A_pdns = ParaDNS->new(
+                 join(", ", @$lookups),
+                 " for $type record in the background");
+        $pdns->{$type} = ParaDNS->new(
             callback => sub {
                 my ($result, $query) = @_;
-                return if $result !~ /^\d+\.\d+\.\d+\.\d+$/;
-                $qp->log(LOGDEBUG, "Result for A $query: $result");
-                $class->process_a_result($qp, $result, $query);
+                return if $type eq 'A'   and $result !~ /^\d+\.\d+\.\d+\.\d+$/;
+                return if $type eq 'TXT' and $result !~ /[a-z]/;
+                $qp->log(LOGDEBUG, "Result for $type $query: $result");
+                my $sub = lc "process_${type}_result";
+                $class->$sub($qp, $result, $query);
             },
             finished => sub {
-                $total_zones -= @$A_lookups;
+                $total_zones -= @$lookups;
                 $class->finished($qp, $total_zones);
             },
-            hosts  => [@$A_lookups],
-            type   => 'A',
+            hosts  => [@$lookups],
+            type   => $type,
             client => $qp->input_sock,
-                              );
 
-        return unless defined $A_pdns;
+                              );
     }
 
-    if (@$TXT_lookups) {
-        $qp->log(LOGDEBUG, "Checking ",
-                 join(", ", @$TXT_lookups),
-                 " for TXT record in the background");
-
-        $TXT_pdns = ParaDNS->new(
-            callback => sub {
-                my ($result, $query) = @_;
-                return if $result !~ /[a-z]/;
-                $qp->log(LOGDEBUG, "Result for TXT $query: $result");
-                $class->process_txt_result($qp, $result, $query);
-            },
-            finished => sub {
-                $total_zones -= @$TXT_lookups;
-                $class->finished($qp, $total_zones);
-            },
-            hosts  => [@$TXT_lookups],
-            type   => 'TXT',
-            client => $qp->input_sock,
-                                );
-
-        unless (defined $TXT_pdns) {
-            undef $A_pdns;
-            return;
-        }
-    }
-
-    return 1;
+    # Is this really necessary? The URIBL plugin, at least, can use A results even
+    # if TXT fails...
+    undef $pdns->{A} if ! $defined $pdns->{TXT};
+    return grep { defined } values %$pdns ? 1 : 0;
 }
 
 sub finished {
@@ -84,4 +63,8 @@
     my ($class, $qp, $result, $query) = @_;
 }
 
+sub process_ns_result {
+    my ($class, $qp, $result, $query) = @_;
+}
+
 1;
gen_tld_lists (application/octet-stream, 4.4 KB) - not displayed
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.