* 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;
}
# 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.
# Returns '' if no clear domain found.
sub extract_domain {
@@ -108,7 +116,7 @@
if ($pmGlobRules > 0)
{
$OUT .= "\n";
$OUT .= "# --- start of Global Sieve rules (".$pmGlobRules.")---------\n";
$OUT .= "# --- start of Global Sieve rules ($pmGlobRules)---------\n";
my $pmGlobRule;
foreach $pmGlobRule (sort {$a <=> $b} @pmGlobRules)
@@ -165,24 +173,38 @@
my $hv_simple = simplify_email_value($hv);
my $hv_domain = extract_domain($hv_simple);
if ($all_addr && $hv_domain ne '') {
if ($all_addr) {
my @lb = map { lc $_ } @hn;
my $hn_list = join '","', @lb;
my $dom_q = sieve_quote($hv_domain);
$cond1 = "address :domain :is [\"$hn_list\"] \"$dom_q\"";
if (is_full_email($hv_simple)) {
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 {
# 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_list = join '","', @hn_q;
my $hn_list_q = join '","', @hn_q;
my $hv_re = sieve_regex_passthrough($hv);
$cond1 = "header :regex [\"$hn_list\"] \"$hv_re\"";
$cond1 = "header :regex [\"$hn_list_q\"] \"$hv_re\"";
}
}
else {
# No valid header names extracted: prefer address match only if email/domain-like
my $hv_simple = simplify_email_value($h);
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);
$cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
} else {
@@ -196,7 +218,10 @@
# No "Header: value" structure: prefer address match only if email/domain-like
my $hv_simple = simplify_email_value($h);
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);
$cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
} else {
@@ -258,23 +283,36 @@
my $hv2_simple = simplify_email_value($hv2);
my $hv2_domain = extract_domain($hv2_simple);
if ($all_addr2 && $hv2_domain ne '') {
if ($all_addr2) {
my @lb2 = map { lc $_ } @hn2;
my $hn2_list = join '","', @lb2;
my $dq2 = sieve_quote($hv2_domain);
$cond2 = "address :domain :is [\"$hn2_list\"] \"$dq2\"";
if (is_full_email($hv2_simple)) {
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 {
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);
$cond2 = "header :regex [\"$hn2_list\"] \"$hv2_re\"";
$cond2 = "header :regex [\"$hn2_list_q\"] \"$hv2_re\"";
}
}
else {
# No valid header names: prefer address match only if email/domain-like
my $hv2_simple = simplify_email_value($hh);
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);
$cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
} else {
@@ -288,7 +326,10 @@
# No "Header: value" structure: prefer address match only if email/domain-like
my $hv2_simple = simplify_email_value($hh);
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);
$cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
} else {
@@ -429,7 +470,6 @@
}
$OUT .= " stop;\n";
}
$OUT .= "\}\n";
$OUT .= "# End of Global rule $pmGlobRule\n";
}#foreach rule

View File

@@ -56,6 +56,14 @@
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.
# Returns '' if no clear domain found.
sub extract_domain {
@@ -165,24 +173,38 @@
my $hv_simple = simplify_email_value($hv);
my $hv_domain = extract_domain($hv_simple);
if ($all_addr && $hv_domain ne '') {
if ($all_addr) {
my @lb = map { lc $_ } @hn;
my $hn_list = join '","', @lb;
my $dom_q = sieve_quote($hv_domain);
$cond1 = "address :domain :is [\"$hn_list\"] \"$dom_q\"";
if (is_full_email($hv_simple)) {
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 {
# 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_list = join '","', @hn_q;
my $hn_list_q = join '","', @hn_q;
my $hv_re = sieve_regex_passthrough($hv);
$cond1 = "header :regex [\"$hn_list\"] \"$hv_re\"";
$cond1 = "header :regex [\"$hn_list_q\"] \"$hv_re\"";
}
}
else {
# No valid header names extracted: prefer address match only if email/domain-like
my $hv_simple = simplify_email_value($h);
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);
$cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
} else {
@@ -196,7 +218,10 @@
# No "Header: value" structure: prefer address match only if email/domain-like
my $hv_simple = simplify_email_value($h);
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);
$cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
} else {
@@ -258,23 +283,36 @@
my $hv2_simple = simplify_email_value($hv2);
my $hv2_domain = extract_domain($hv2_simple);
if ($all_addr2 && $hv2_domain ne '') {
if ($all_addr2) {
my @lb2 = map { lc $_ } @hn2;
my $hn2_list = join '","', @lb2;
my $dq2 = sieve_quote($hv2_domain);
$cond2 = "address :domain :is [\"$hn2_list\"] \"$dq2\"";
if (is_full_email($hv2_simple)) {
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 {
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);
$cond2 = "header :regex [\"$hn2_list\"] \"$hv2_re\"";
$cond2 = "header :regex [\"$hn2_list_q\"] \"$hv2_re\"";
}
}
else {
# No valid header names: prefer address match only if email/domain-like
my $hv2_simple = simplify_email_value($hh);
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);
$cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
} else {
@@ -288,7 +326,10 @@
# No "Header: value" structure: prefer address match only if email/domain-like
my $hv2_simple = simplify_email_value($hh);
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);
$cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
} else {

View File

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

View File

@@ -6,7 +6,7 @@ Summary: Lets users configure procmail or maildrop rules.
%define name smeserver-mailsorting
Name: %{name}
%define version 11.0.0
%define release 6
%define release 7
Version: %{version}
Release: %{release}%{?dist}
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
%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
- Make coding for global rules for mailfilters and sieve the same as for user rules [SME: 13245]