svn commit: r1933226 - in spamassassin/trunk: lib/Mail/SpamAssassin lib/Mail/SpamAssassin/Conf lib/Mail/SpamAssassin/Plugin t
[email protected] Tue, 21 Apr 2026 21:02:57 -0000
| Newsgroups | gmane.mail.spam.spamassassin.cvs |
|---|---|
| Message-ID | <177680537760.1294686.8796666470046448897@svn03-he-fi> |
Author: gbechis
Date: Tue Apr 21 21:02:57 2026
New Revision: 1933226
Log:
Substitute %{TAGNAME} at each message check, fixes capture tags in spamd(8)
bz #8388
Added:
spamassassin/trunk/t/regexp_named_capture_spamd.t
Modified:
spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm
spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Check.pm
Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm Tue Apr 21 20:13:12 2026 (r1933225)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm Tue Apr 21 21:02:57 2026 (r1933226)
@@ -5129,6 +5129,7 @@ sub new {
# regex capture template rules
$self->{capture_rules} = {};
$self->{capture_template_rules} = {};
+ $self->{capture_template_strings} = {};
# testing stuff
$self->{regression_tests} = { };
Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm Tue Apr 21 20:13:12 2026 (r1933225)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Conf/Parser.pm Tue Apr 21 21:02:57 2026 (r1933226)
@@ -1526,11 +1526,15 @@ sub parse_captures {
# Check for named regex capture templates
if (index($$re, '%{') >= 0) {
local($1);
+ my $template = $$re; # save original before %{FOO} -> %\{FOO\} escaping
# Replace %{FOO} with %\{FOO\} so compile_regexp doesn't fail with unescaped left brace
while ($$re =~ s/(?<!\\)\%\{([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*(?:\([^\)\}]*\))?)\}/%\\{$1\\}/g) {
dbg("config: found named capture for rule $name: $1");
$self->{conf}->{capture_template_rules}->{$name}->{$1} = 1;
}
+ if (exists $self->{conf}->{capture_template_rules}->{$name}) {
+ $self->{conf}->{capture_template_strings}->{$name} = $template;
+ }
}
# Make rules with captures run before anything else
if ($$re =~ /\(\?P?[<'][A-Z]/) {
Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Check.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Check.pm Tue Apr 21 20:13:12 2026 (r1933225)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/Check.pm Tue Apr 21 21:02:57 2026 (r1933226)
@@ -22,7 +22,7 @@ use Time::HiRes qw(time);
use Mail::SpamAssassin::Plugin;
use Mail::SpamAssassin::Logger;
-use Mail::SpamAssassin::Util qw(untaint_var);
+use Mail::SpamAssassin::Util qw(untaint_var compile_regexp);
use Mail::SpamAssassin::Timeout;
use Mail::SpamAssassin::Constants qw(:sa);
@@ -699,7 +699,7 @@ sub do_head_tests {
$whlast = 'last if ++$hits >= '.untaint_var($1).';';
}
}
- $expr = '$hval '.$op.' /$test_qr/'.$matchg.'op';
+ $expr = '$hval '.$op.' /$test_qr/'.$matchg.'p';
}
# Make sure rule is marked ready for meta rules
@@ -774,7 +774,7 @@ sub do_body_tests {
$sub .= '
pos $l = 0;
'.$self->hash_line_for_rule($pms, $rulename).'
- while ($l =~ /$test_qr/gop) {
+ while ($l =~ /$test_qr/gp) {
'.$self->capture_plugin_code().'
$self->got_hit(q{'.$rulename.'}, "BODY: ", ruletype => "body");
'. $self->hit_rule_plugin_code($pms, $rulename, "body", "") . '
@@ -796,7 +796,7 @@ sub do_body_tests {
}
$sub .= '
'.$self->hash_line_for_rule($pms, $rulename).'
- if ($l =~ /$test_qr/op) {
+ if ($l =~ /$test_qr/p) {
'.$self->capture_plugin_code().'
$self->got_hit(q{'.$rulename.'}, "BODY: ", ruletype => "body");
'. $self->hit_rule_plugin_code($pms, $rulename, "body", "last") .'
@@ -852,7 +852,7 @@ sub do_uri_tests {
uri_'.$loopid.': foreach my $l (@_) {
pos $l = 0;
'.$self->hash_line_for_rule($pms, $rulename).'
- while ($l =~ /$test_qr/gop) {
+ while ($l =~ /$test_qr/gp) {
'.$self->capture_plugin_code().'
$self->got_hit(q{'.$rulename.'}, "URI: ", ruletype => "uri");
'. $self->hit_rule_plugin_code($pms, $rulename, "uri", "") . '
@@ -864,7 +864,7 @@ sub do_uri_tests {
$sub .= '
foreach my $l (@_) {
'.$self->hash_line_for_rule($pms, $rulename).'
- if ($l =~ /$test_qr/op) {
+ if ($l =~ /$test_qr/p) {
'.$self->capture_plugin_code().'
$self->got_hit(q{'.$rulename.'}, "URI: ", ruletype => "uri");
'. $self->hit_rule_plugin_code($pms, $rulename, "uri", "last") .'
@@ -918,7 +918,7 @@ sub do_rawbody_tests {
rawbody_'.$loopid.': foreach my $l (@_) {
pos $l = 0;
'.$self->hash_line_for_rule($pms, $rulename).'
- while ($l =~ /$test_qr/gop) {
+ while ($l =~ /$test_qr/gp) {
'.$self->capture_plugin_code().'
$self->got_hit(q{'.$rulename.'}, "RAW: ", ruletype => "rawbody");
'. $self->hit_rule_plugin_code($pms, $rulename, "rawbody", "") . '
@@ -931,7 +931,7 @@ sub do_rawbody_tests {
$sub .= '
foreach my $l (@_) {
'.$self->hash_line_for_rule($pms, $rulename).'
- if ($l =~ /$test_qr/op) {
+ if ($l =~ /$test_qr/p) {
'.$self->capture_plugin_code().'
$self->got_hit(q{'.$rulename.'}, "RAW: ", ruletype => "rawbody");
'. $self->hit_rule_plugin_code($pms, $rulename, "rawbody", "last") . '
@@ -1376,37 +1376,42 @@ sub capture_rules_replace {
return '{' unless exists $conf->{capture_template_rules}->{$rulename};
- # Replace all named capture templates in regex, format %{CAPTURE_NAME}
- # Note that backquotes must be double escaped in $test_qr
+ # Per-message: substitute %{CAPTURE_NAME} placeholders in the original
+ # regex template string (with unescaped braces), then compile it.
my $code = '
- foreach my $cname (keys %{$self->{conf}->{capture_template_rules}->{q{'.$rulename.'}}}) {
- my $valref = $self->get_tag_raw($cname);
- my @vals = grep { defined $_ && $_ ne "" } (ref $valref ? @$valref : $valref);
- if (@vals) {
- my $cval = "(?:".join("|", map { quotemeta($_) } @vals).")";
- $test_qr =~ s/(?<!\\\\)\\%\\\\\\{\Q${cname}\E\\\\\\}/$cval/gs;
+ {
+ my $tmpl = $self->{conf}->{capture_template_strings}->{q{'.$rulename.'}};
+ foreach my $cname (keys %{$self->{conf}->{capture_template_rules}->{q{'.$rulename.'}}}) {
+ my $valref = $self->get_tag_raw($cname);
+ my @vals = grep { defined $_ && $_ ne "" } (ref $valref ? @$valref : $valref);
+ if (@vals) {
+ my $cval = "(?:".join("|", map { quotemeta($_) } @vals).")";
+ $tmpl =~ s/(?<!\\\\)%\\{\\Q$cname\\E\\}/$cval/g;
';
if ($would_log_rules_all) {
$code .= '
- dbg("rules-all: replaced regex capture template: %s, %s, %s",
- q{'.$rulename.'}, $cname, $test_qr);
+ dbg("rules-all: replaced regex capture template: %s, %s, %s",
+ q{'.$rulename.'}, $cname, $tmpl);
';
}
# bz 8360, instead of disabling the entire rule, change the part of the regexp that doesn't match to
# a different regexp that will never match (without capture tags)
$code .= '
- } else {
- my $cval = "(?!)";
- $test_qr =~ s/(?<!\\\\)\\%\\\\\\{\Q${cname}\E\\\\\\}/$cval/gs;
+ } else {
+ my $cval = "(?!)";
+ $tmpl =~ s/(?<!\\\\)%\\{\\Q$cname\\E\\}/$cval/g;
';
if ($would_log_rules_all) {
$code .= '
- dbg("rules-all: dependent tag not defined on rule %s, using empty alternation: %s",
- q{'.$rulename.'}, $cname);
+ dbg("rules-all: dependent tag not defined on rule %s, using empty alternation: %s",
+ q{'.$rulename.'}, $cname);
';
}
$code .= '
+ }
}
+ my ($compiled_re) = Mail::SpamAssassin::Util::compile_regexp($tmpl, 1);
+ $test_qr = $compiled_re;
}
if ($test_qr) {
';
Added: spamassassin/trunk/t/regexp_named_capture_spamd.t
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++ spamassassin/trunk/t/regexp_named_capture_spamd.t Tue Apr 21 21:02:57 2026 (r1933226)
@@ -0,0 +1,37 @@
+#!/usr/bin/perl -T
+
+use lib '.'; use lib 't';
+use SATest; sa_t_init("regexp_named_capture_spamd");
+
+use Test::More;
+plan skip_all => "Spamd tests disabled" if $SKIP_SPAMD_TESTS;
+plan tests => 3;
+
+# bz #8388
+# Regression test for the CAPTURING TAGS feature in spamd mode.
+#
+
+tstlocalrules(q{
+ score TEST_SPAMD_CAP_CAPTURE 1.0
+ score TEST_SPAMD_CAP_TEMPLATE 1.0
+ score TEST_SPAMD_CAP_UNDEF 1.0
+
+ # Sets tag TESTCAP_SPAMD; runs at priority -10000
+ body TEST_SPAMD_CAP_CAPTURE /release of (?<TESTCAP_SPAMD>\w+)/
+
+ # Uses %{TESTCAP_SPAMD}: must match after warm-up via spamd
+ body TEST_SPAMD_CAP_TEMPLATE m,www\.%{TESTCAP_SPAMD}\.,i
+
+ # Undefined tag in alternation: the (?!) branch never matches but
+ # the plain-text alternative 'Evolution' still should
+ body TEST_SPAMD_CAP_UNDEF /(?:%{TESTCAP_UNDEF_X}|Evolution)/
+});
+
+%patterns = (
+ q{TEST_SPAMD_CAP_CAPTURE}, 'capture_producing_rule',
+ q{TEST_SPAMD_CAP_TEMPLATE}, 'capture_template_rule',
+ q{TEST_SPAMD_CAP_UNDEF}, 'capture_undef_alternation',
+);
+
+sdrun("-L", "< data/nice/001", \&patterns_run_cb);
+ok_all_patterns();