svn commit: r1936659 - spamassassin/trunk/lib/Mail/SpamAssassin

[email protected] Tue, 28 Jul 2026 06:43:52 -0000
Newsgroups gmane.mail.spam.spamassassin.cvs
Message-ID <178522103297.3392318.11340558621560961230@svn03-he-fi>
Author: gbechis
Date: Tue Jul 28 06:43:52 2026
New Revision: 1936659

Log:
use a queue to speedup CNAME processing

Modified:
   spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm	Tue Jul 28 05:19:04 2026	(r1936658)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/PerMsgStatus.pm	Tue Jul 28 06:43:52 2026	(r1936659)
@@ -56,6 +56,7 @@ use re 'taint';
 use Errno qw(ENOENT);
 use Time::HiRes qw(time);
 use Encode;
+use IO::Select;
 
 use Mail::SpamAssassin::Constants qw(:sa :ip);
 use Mail::SpamAssassin::AsyncLoop;
@@ -2063,6 +2064,11 @@ sub extract_message_metadata {
 
   # allow plugins to add more metadata, read the stuff that's there, etc.
   $self->{main}->call_plugins ("parsed_metadata", { permsgstatus => $self });
+
+  # resolve any CNAME lookups queued by plugins that called
+  # add_uri_detail_list() directly above, in case get_uri_detail_list()
+  # never gets called for this message.
+  $self->_flush_pending_cname_lookups();
 }
 
 ###########################################################################
@@ -2702,6 +2708,10 @@ sub get_uri_detail_list {
   # process dkim uris
   $self->_process_dkim_uri_list();
 
+  # resolve any CNAME lookups queued above (and by any parsed_metadata
+  # plugins that called add_uri_detail_list() directly) concurrently
+  $self->_flush_pending_cname_lookups();
+
   return $self->{uri_detail_list};
 }
 
@@ -2892,45 +2902,22 @@ sub add_uri_detail_list {
     }
 
     my $dns_max_cname_cache = $self->{main}->{conf}->{dns_max_cname_cache};
-    if($self->is_dns_available() and ($dns_max_cname_cache > 0) and (not defined $self->{dns_cname_cache} or scalar %{$self->{dns_cname_cache}} < $dns_max_cname_cache)) {
-      # XXX we cannot call bgsend_and_start_lookup,
-      # otherwise get_uri_detail_list() might not
-      # return domains extracted from CNAME dns queries
-      # in time
-      my $orig_resolver =  $self->{main}->{resolver}->get_resolver();
-      my $pkt;
-      eval {
-        return if not defined $host;
-        return if exists $self->{dns_cname_cache}{$host};
-        my $handle = $orig_resolver->bgsend($host, 'CNAME');
-        $pkt = $orig_resolver->bgread($handle);
-        return if !$pkt; # aborted / timed out
-        my @answ = $pkt->answer;
-        # Set an invalid value in the cache, it will be overwritten later
-        # if a CNAME is present
-        $self->{dns_cname_cache}{$host} = 'invalid';
-        foreach my $ans ( @answ ) {
-          if($ans->can("cname")) {
-            return if not defined $ans->cname;
-            if(not exists $self->{dns_cname_cache}{$host}) {
-              $self->{dns_cname_cache}{$host} = $ans->cname;
-              dbg("dns: found CNAME " . $ans->cname . " for host $host");
-              my $cname_types = { %{$types} };
-              $cname_types->{unlinked} = 1;
-              $cname_types->{noclean} = 1;
-              $self->{uri_cnames}{$ans->cname} = $host;
-              $self->add_uri_detail_list($ans->cname, $cname_types, $source, 1);
-            }
-          }
-        }
-      } or do {
-        undef $pkt;
-        if($@) {
-          my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
-          # resignal if alarm went off
-          die $eval_stat  if $eval_stat =~ /__alarm__ignore__\(.*\)/s;
-          info("dns: bad dns reply: %s", $eval_stat);
-        }
+    if (defined $host and $self->is_dns_available() and ($dns_max_cname_cache > 0)
+        and (not defined $self->{dns_cname_cache} or scalar %{$self->{dns_cname_cache}} < $dns_max_cname_cache)
+        and not exists $self->{dns_cname_cache}{$host})
+    {
+      # Queue the lookup instead of resolving inline (bgsend+bgread) so that
+      # many hosts discovered in the same message can be resolved
+      # concurrently. We still can't use bgsend_and_start_lookup/AsyncLoop
+      # here, since get_uri_detail_list() needs domains extracted from CNAME
+      # dns queries to be available by the time it returns. The queue is
+      # drained by _flush_pending_cname_lookups().
+      #
+      # Set an invalid value in the cache, to be overwritten later
+      # if a CNAME is present.
+      $self->{dns_cname_cache}{$host} = 'invalid';
+      push @{$self->{_pending_cname_lookups}}, {
+        host => $host, types => $types, source => $source,
       };
     }
   }
@@ -2962,6 +2949,96 @@ sub add_uri_detail_list {
   return 1;
 }
 
+=item $status-E<gt>_flush_pending_cname_lookups ()
+
+Resolves all CNAME lookups queued by add_uri_detail_list() concurrently.
+Must be called before anything relies on C<uri_cnames>/C<dns_cname_cache>
+being complete (i.e. before get_uri_detail_list() returns, and 
+after parsed_metadata plugins have run).
+
+=cut
+
+sub _flush_pending_cname_lookups {
+  my ($self) = @_;
+
+  return unless $self->{_pending_cname_lookups} && @{$self->{_pending_cname_lookups}};
+
+  my $orig_resolver = $self->{main}->{resolver}->get_resolver();
+  return unless $orig_resolver;
+
+  my %pending_by_handle;  # handle => { host, types, source }
+  my $sel = IO::Select->new();
+
+  # dispatch every not-yet-sent entry (including ones queued by a CNAME
+  # answer we just processed to follow chains)
+  my $dispatch = sub {
+    while (my $item = shift @{$self->{_pending_cname_lookups}}) {
+      my $handle;
+      eval {
+        $handle = $orig_resolver->bgsend($item->{host}, 'CNAME');
+        1;
+      } or do {
+        my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+        die $eval_stat  if $eval_stat =~ /__alarm__ignore__\(.*\)/s;
+        info("dns: bad dns query for %s: %s", $item->{host}, $eval_stat);
+        undef $handle;
+      };
+      next unless $handle;
+      $pending_by_handle{$handle} = $item;
+      $sel->add($handle);
+    }
+  };
+  $dispatch->();
+
+  while ($sel->count) {
+    my @ready = $sel->can_read(10);  # overall check timeout
+                                     # is still enforced by master_deadline
+    if (!@ready) {
+      # timed out waiting, abandon anything
+      foreach my $handle ($sel->handles) {
+        $sel->remove($handle);
+        delete $pending_by_handle{$handle};
+      }
+      last;
+    }
+    foreach my $handle (@ready) {
+      $sel->remove($handle);
+      my $item = delete $pending_by_handle{$handle};
+      next unless $item;
+      my ($host, $types, $source) = @{$item}{qw(host types source)};
+
+      my $pkt;
+      eval {
+        $pkt = $orig_resolver->bgread($handle);
+        1;
+      } or do {
+        undef $pkt;
+        my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
+        die $eval_stat  if $eval_stat =~ /__alarm__ignore__\(.*\)/s;
+        info("dns: bad dns reply: %s", $eval_stat);
+      };
+      next unless $pkt;  # aborted / timed out
+
+      foreach my $ans ($pkt->answer) {
+        next unless $ans->can("cname");
+        next unless defined $ans->cname;
+        # only take the first CNAME seen for this host
+        next if $self->{dns_cname_cache}{$host} ne 'invalid';
+        $self->{dns_cname_cache}{$host} = $ans->cname;
+        dbg("dns: found CNAME " . $ans->cname . " for host $host");
+        my $cname_types = { %{$types} };
+        $cname_types->{unlinked} = 1;
+        $cname_types->{noclean} = 1;
+        $self->{uri_cnames}{$ans->cname} = $host;
+        # may queue a further hop in a CNAME chain
+        $self->add_uri_detail_list($ans->cname, $cname_types, $source, 1);
+      }
+    }
+    # pick up anything a callback above just queued (chained CNAMEs)
+    $dispatch->();
+  }
+}
+
 ###########################################################################
 
 # Deprecated since 4.0, meta rules do not depend on priorities anymore