* Tue Oct 21 2025 Brian Read <brianr@koozali.org> 11.0.0-7.sme

- Typo in REMOVE partial template [SME: 13251]
- Update to sievefilter generation for Headers - use whole email address if provided [SME: 13251]
This commit is contained in:
2025-10-22 20:23:06 +01:00
parent 3f4de56381
commit f5b30e511d
4 changed files with 120 additions and 35 deletions

View File

@@ -56,6 +56,14 @@
return $s; return $s;
} }
# Detect a full email address (simple, robust)
sub is_full_email {
my ($s) = @_;
$s //= '';
$s =~ s/^\s+|\s+$//g;
return ($s =~ /^[^@\s<>"',;]+@[^@\s<>"',;]+$/) ? 1 : 0;
}
# Extract a domain from a simplified email-like string. # Extract a domain from a simplified email-like string.
# Returns '' if no clear domain found. # Returns '' if no clear domain found.
sub extract_domain { sub extract_domain {
@@ -108,7 +116,7 @@
if ($pmGlobRules > 0) if ($pmGlobRules > 0)
{ {
$OUT .= "\n"; $OUT .= "\n";
$OUT .= "# --- start of Global Sieve rules (".$pmGlobRules.")---------\n"; $OUT .= "# --- start of Global Sieve rules ($pmGlobRules)---------\n";
my $pmGlobRule; my $pmGlobRule;
foreach $pmGlobRule (sort {$a <=> $b} @pmGlobRules) foreach $pmGlobRule (sort {$a <=> $b} @pmGlobRules)
@@ -165,24 +173,38 @@
my $hv_simple = simplify_email_value($hv); my $hv_simple = simplify_email_value($hv);
my $hv_domain = extract_domain($hv_simple); my $hv_domain = extract_domain($hv_simple);
if ($all_addr && $hv_domain ne '') { if ($all_addr) {
my @lb = map { lc $_ } @hn; my @lb = map { lc $_ } @hn;
my $hn_list = join '","', @lb; my $hn_list = join '","', @lb;
my $dom_q = sieve_quote($hv_domain); if (is_full_email($hv_simple)) {
$cond1 = "address :domain :is [\"$hn_list\"] \"$dom_q\""; my $addr_q = sieve_quote($hv_simple);
$cond1 = "address :is [\"$hn_list\"] \"$addr_q\"";
} elsif ($hv_domain ne '') {
my $dom_q = sieve_quote($hv_domain);
$cond1 = "address :domain :is [\"$hn_list\"] \"$dom_q\"";
} else {
# Fall back to true regex against listed headers
my @hn_q = map { sieve_quote($_) } @hn;
my $hn_list_q = join '","', @hn_q;
my $hv_re = sieve_regex_passthrough($hv);
$cond1 = "header :regex [\"$hn_list_q\"] \"$hv_re\"";
}
} else { } else {
# Fall back to true regex against listed headers # Mixed/non-address headers: regex against the listed headers
my @hn_q = map { sieve_quote($_) } @hn; my @hn_q = map { sieve_quote($_) } @hn;
my $hn_list = join '","', @hn_q; my $hn_list_q = join '","', @hn_q;
my $hv_re = sieve_regex_passthrough($hv); my $hv_re = sieve_regex_passthrough($hv);
$cond1 = "header :regex [\"$hn_list\"] \"$hv_re\""; $cond1 = "header :regex [\"$hn_list_q\"] \"$hv_re\"";
} }
} }
else { else {
# No valid header names extracted: prefer address match only if email/domain-like # No valid header names extracted: prefer address match only if email/domain-like
my $hv_simple = simplify_email_value($h); my $hv_simple = simplify_email_value($h);
my $hv_domain = extract_domain($hv_simple); my $hv_domain = extract_domain($hv_simple);
if ($hv_domain ne '') { if (is_full_email($hv_simple)) {
my $aq = sieve_quote($hv_simple);
$cond1 = "address :is [\"from\",\"to\",\"cc\"] \"$aq\"";
} elsif ($hv_domain ne '') {
my $dq = sieve_quote($hv_domain); my $dq = sieve_quote($hv_domain);
$cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\""; $cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
} else { } else {
@@ -196,7 +218,10 @@
# No "Header: value" structure: prefer address match only if email/domain-like # No "Header: value" structure: prefer address match only if email/domain-like
my $hv_simple = simplify_email_value($h); my $hv_simple = simplify_email_value($h);
my $hv_domain = extract_domain($hv_simple); my $hv_domain = extract_domain($hv_simple);
if ($hv_domain ne '') { if (is_full_email($hv_simple)) {
my $aq = sieve_quote($hv_simple);
$cond1 = "address :is [\"from\",\"to\",\"cc\"] \"$aq\"";
} elsif ($hv_domain ne '') {
my $dq = sieve_quote($hv_domain); my $dq = sieve_quote($hv_domain);
$cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\""; $cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
} else { } else {
@@ -258,23 +283,36 @@
my $hv2_simple = simplify_email_value($hv2); my $hv2_simple = simplify_email_value($hv2);
my $hv2_domain = extract_domain($hv2_simple); my $hv2_domain = extract_domain($hv2_simple);
if ($all_addr2 && $hv2_domain ne '') { if ($all_addr2) {
my @lb2 = map { lc $_ } @hn2; my @lb2 = map { lc $_ } @hn2;
my $hn2_list = join '","', @lb2; my $hn2_list = join '","', @lb2;
my $dq2 = sieve_quote($hv2_domain); if (is_full_email($hv2_simple)) {
$cond2 = "address :domain :is [\"$hn2_list\"] \"$dq2\""; my $aq2 = sieve_quote($hv2_simple);
$cond2 = "address :is [\"$hn2_list\"] \"$aq2\"";
} elsif ($hv2_domain ne '') {
my $dq2 = sieve_quote($hv2_domain);
$cond2 = "address :domain :is [\"$hn2_list\"] \"$dq2\"";
} else {
my @hn2_q = map { sieve_quote($_) } @hn2;
my $hn2_list_q = join '","', @hn2_q;
my $hv2_re = sieve_regex_passthrough($hv2);
$cond2 = "header :regex [\"$hn2_list_q\"] \"$hv2_re\"";
}
} else { } else {
my @hn2_q = map { sieve_quote($_) } @hn2; my @hn2_q = map { sieve_quote($_) } @hn2;
my $hn2_list = join '","', @hn2_q; my $hn2_list_q = join '","', @hn2_q;
my $hv2_re = sieve_regex_passthrough($hv2); my $hv2_re = sieve_regex_passthrough($hv2);
$cond2 = "header :regex [\"$hn2_list\"] \"$hv2_re\""; $cond2 = "header :regex [\"$hn2_list_q\"] \"$hv2_re\"";
} }
} }
else { else {
# No valid header names: prefer address match only if email/domain-like # No valid header names: prefer address match only if email/domain-like
my $hv2_simple = simplify_email_value($hh); my $hv2_simple = simplify_email_value($hh);
my $hv2_domain = extract_domain($hv2_simple); my $hv2_domain = extract_domain($hv2_simple);
if ($hv2_domain ne '') { if (is_full_email($hv2_simple)) {
my $aq2 = sieve_quote($hv2_simple);
$cond2 = "address :is [\"from\",\"to\",\"cc\"] \"$aq2\"";
} elsif ($hv2_domain ne '') {
my $dq2 = sieve_quote($hv2_domain); my $dq2 = sieve_quote($hv2_domain);
$cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\""; $cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
} else { } else {
@@ -288,7 +326,10 @@
# No "Header: value" structure: prefer address match only if email/domain-like # No "Header: value" structure: prefer address match only if email/domain-like
my $hv2_simple = simplify_email_value($hh); my $hv2_simple = simplify_email_value($hh);
my $hv2_domain = extract_domain($hv2_simple); my $hv2_domain = extract_domain($hv2_simple);
if ($hv2_domain ne '') { if (is_full_email($hv2_simple)) {
my $aq2 = sieve_quote($hv2_simple);
$cond2 = "address :is [\"from\",\"to\",\"cc\"] \"$aq2\"";
} elsif ($hv2_domain ne '') {
my $dq2 = sieve_quote($hv2_domain); my $dq2 = sieve_quote($hv2_domain);
$cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\""; $cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
} else { } else {
@@ -429,9 +470,8 @@
} }
$OUT .= " stop;\n"; $OUT .= " stop;\n";
} }
$OUT .= "\}\n"; $OUT .= "\}\n";
$OUT .= "# End of Global rule $pmGlobRule\n"; $OUT .= "# End of Global rule $pmGlobRule\n";
}#foreach rule }#foreach rule
}#if rules exist }#if rules exist
} }

View File

@@ -56,6 +56,14 @@
return $s; return $s;
} }
# Detect a full email address (simple, robust)
sub is_full_email {
my ($s) = @_;
$s //= '';
$s =~ s/^\s+|\s+$//g;
return ($s =~ /^[^@\s<>"',;]+@[^@\s<>"',;]+$/) ? 1 : 0;
}
# Extract a domain from a simplified email-like string. # Extract a domain from a simplified email-like string.
# Returns '' if no clear domain found. # Returns '' if no clear domain found.
sub extract_domain { sub extract_domain {
@@ -165,24 +173,38 @@
my $hv_simple = simplify_email_value($hv); my $hv_simple = simplify_email_value($hv);
my $hv_domain = extract_domain($hv_simple); my $hv_domain = extract_domain($hv_simple);
if ($all_addr && $hv_domain ne '') { if ($all_addr) {
my @lb = map { lc $_ } @hn; my @lb = map { lc $_ } @hn;
my $hn_list = join '","', @lb; my $hn_list = join '","', @lb;
my $dom_q = sieve_quote($hv_domain); if (is_full_email($hv_simple)) {
$cond1 = "address :domain :is [\"$hn_list\"] \"$dom_q\""; my $addr_q = sieve_quote($hv_simple);
$cond1 = "address :is [\"$hn_list\"] \"$addr_q\"";
} elsif ($hv_domain ne '') {
my $dom_q = sieve_quote($hv_domain);
$cond1 = "address :domain :is [\"$hn_list\"] \"$dom_q\"";
} else {
# Fall back to true regex against listed headers
my @hn_q = map { sieve_quote($_) } @hn;
my $hn_list_q = join '","', @hn_q;
my $hv_re = sieve_regex_passthrough($hv);
$cond1 = "header :regex [\"$hn_list_q\"] \"$hv_re\"";
}
} else { } else {
# Fall back to true regex against listed headers # Mixed/non-address headers: regex against the listed headers
my @hn_q = map { sieve_quote($_) } @hn; my @hn_q = map { sieve_quote($_) } @hn;
my $hn_list = join '","', @hn_q; my $hn_list_q = join '","', @hn_q;
my $hv_re = sieve_regex_passthrough($hv); my $hv_re = sieve_regex_passthrough($hv);
$cond1 = "header :regex [\"$hn_list\"] \"$hv_re\""; $cond1 = "header :regex [\"$hn_list_q\"] \"$hv_re\"";
} }
} }
else { else {
# No valid header names extracted: prefer address match only if email/domain-like # No valid header names extracted: prefer address match only if email/domain-like
my $hv_simple = simplify_email_value($h); my $hv_simple = simplify_email_value($h);
my $hv_domain = extract_domain($hv_simple); my $hv_domain = extract_domain($hv_simple);
if ($hv_domain ne '') { if (is_full_email($hv_simple)) {
my $aq = sieve_quote($hv_simple);
$cond1 = "address :is [\"from\",\"to\",\"cc\"] \"$aq\"";
} elsif ($hv_domain ne '') {
my $dq = sieve_quote($hv_domain); my $dq = sieve_quote($hv_domain);
$cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\""; $cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
} else { } else {
@@ -196,7 +218,10 @@
# No "Header: value" structure: prefer address match only if email/domain-like # No "Header: value" structure: prefer address match only if email/domain-like
my $hv_simple = simplify_email_value($h); my $hv_simple = simplify_email_value($h);
my $hv_domain = extract_domain($hv_simple); my $hv_domain = extract_domain($hv_simple);
if ($hv_domain ne '') { if (is_full_email($hv_simple)) {
my $aq = sieve_quote($hv_simple);
$cond1 = "address :is [\"from\",\"to\",\"cc\"] \"$aq\"";
} elsif ($hv_domain ne '') {
my $dq = sieve_quote($hv_domain); my $dq = sieve_quote($hv_domain);
$cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\""; $cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
} else { } else {
@@ -258,23 +283,36 @@
my $hv2_simple = simplify_email_value($hv2); my $hv2_simple = simplify_email_value($hv2);
my $hv2_domain = extract_domain($hv2_simple); my $hv2_domain = extract_domain($hv2_simple);
if ($all_addr2 && $hv2_domain ne '') { if ($all_addr2) {
my @lb2 = map { lc $_ } @hn2; my @lb2 = map { lc $_ } @hn2;
my $hn2_list = join '","', @lb2; my $hn2_list = join '","', @lb2;
my $dq2 = sieve_quote($hv2_domain); if (is_full_email($hv2_simple)) {
$cond2 = "address :domain :is [\"$hn2_list\"] \"$dq2\""; my $aq2 = sieve_quote($hv2_simple);
$cond2 = "address :is [\"$hn2_list\"] \"$aq2\"";
} elsif ($hv2_domain ne '') {
my $dq2 = sieve_quote($hv2_domain);
$cond2 = "address :domain :is [\"$hn2_list\"] \"$dq2\"";
} else {
my @hn2_q = map { sieve_quote($_) } @hn2;
my $hn2_list_q = join '","', @hn2_q;
my $hv2_re = sieve_regex_passthrough($hv2);
$cond2 = "header :regex [\"$hn2_list_q\"] \"$hv2_re\"";
}
} else { } else {
my @hn2_q = map { sieve_quote($_) } @hn2; my @hn2_q = map { sieve_quote($_) } @hn2;
my $hn2_list = join '","', @hn2_q; my $hn2_list_q = join '","', @hn2_q;
my $hv2_re = sieve_regex_passthrough($hv2); my $hv2_re = sieve_regex_passthrough($hv2);
$cond2 = "header :regex [\"$hn2_list\"] \"$hv2_re\""; $cond2 = "header :regex [\"$hn2_list_q\"] \"$hv2_re\"";
} }
} }
else { else {
# No valid header names: prefer address match only if email/domain-like # No valid header names: prefer address match only if email/domain-like
my $hv2_simple = simplify_email_value($hh); my $hv2_simple = simplify_email_value($hh);
my $hv2_domain = extract_domain($hv2_simple); my $hv2_domain = extract_domain($hv2_simple);
if ($hv2_domain ne '') { if (is_full_email($hv2_simple)) {
my $aq2 = sieve_quote($hv2_simple);
$cond2 = "address :is [\"from\",\"to\",\"cc\"] \"$aq2\"";
} elsif ($hv2_domain ne '') {
my $dq2 = sieve_quote($hv2_domain); my $dq2 = sieve_quote($hv2_domain);
$cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\""; $cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
} else { } else {
@@ -288,7 +326,10 @@
# No "Header: value" structure: prefer address match only if email/domain-like # No "Header: value" structure: prefer address match only if email/domain-like
my $hv2_simple = simplify_email_value($hh); my $hv2_simple = simplify_email_value($hh);
my $hv2_domain = extract_domain($hv2_simple); my $hv2_domain = extract_domain($hv2_simple);
if ($hv2_domain ne '') { if (is_full_email($hv2_simple)) {
my $aq2 = sieve_quote($hv2_simple);
$cond2 = "address :is [\"from\",\"to\",\"cc\"] \"$aq2\"";
} elsif ($hv2_domain ne '') {
my $dq2 = sieve_quote($hv2_domain); my $dq2 = sieve_quote($hv2_domain);
$cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\""; $cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
} else { } else {

View File

@@ -14,7 +14,7 @@
%= hidden_field 'trt' => $ms_data->{trt} %= hidden_field 'trt' => $ms_data->{trt}
%# die("here"); %# die("here");
%= hidden_field 'Selected' => $c->param('Selected') %= hidden_field 'Selected' => $c->param('Selected')
%= hidden_field ''account'' => $c->param('account') %= hidden_field 'account' => $c->param('account')
%# Inputs etc in here. %# Inputs etc in here.

View File

@@ -6,7 +6,7 @@ Summary: Lets users configure procmail or maildrop rules.
%define name smeserver-mailsorting %define name smeserver-mailsorting
Name: %{name} Name: %{name}
%define version 11.0.0 %define version 11.0.0
%define release 6 %define release 7
Version: %{version} Version: %{version}
Release: %{release}%{?dist} Release: %{release}%{?dist}
License: GPL License: GPL
@@ -32,6 +32,10 @@ SME Server enhancement to enable procmail or maildrop filtering for users.
Optionally provides user panels where users can create mail rules for themselves Optionally provides user panels where users can create mail rules for themselves
%changelog %changelog
* Tue Oct 21 2025 Brian Read <brianr@koozali.org> 11.0.0-7.sme
- Typo in REMOVE partial template [SME: 13251]
- Update to sievefilter generation for Headers - use whole email address if provided [SME: 13251]
* Tue Oct 21 2025 Brian Read <brianr@koozali.org> 11.0.0-6.sme * Tue Oct 21 2025 Brian Read <brianr@koozali.org> 11.0.0-6.sme
- Make coding for global rules for mailfilters and sieve the same as for user rules [SME: 13245] - Make coding for global rules for mailfilters and sieve the same as for user rules [SME: 13245]