svn commit: r1932428 - in spamassassin/trunk: . lib/Mail lib/Mail/SpamAssassin

[email protected]
Newsgroups gmane.mail.spam.spamassassin.cvs
Message-ID <[email protected]>
Author: fkento
Date: Sat Mar 21 04:35:43 2026
New Revision: 1932428

Log:
Add multipart_alternative_preferred_part config option

New setting allows choosing which part of a multipart/alternative
section to use for rendered body text (text/html or text/plain),
skipping the other alternative. Defaults to empty (all parts included,
preserving existing behavior).

Modified:
   spamassassin/trunk/UPGRADE
   spamassassin/trunk/lib/Mail/SpamAssassin.pm
   spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
   spamassassin/trunk/lib/Mail/SpamAssassin/Message.pm

Modified: spamassassin/trunk/UPGRADE
==============================================================================
--- spamassassin/trunk/UPGRADE	Sat Mar 21 04:31:44 2026	(r1932427)
+++ spamassassin/trunk/UPGRADE	Sat Mar 21 04:35:43 2026	(r1932428)
@@ -44,6 +44,12 @@ Note for Users Upgrading to SpamAssassin
   If no AuthRes results are available or the policy is missing, the plugin
   falls back to its own Mail::DMARC::PurePerl-based validation as before.
 
+- New configuration option: multipart_alternative_preferred_part
+  When set to "text/html" or "text/plain", only the preferred part type
+  from multipart/alternative sections will be used for rendered body text,
+  skipping the other alternative. By default this is unset and all parts
+  are included as before.
+
 - New Mail::SpamAssassin::Plugin::NeuralNetwork
   This plugin checks messages using Fast Artificial Neural Network library.
 

Modified: spamassassin/trunk/lib/Mail/SpamAssassin.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin.pm	Sat Mar 21 04:31:44 2026	(r1932427)
+++ spamassassin/trunk/lib/Mail/SpamAssassin.pm	Sat Mar 21 04:35:43 2026	(r1932428)
@@ -621,6 +621,7 @@ sub parse {
     normalize=>$self->{conf}->{normalize_charset},
     body_part_scan_size=>$self->{conf}->{body_part_scan_size},
     rawbody_part_scan_size=>$self->{conf}->{rawbody_part_scan_size},
+    multipart_alternative_preferred_part=>$self->{conf}->{multipart_alternative_preferred_part},
     master_deadline=>$master_deadline, suppl_attrib=>$suppl_attrib });
 
   # bug 5069: The goal here is to get rendering plugins to do things

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm	Sat Mar 21 04:31:44 2026	(r1932427)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm	Sat Mar 21 04:35:43 2026	(r1932428)
@@ -3809,7 +3809,34 @@ Like body_part_scan_size, for "rawbody"
     default => 500000,
     type => $CONF_TYPE_NUMERIC,
   });
-  
+
+=item multipart_alternative_preferred_part (default: empty)
+
+Specifies which part of a multipart/alternative section to prefer for
+rendered body text.  Valid values are C<text/html> and C<text/plain>.
+When set, only the preferred part type will be used from
+multipart/alternative sections, skipping the other alternative.  When
+empty (the default), all parts are included as before.
+
+=cut
+
+  push (@cmds, {
+    setting => 'multipart_alternative_preferred_part',
+    default => '',
+    type => $CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      unless (defined $value && $value !~ /^$/) {
+        return $MISSING_REQUIRED_VALUE;
+      }
+      if ($value eq 'text/html' || $value eq 'text/plain') {
+        $self->{multipart_alternative_preferred_part} = $value;
+      } else {
+        return $INVALID_VALUE;
+      }
+    }
+  });
+
 =item rbl_timeout t [t_min] [zone]		(default: 15 3)
 
 All DNS queries are made at the beginning of a check and we try to read

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Message.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Message.pm	Sat Mar 21 04:31:44 2026	(r1932427)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Message.pm	Sat Mar 21 04:35:43 2026	(r1932428)
@@ -120,6 +120,7 @@ sub new {
   $self->{suppl_attrib} = $opts->{'suppl_attrib'};
   $self->{body_part_scan_size} = $opts->{'body_part_scan_size'} || 0;
   $self->{rawbody_part_scan_size} = $opts->{'rawbody_part_scan_size'} || 0;
+  $self->{multipart_alternative_preferred_part} = $opts->{'multipart_alternative_preferred_part'} || '';
 
   if ($self->{suppl_attrib}) {  # caller-provided additional information
     # pristine_body_length is currently used by an eval test check_body_length
@@ -735,6 +736,7 @@ sub finish {
   delete $self->{'normalize'};
   delete $self->{'body_part_scan_size'};
   delete $self->{'rawbody_part_scan_size'};
+  delete $self->{'multipart_alternative_preferred_part'};
   delete $self->{'pristine_msg'};
   delete $self->{'pristine_body'};
   delete $self->{'pristine_headers'};
@@ -1254,6 +1256,18 @@ sub get_mimepart_digests {
 
 # ---------------------------------------------------------------------------
 
+# Search children/descendants of a node for a leaf part matching $type.
+# Returns the part if found, undef otherwise.
+sub _find_part_by_type {
+  my ($node, $type) = @_;
+  my @q = @{$node->{'body_parts'} || []};
+  while (my $n = shift @q) {
+    return $n if $n->is_leaf() && $n->{'type'} eq $type;
+    push @q, @{$n->{'body_parts'}} if !$n->is_leaf();
+  }
+  return undef;
+}
+
 # common code for get_rendered_body_text_array,
 # get_visible_rendered_body_text_array, get_invisible_rendered_body_text_array
 #
@@ -1266,10 +1280,7 @@ sub get_body_text_array_common {
   $self->{$key} = [];
 
   my $scansize = $self->{body_part_scan_size};
-
-  # Find all parts which are leaves
-  my @parts = $self->find_parts(qr/./,1);
-  return $self->{$key} unless @parts;
+  my $preferred_alt = $self->{multipart_alternative_preferred_part};
 
   # the html metadata may have already been set, so let's not bother if it's
   # already been done.
@@ -1278,13 +1289,25 @@ sub get_body_text_array_common {
   my $subject = $method_name eq 'invisible_rendered' ? ''
                : ($self->get_header('subject') || "\n");
 
-  # Go through each part
+  # Walk the MIME tree using a queue.  For multipart/alternative nodes,
+  # if a preferred part type is set and found, enqueue only that part
+  # instead of all children (skipping the other alternative).
+  my @queue = ($self);
   my $text = '';
-  for (my $pt = 0 ; $pt <= $#parts ; $pt++ ) {
-    my $p = $parts[$pt];
-
-    # put a blank line between parts ...
-    $text .= "\n"  if $text ne '';
+  while (my $p = shift @queue) {
+    if (!$p->is_leaf()) {
+      if ($preferred_alt && $p->{'type'} eq 'multipart/alternative') {
+        my $preferred_part = _find_part_by_type($p, $preferred_alt);
+        if ($preferred_part) {
+          unshift @queue, $preferred_part;
+        } else {
+          unshift @queue, @{$p->{'body_parts'}};
+        }
+      } else {
+        unshift @queue, @{$p->{'body_parts'}};
+      }
+      next;
+    }
 
     my($type, $rnd) = $p->$method_name();  # decode this part
     # Only text/* types are rendered ...
@@ -1315,6 +1338,9 @@ sub get_body_text_array_common {
         }
       }
 
+      # put a blank line between parts ...
+      $text .= "\n"  if $text ne '';
+
       # Add to rendered text
       $text .= $rnd;
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.