* Sat Oct 11 2025 Brian Read <brianr@koozali.org> 11.0.0-5.sme

- Add Dovecot Sieve generation and application [SME: 13232]
This commit is contained in:
2025-10-20 16:31:41 +01:00
parent 1f26e43d09
commit 61ec02df1f
25 changed files with 1228 additions and 188 deletions

View File

@@ -63,7 +63,7 @@ foreach my $userName (@users)
next;
}
for my $dotfile ( qw(.procmailrc .mailfilter) )
for my $dotfile ( qw(.procmailrc .mailfilter .sievefilter) )
{
my $pathtohome = ($userName eq 'admin')? "/home/e-smith":"/home/e-smith/files/users/$userName";
@@ -87,4 +87,4 @@ foreach my $userName (@users)
};
}
exit (0);
exit (0);

View File

@@ -2,45 +2,48 @@
use esmith::config;
use esmith::db;
use constant REGEX_DELIM => '/';
sub quote_regex {
my ($pattern) = @_;
my $delim = REGEX_DELIM;
# Escape delimiter characters inside pattern
$pattern =~ s/\Q$delim\E/\\$delim/g;
return $delim . $pattern . $delim;
}
my %conf;
tie %conf, 'esmith::config';
my %accounts;
tie %accounts, 'esmith::config', '/home/e-smith/db/accounts';
die "Username missing." unless defined ($USERNAME);
die "Username missing." unless defined($USERNAME);
my $type = db_get_type(\%accounts, $USERNAME);
die
"Account $USERNAME is not a user account; "
. "update email forwarding failed.\n"
unless $type eq 'user' || $USERNAME eq 'admin';
unless $type eq 'user' || $USERNAME eq 'admin';
my %processmail;
tie %processmail, 'esmith::config', '/home/e-smith/db/processmail';
# the syntax of imap folder names keeps changing
my $sep = '.';
#get Global rules
my @pmGlobRules = ();
foreach (sort keys %processmail)
{
push (@pmGlobRules, $_)
foreach (sort keys %processmail) {
push(@pmGlobRules, $_)
if (db_get_type(\%processmail, $_) eq 'pmGlobalRule');
}
#if they have rules add them to the templete
my $pmGlobRules = @pmGlobRules || '0';
if ($pmGlobRules > 0)
{
if ($pmGlobRules > 0) {
$OUT .= "\n";
$OUT .= "# ----start of Global rules--------\n";
my $pmGlobRule;
foreach $pmGlobRule (sort {$a <=> $b} @pmGlobRules)
{
foreach my $pmGlobRule (sort { $a <=> $b } @pmGlobRules) {
my $basis = db_get_prop(\%processmail, $pmGlobRule, "basis") || '';
my $criterion = db_get_prop(\%processmail, $pmGlobRule, "criterion") || '';
my $basis2 = db_get_prop(\%processmail, $pmGlobRule, "basis2") || '';
@@ -52,113 +55,86 @@
my $action = db_get_prop(\%processmail, $pmGlobRule, "action") || '';
my $action2 = db_get_prop(\%processmail, $pmGlobRule, "action2") || '';
## headers include the basis in the criterion
if ($basis eq 'headers')
{
if ($basis eq 'headers') {
$basis = $criterion;
$criterion = '';
}
if ($basis2 eq 'headers')
{
if ($basis2 eq 'headers') {
$basis2 = $criterion2;
$criterion2 = '';
}
## convert to procmail 'TO_' macro equivalent ??
foreach ($basis, $basis2)
{
if ($_ eq 'TO_')
{
#$_ = '((Original-)?(Resent-)?(To|Cc|Bcc)|(X-Envelope|Apparently(-Resent)?)-To)';
foreach ($basis, $basis2) {
if ($_ eq 'TO_') {
$_ = '(To|Cc)';
}
}
## construct the deliver line
if ($action eq 'sort')
{
# to a folder
$deliver1 = "to \"Maildir/"."$sep"."$deliver"."/\"";
my $deliver1 = '';
if ($action eq 'sort') {
$deliver1 = "to \"Maildir/" . "$sep" . "$deliver" . "/\"";
}
elsif ($action eq 'forward')
{
# to an email
$deliver1 = "to "."\"!$deliver\"";
elsif ($action eq 'forward') {
$deliver1 = "to " . "\"!$deliver\"";
}
elsif ($action eq 'delete')
{
# delete it, report, and add a blank line
elsif ($action eq 'delete') {
$deliver1 = "log \" --deleted --\" \n log \"\" \n exit";
#$deliver1 = "log \"--- deleted --\" \n log \"From: $From \" \n log \"Subject: $Subject \" \n log \"\" \n exit";
}
else
{
# freeform
else {
$deliver1 = "$deliver";
}
## construct the 2nd deliver line
if ($action2 eq 'sort')
{
# to a folder
$deliver2 = "\"Maildir/"."$sep"."$deliver2"."/\"";
my $deliver2 = '';
if ($action2 eq 'sort') {
$deliver2 = "\"Maildir/" . "$sep" . "$deliver2" . "/\"";
}
elsif ($action2 eq 'forward')
{
# to an email
elsif ($action2 eq 'forward') {
$deliver2 = "\"!$deliver2\"";
}
$OUT .= "\n";
if ($secondtest eq '')
{
if ($basis =~ /(>|<)/)
{
if ($secondtest eq '') {
if ($basis =~ /(>|<)/) {
$OUT .= "if ( \$SIZE $basis $criterion )\n";
}
else
{
$OUT .= "if ( /^"."$basis".".*$criterion/ )\n";
else {
my $regex = quote_regex("^$basis.*$criterion");
$OUT .= "if ( $regex )\n";
}
$OUT .= "\{\n";
$OUT .= "log \"--------- match user rule -- \"\n";
$OUT .= "log \"--------- $basis $criterion -- \"\n";
}
#basis2 can't test on size
else
{
if ($basis =~ /(>|<)/)
{
$OUT .= "if (( \$SIZE $basis $criterion ) && ( /^"."$basis2".".*$criterion2/ ))\n";
else {
if ($basis =~ /(>|<)/) {
$OUT .= "if (( \$SIZE $basis $criterion ) && ( /^" . "$basis2" . ".*$criterion2/ ))\n";
}
else
{
$OUT .= "if (( /^"."$basis".".*$criterion/) && (/^"."$basis2".".*$criterion2/ ))\n";
else {
my $regex1 = quote_regex("^$basis.*$criterion");
my $regex2 = quote_regex("^$basis2.*$criterion2");
$OUT .= "if (( $regex1 ) && ( $regex2 ))\n";
}
$OUT .= "\{\n";
$OUT .= "log \"--- match user rule ------------- \"\n";
$OUT .= "log \"--- $basis $criterion & $basis2 $criterion2 -- \"\n";
}
if ($copy eq 'no')
{
if ($copy eq 'no') {
$OUT .= "$deliver1\n";
$OUT .= "\}\n";
}
elsif ($copy eq 'yes' && $action2 eq 'inbox')
{
elsif ($copy eq 'yes' && $action2 eq 'inbox') {
$OUT .= "cc Maildir\n";
$OUT .= "$deliver1\n";
$OUT .= "\}\n";
}
else
{
else {
$OUT .= "cc $deliver2\n";
$OUT .= "$deliver1\n";
$OUT .= "\}\n";
}
}#foreach rule
}#if rules exist
}
}
}
}

View File

@@ -111,6 +111,8 @@
}
$OUT .= "\n";
$OUT .= "# User rule $pmRule\n";
if ($secondtest eq '')
{
if ($basis =~ /(>|<)/)
@@ -159,6 +161,7 @@
$OUT .= "$deliver1\n";
$OUT .= "\}\n";
}
$OUT .= "# End of User rule $pmRule\n";
}#foreach rule
}#if rules exist
}
}

View File

@@ -132,6 +132,9 @@
$secondtest = "* "."$basis2"."$criterion2"."\n";
}
$OUT .= "\n";
$OUT .= "# User rule $pmRule";
if ($copy eq 'no')
{
$OUT .= "\n";
@@ -162,6 +165,7 @@
$OUT .= " $deliver2\n";
$OUT .= "\}\n";
}
$OUT .= "# End of User rule $pmRule\n";
}#foreach rule
}#if rules exist
}
}

View File

@@ -18,15 +18,12 @@
if ($EmailForward eq 'forward');
}
if ($qmail{FilterType})
{
return '| /usr/bin/procmail ~/.procmailrc ; if [ $? -ne 0 ] ; then exit -1; else exit 99; fi;'
if ($qmail{FilterType} eq 'procmail');
return '| /usr/bin/maildrop ; if [ $? -ne 0 ] ; then exit -1; else exit 99; fi;'
if ($qmail{FilterType} eq 'maildrop' );
}
return '# Procmail/Maildrop disabled for all users'
}
if ($qmail{FilterType})
{
return '| /usr/bin/procmail ~/.procmailrc ; if [ $? -ne 0 ] ; then exit -1; else exit 99; fi;' if ($qmail{FilterType} eq 'procmail');
return '| /usr/bin/maildrop ; if [ $? -ne 0 ] ; then exit -1; else exit 99; fi;' if ($qmail{FilterType} eq 'maildrop');
return '| /var/qmail/bin/preline -f /usr/libexec/dovecot/dovecot-lda -a "$RECIPIENT" -d "$USER" ; if [ $? -ne 0 ] ; then exit -1; else exit 99; fi;' if ($qmail{FilterType} eq 'sieve');
}
return '# Procmail/Maildrop/sieve disabled for all users'
}
}

View File

@@ -22,15 +22,12 @@
if ($EmailForward eq 'forward');
}
if ($qmail{FilterType})
{
return '| /usr/bin/procmail ~/.procmailrc ; if [ $? -ne 0 ] ; then exit -1; else exit 99; fi;'
if ($qmail{FilterType} eq 'procmail');
return '| /usr/bin/maildrop ; if [ $? -ne 0 ] ; then exit -1; else exit 99; fi;'
if ($qmail{FilterType} eq 'maildrop' );
}
return '# Procmail/Maildrop disabled for all users'
if ($qmail{FilterType})
{
return '| /usr/bin/procmail ~/.procmailrc ; if [ $? -ne 0 ] ; then exit -1; else exit 99; fi;' if ($qmail{FilterType} eq 'procmail');
return '| /usr/bin/maildrop ; if [ $? -ne 0 ] ; then exit -1; else exit 99; fi;' if ($qmail{FilterType} eq 'maildrop');
return '| /var/qmail/bin/preline -f /usr/libexec/dovecot/dovecot-lda -a "$RECIPIENT" -d "$USER" ; if [ $? -ne 0 ] ; then exit -1; else exit 99; fi;' if ($qmail{FilterType} eq 'sieve');
}
return '# Procmail/Maildrop/Sieve disabled for all users'
}
}
}

View File

@@ -0,0 +1,22 @@
{
# vim: ft=perl:
use esmith::AccountsDB;
use esmith::ConfigDB;
our $adb = esmith::AccountsDB->open_ro or die "Couldn't open AccountsDB";
our $cdb = esmith::ConfigDB->open_ro or die "Couldn't open ConfigDB";
$user = $adb->get($USERNAME) or die "No user $USERNAME in AccountsDB";
%props = $user->props;
our $sievesupport = $cdb->get_prop('sieve','status') || 'disabled';
our $sieveuser = $props{Sieve} || 'enabled';
our $zarafa1 = $props{zarafa} || 'disabled1';
our $zarafa2 = ${'zarafa-server'}{GlobalForward} || 'disabled2';
our $EmailForward = $props{EmailForward} || '';
our $ForwardAddress = $props{ForwardAddress} || '';
$OUT = '';
}

View File

@@ -0,0 +1,6 @@
{
$OUT .= "# ---- Dovecot Sieve: Defaults and requirements --------\n";
$OUT .= "# Generated for user: $USERNAME\n";
$OUT .= "# Sieve support (system): $sievesupport, Sieve (user): $sieveuser\n";
$OUT .= "require [\"fileinto\", \"copy\", \"regex\", \"relational\", \"comparator-i;ascii-numeric\", \"duplicate\", \"envelope\", \"mime\"];\n";
}

View File

@@ -0,0 +1,28 @@
{
use esmith::config;
use esmith::db;
my %processmail;
tie %processmail, 'esmith::config', '/home/e-smith/db/processmail';
$OUT = '';
# control level of logging (comments only; Sieve has no direct logging)
my $loglevel = db_get_prop(\%processmail, $USERNAME, "loglevel") || 'some';
if ($loglevel eq 'none')
{
$OUT .= "\n";
$OUT .= "# ---- logging: none ------------------\n";
}
elsif ($loglevel eq 'some')
{
$OUT .= "\n";
$OUT .= "# ---- logging: some ------------------\n";
}
else
{
$OUT .= "\n";
$OUT .= "# ---- logging: verbose (debug) --------------\n";
}
}

View File

@@ -0,0 +1,9 @@
{
$OUT .= "\n";
$OUT .= "# ---- delete duplicates (by Message-ID) -------\n";
$OUT .= "if duplicate {\n";
$OUT .= " discard;\n";
$OUT .= " stop;\n";
$OUT .= "}\n";
}

View File

@@ -0,0 +1,236 @@
{
use esmith::config;
use esmith::db;
my %processmail;
tie %processmail, 'esmith::config', '/home/e-smith/db/processmail';
#get Global rules
my @pmGlobRules = ();
foreach (sort keys %processmail)
{
push (@pmGlobRules, $_)
if (db_get_type(\%processmail, $_) eq 'pmGlobalRule');
}
#if they have rules add them to the templete
my $pmGlobRules = @pmGlobRules || '0';
if ($pmGlobRules > 0)
{
$OUT .= "\n";
$OUT .= "# --- start of Global Sieve rules ---------\n";
my $pmGlobRule;
foreach $pmGlobRule (sort {$a <=> $b} @pmGlobRules)
{
my $basis = db_get_prop(\%processmail, $pmGlobRule, "basis") || '';
my $criterion = db_get_prop(\%processmail, $pmGlobRule, "criterion") || '';
my $basis2 = db_get_prop(\%processmail, $pmGlobRule, "basis2") || '';
my $secondtest = db_get_prop(\%processmail, $pmGlobRule, "basis2") || '';
my $criterion2 = db_get_prop(\%processmail, $pmGlobRule, "criterion2") || '';
my $deliver = db_get_prop(\%processmail, $pmGlobRule, "deliver") || '';
my $deliver2 = db_get_prop(\%processmail, $pmGlobRule, "deliver2") || '';
my $copy = db_get_prop(\%processmail, $pmGlobRule, "copy") || '';
my $action = db_get_prop(\%processmail, $pmGlobRule, "action") || '';
my $action2 = db_get_prop(\%processmail, $pmGlobRule, "action2") || '';
# prepare/escape criteria for Sieve strings
my $crit1 = $criterion; $crit1 =~ s/\\/\\\\/g; $crit1 =~ s/"/\\"/g;
my $crit2 = $criterion2; $crit2 =~ s/\\/\\\\/g; $crit2 =~ s/"/\\"/g;
# build condition 1
my $cond1 = '';
if ($basis eq '<' || $basis eq '>')
{
my $num = $criterion; $num =~ s/\s+//g;
$cond1 = ($basis eq '<') ? "size :under $num" : "size :over $num";
}
elsif ($basis eq 'TO_')
{
$cond1 = "anyof (address :all :contains [\"to\",\"cc\",\"bcc\"] \"$crit1\")";
}
elsif ($basis eq 'headers')
{
my $h = $criterion;
if ($h =~ /^\s*\^?([A-Za-z0-9\-]+)\s*:\s*(.*)$/s)
{
my $hn = $1; my $hv = $2; $hv =~ s/\\/\\\\/g; $hv =~ s/"/\\"/g;
$cond1 = "header :regex \"$hn\" \"$hv\"";
}
else
{
$cond1 = "anyof (header :regex \"Subject\" \"$crit1\", address :all :regex [\"from\",\"to\",\"cc\"] \"$crit1\")";
}
}
else
{
my %addr = map { $_ => 1 } qw(From To Cc Bcc Sender Reply-To Resent-From Resent-To Resent-Cc);
if ($addr{$basis})
{
my $lb = lc $basis;
$cond1 = "address :all :contains \"$lb\" \"$crit1\"";
}
else
{
$cond1 = "header :contains \"$basis\" \"$crit1\"";
}
}
# build condition 2 if present
my $cond2 = '';
if ($secondtest ne '')
{
if ($basis2 eq '<' || $basis2 eq '>')
{
my $num2 = $criterion2; $num2 =~ s/\s+//g;
$cond2 = ($basis2 eq '<') ? "size :under $num2" : "size :over $num2";
}
elsif ($basis2 eq 'TO_')
{
$cond2 = "anyof (address :all :contains [\"to\",\"cc\",\"bcc\"] \"$crit2\")";
}
elsif ($basis2 eq 'headers')
{
my $hh = $criterion2;
if ($hh =~ /^\s*\^?([A-Za-z0-9\-]+)\s*:\s*(.*)$/s)
{
my $hn2 = $1; my $hv2 = $2; $hv2 =~ s/\\/\\\\/g; $hv2 =~ s/"/\\"/g;
$cond2 = "header :regex \"$hn2\" \"$hv2\"";
}
else
{
$cond2 = "anyof (header :regex \"Subject\" \"$crit2\", address :all :regex [\"from\",\"to\",\"cc\"] \"$crit2\")";
}
}
else
{
my %addr2 = map { $_ => 1 } qw(From To Cc Bcc Sender Reply-To Resent-From Resent-To Resent-Cc);
if ($addr2{$basis2})
{
my $lb2 = lc $basis2;
$cond2 = "address :all :contains \"$lb2\" \"$crit2\"";
}
else
{
$cond2 = "header :contains \"$basis2\" \"$crit2\"";
}
}
}
# mailbox names for sort/create
my $mb1 = $deliver; $mb1 =~ s/"/\\"/g;
my $mb2 = $deliver2; $mb2 =~ s/"/\\"/g;
my $mbox1 = ($mb1 eq 'junkmail') ? "INBOX.Junk" : "INBOX.$mb1";
my $mbox2 = ($mb2 eq 'junkmail') ? "INBOX.Junk" : "INBOX.$mb2";
# begin rule
$OUT .= "\n";
$OUT .= "# Global rule $pmGlobRule\n";
if ($cond2 ne '')
{
$OUT .= "if allof ($cond1, $cond2) \{\n";
}
else
{
$OUT .= "if $cond1 \{\n";
}
# actions
if ($copy eq 'no')
{
if ($action eq 'sort' || $action eq 'create')
{
$OUT .= " fileinto \"$mbox1\";\n";
$OUT .= " stop;\n";
}
elsif ($action eq 'forward')
{
my $addr = $deliver; $addr =~ s/"/\\"/g;
$OUT .= " redirect \"$addr\";\n";
$OUT .= " stop;\n";
}
elsif ($action eq 'delete')
{
$OUT .= " discard;\n";
$OUT .= " stop;\n";
}
else
{
$OUT .= " # unsupported action \"$action\"; keeping in INBOX\n";
$OUT .= " keep;\n";
$OUT .= " stop;\n";
}
}
elsif ($copy eq 'yes' && $action2 eq 'inbox')
{
if ($action eq 'sort' || $action eq 'create')
{
$OUT .= " fileinto :copy \"$mbox1\";\n";
$OUT .= " keep;\n";
$OUT .= " stop;\n";
}
elsif ($action eq 'forward')
{
my $addr = $deliver; $addr =~ s/"/\\"/g;
$OUT .= " redirect :copy \"$addr\";\n";
$OUT .= " keep;\n";
$OUT .= " stop;\n";
}
elsif ($action eq 'delete')
{
$OUT .= " discard;\n";
$OUT .= " stop;\n";
}
else
{
$OUT .= " # unsupported action \"$action\"; keeping in INBOX\n";
$OUT .= " keep;\n";
$OUT .= " stop;\n";
}
}
else
{
# two deliveries (copy + second action)
if ($action eq 'sort' || $action eq 'create')
{
$OUT .= " fileinto :copy \"$mbox1\";\n";
}
elsif ($action eq 'forward')
{
my $addr = $deliver; $addr =~ s/"/\\"/g;
$OUT .= " redirect :copy \"$addr\";\n";
}
elsif ($action eq 'delete')
{
$OUT .= " discard;\n";
}
else
{
$OUT .= " # unsupported primary action \"$action\"\n";
}
if ($action2 eq 'sort')
{
$OUT .= " fileinto \"$mbox2\";\n";
}
elsif ($action2 eq 'forward')
{
my $addr2 = $deliver2; $addr2 =~ s/"/\\"/g;
$OUT .= " redirect \"$addr2\";\n";
}
elsif ($action2 eq 'inbox')
{
$OUT .= " keep;\n";
}
else
{
$OUT .= " # unsupported secondary action \"$action2\"\n";
}
$OUT .= " stop;\n";
}
$OUT .= "\}\n";
}#foreach rule
}#if rules exist
}

View File

@@ -0,0 +1,436 @@
{
use esmith::config;
use esmith::db;
# Quote for Sieve string literals (escape " and \ for Sieve)
sub sieve_quote {
my ($s) = @_;
$s //= '';
$s =~ s/\\/\\\\/g; # backslash -> double backslash
$s =~ s/"/\\"/g; # quote -> escaped quote
return $s;
}
# Prepare a regex pattern for embedding in a Sieve string (legacy fallback)
sub sieve_regex_quote_basic {
my ($s) = @_;
$s //= '';
$s =~ s/([\[\]\.])/\\$1/g; # make [ ] . literal in regex
$s =~ s/\\/\\\\/g; # escape backslashes for Sieve string
$s =~ s/"/\\"/g; # escape quotes for Sieve string
return $s;
}
# Pass through a true regex pattern but escape for Sieve string literal.
sub sieve_regex_passthrough {
my ($s) = @_;
$s //= '';
$s =~ s/\\/\\\\/g; # escape backslashes for Sieve string
$s =~ s/"/\\"/g; # escape quotes for Sieve string
return $s;
}
# Normalize DB input for non-regex (contains) tests:
# - remove user-added escapes for many common punctuation
# - strip a leading "Subject.*" (case-insensitive) if present
sub normalize_db_pattern {
my ($s) = @_;
$s //= '';
$s =~ s/\\([\[\]\.\-\(\)\{\}\+\?\^\$\|])/$1/g; # unescape common punctuation
$s =~ s/^\s*subject\s*\.\*\s*//i; # drop leading Subject.*
$s =~ s/^\s+|\s+$//g; # trim
return $s;
}
# Simplify an email-like pattern to a plain substring for address tests.
# Example: ".*user@domain\.tld" -> "user@domain.tld".
sub simplify_email_value {
my ($s) = @_;
$s //= '';
$s =~ s/^\s+|\s+$//g;
return '' if $s eq '';
$s =~ s/\.\*//g; # remove wildcard segments
# unescape common punctuation including @
$s =~ s/\\([@\[\]\.\-\(\)\{\}\+\?\^\$\|])/$1/g;
$s =~ s/^\s+|\s+$//g;
return $s;
}
# Extract a domain from a simplified email-like string.
# Returns '' if no clear domain found.
sub extract_domain {
my ($s) = @_;
$s //= '';
$s =~ s/^\s+|\s+$//g;
return '' if $s eq '';
if ($s =~ /@([^@\s<>"',;]+)/) {
my $d = $1;
$d =~ s/^[<"]+|[>"]+$//g;
$d =~ s/[,"';].*$//;
return lc $d;
}
# bare domain case (no @)
if ($s =~ /^[A-Za-z0-9](?:[A-Za-z0-9\.\-]*[A-Za-z0-9])?\.[A-Za-z0-9\-]{2,}$/) {
return lc $s;
}
return '';
}
# Build a robust Subject contains test:
# - raw header :contains "Subject" original
# - MIME-decoded header :mime :contains "Subject" underscore->space variant
sub build_subject_contains_anyof {
my ($p) = @_;
my $p_us2sp = $p; $p_us2sp =~ s/_/ /g;
return "anyof (header :contains \"Subject\" \"$p\", header :mime :contains \"Subject\" \"$p_us2sp\")";
}
# Build Subject+addresses fallback anyof clause
sub build_subject_addr_contains_anyof {
my ($p) = @_;
my $p_us2sp = $p; $p_us2sp =~ s/_/ /g;
return "anyof (header :contains \"Subject\" \"$p\", header :mime :contains \"Subject\" \"$p_us2sp\", address :all :contains [\"from\",\"to\",\"cc\"] \"$p\")";
}
my %processmail;
tie %processmail, 'esmith::config', '/home/e-smith/db/processmail';
# get users rules
my @pmRules = ();
foreach (sort keys %processmail)
{
push (@pmRules, $_)
if (db_get_type(\%processmail, $_) eq $USERNAME);
}
# if they have rules add them to the template
my $pmRules = @pmRules || '0';
if ($pmRules > 0)
{
$OUT .= "\n";
$OUT .= "# ---- user Sieve rules (".$pmRules.")------------------\n";
my $pmRule;
foreach $pmRule (sort @pmRules)
{
my $basis = db_get_prop(\%processmail, $pmRule, "basis") || '';
my $criterion = db_get_prop(\%processmail, $pmRule, "criterion") || '';
my $basis2 = db_get_prop(\%processmail, $pmRule, "basis2") || '';
my $secondtest = db_get_prop(\%processmail, $pmRule, "basis2") || '';
my $criterion2 = db_get_prop(\%processmail, $pmRule, "criterion2") || '';
my $deliver = db_get_prop(\%processmail, $pmRule, "deliver") || '';
my $deliver2 = db_get_prop(\%processmail, $pmRule, "deliver2") || '';
my $copy = db_get_prop(\%processmail, $pmRule, "copy") || '';
my $action = db_get_prop(\%processmail, $pmRule, "action") || '';
my $action2 = db_get_prop(\%processmail, $pmRule, "action2") || '';
# Normalize DB criteria (for contains/fallback paths)
my $norm1 = normalize_db_pattern($criterion);
my $norm2 = normalize_db_pattern($criterion2);
# Prepare strings for contains tests
my $crit1 = sieve_quote($norm1);
my $crit2 = sieve_quote($norm2);
# build condition 1
my $cond1 = '';
if ($basis eq '<' || $basis eq '>')
{
my $num = $criterion; $num =~ s/\s+//g;
$cond1 = ($basis eq '<') ? "size :under $num" : "size :over $num";
}
elsif ($basis eq 'TO_')
{
$cond1 = "anyof (address :all :contains [\"to\",\"cc\",\"bcc\"] \"$crit1\")";
}
elsif ($basis eq 'headers')
{
# Use raw DB value to preserve regex meta (.*, [], \., etc.) for parsing
my $raw = $criterion // '';
my $h = $raw; $h =~ s/^\s+|\s+$//g;
# Parse "HeaderName(s): value" (accept (From|To):... or \(From\|To\):...)
if ($h =~ /^\s*\^?\s*(.*?)\s*:\s*(.*)$/s)
{
my ($names, $hv) = ($1, $2);
$names =~ s/^\s*(?:\\?\()\s*//; # optional leading ( or \(
$names =~ s/\s*(?:\\?\))\s*$//; # optional trailing ) or \)
$names =~ s/\\\|/|/g; # treat \| as |
my @hn = split /\|/, $names;
@hn = grep { defined $_ && $_ ne '' } @hn;
if (@hn) {
my %addr = map { $_ => 1 } qw(From To Cc Bcc Sender Reply-To Resent-From Resent-To Resent-Cc);
my $all_addr = 1; for my $n (@hn) { $all_addr &&= exists $addr{$n}; }
my $hv_simple = simplify_email_value($hv);
my $hv_domain = extract_domain($hv_simple);
if ($all_addr && $hv_domain ne '') {
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\"";
} else {
# Fall back to true regex against listed headers
my @hn_q = map { sieve_quote($_) } @hn;
my $hn_list = join '","', @hn_q;
my $hv_re = sieve_regex_passthrough($hv);
$cond1 = "header :regex [\"$hn_list\"] \"$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 '') {
my $dq = sieve_quote($hv_domain);
$cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
} else {
my $p = sieve_quote($norm1);
$cond1 = build_subject_addr_contains_anyof($p);
}
}
}
else
{
# 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 '') {
my $dq = sieve_quote($hv_domain);
$cond1 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq\"";
} else {
my $p = sieve_quote($norm1);
$cond1 = build_subject_addr_contains_anyof($p);
}
}
}
else
{
# Non-"headers" basis (explicit header names)
if (lc($basis) eq 'subject') {
my $p = $crit1;
$cond1 = build_subject_contains_anyof($p);
} else {
my %addr = map { $_ => 1 } qw(From To Cc Bcc Sender Reply-To Resent-From Resent-To Resent-Cc);
if ($addr{$basis})
{
my $lb = lc $basis;
$cond1 = "address :all :contains \"$lb\" \"$crit1\"";
}
else
{
$cond1 = "header :contains \"$basis\" \"$crit1\"";
}
}
}
# build condition 2 if present
my $cond2 = '';
if ($secondtest ne '')
{
if ($basis2 eq '<' || $basis2 eq '>')
{
my $num2 = $criterion2; $num2 =~ s/\s+//g;
$cond2 = ($basis2 eq '<') ? "size :under $num2" : "size :over $num2";
}
elsif ($basis2 eq 'TO_')
{
$cond2 = "anyof (address :all :contains [\"to\",\"cc\",\"bcc\"] \"$crit2\")";
}
elsif ($basis2 eq 'headers')
{
my $raw2 = $criterion2 // '';
my $hh = $raw2; $hh =~ s/^\s+|\s+$//g;
if ($hh =~ /^\s*\^?\s*(.*?)\s*:\s*(.*)$/s)
{
my ($names2, $hv2) = ($1, $2);
$names2 =~ s/^\s*(?:\\?\()\s*//;
$names2 =~ s/\s*(?:\\?\))\s*$//;
$names2 =~ s/\\\|/|/g;
my @hn2 = split /\|/, $names2;
@hn2 = grep { defined $_ && $_ ne '' } @hn2;
if (@hn2) {
my %addr2 = map { $_ => 1 } qw(From To Cc Bcc Sender Reply-To Resent-From Resent-To Resent-Cc);
my $all_addr2 = 1; for my $n2 (@hn2) { $all_addr2 &&= exists $addr2{$n2}; }
my $hv2_simple = simplify_email_value($hv2);
my $hv2_domain = extract_domain($hv2_simple);
if ($all_addr2 && $hv2_domain ne '') {
my @lb2 = map { lc $_ } @hn2;
my $hn2_list = join '","', @lb2;
my $dq2 = sieve_quote($hv2_domain);
$cond2 = "address :domain :is [\"$hn2_list\"] \"$dq2\"";
} else {
my @hn2_q = map { sieve_quote($_) } @hn2;
my $hn2_list = join '","', @hn2_q;
my $hv2_re = sieve_regex_passthrough($hv2);
$cond2 = "header :regex [\"$hn2_list\"] \"$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 '') {
my $dq2 = sieve_quote($hv2_domain);
$cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
} else {
my $p2 = sieve_quote($norm2);
$cond2 = build_subject_addr_contains_anyof($p2);
}
}
}
else
{
# 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 '') {
my $dq2 = sieve_quote($hv2_domain);
$cond2 = "address :domain :is [\"from\",\"to\",\"cc\"] \"$dq2\"";
} else {
my $p2 = sieve_quote($norm2);
$cond2 = build_subject_addr_contains_anyof($p2);
}
}
}
else
{
if (lc($basis2) eq 'subject') {
my $p2 = $crit2;
$cond2 = build_subject_contains_anyof($p2);
} else {
my %addr2 = map { $_ => 1 } qw(From To Cc Bcc Sender Reply-To Resent-From Resent-To Resent-Cc);
if ($addr2{$basis2})
{
my $lb2 = lc $basis2;
$cond2 = "address :all :contains \"$lb2\" \"$crit2\"";
}
else
{
$cond2 = "header :contains \"$basis2\" \"$crit2\"";
}
}
}
}
# mailbox names for sort/create (sanitize: turn "/" into "." to avoid invalid names)
my $mb1 = $deliver; $mb1 =~ s/"/\\"/g; $mb1 =~ s|/|.|g;
my $mb2 = $deliver2; $mb2 =~ s/"/\\"/g; $mb2 =~ s|/|.|g;
my $mbox1 = ($mb1 eq 'junkmail') ? "Junk" : "$mb1";
my $mbox2 = ($mb2 eq 'junkmail') ? "Junk" : "$mb2";
# begin rule
$OUT .= "\n";
$OUT .= "# User rule $pmRule\n";
if ($cond2 ne '')
{
$OUT .= "if allof ($cond1, $cond2) \{\n";
}
else
{
$OUT .= "if $cond1 \{\n";
}
# actions
if ($copy eq 'no')
{
if ($action eq 'sort' || $action eq 'create')
{
$OUT .= " fileinto \"$mbox1\";\n";
$OUT .= " stop;\n";
}
elsif ($action eq 'forward')
{
my $addr = $deliver; $addr =~ s/"/\\"/g;
$OUT .= " redirect \"$addr\";\n";
$OUT .= " stop;\n";
}
elsif ($action eq 'delete')
{
$OUT .= " discard;\n";
$OUT .= " stop;\n";
}
else
{
$OUT .= " # unsupported action \"$action\"; keeping in INBOX\n";
$OUT .= " keep;\n";
$OUT .= " stop;\n";
}
}
elsif ($copy eq 'yes' && $action2 eq 'inbox')
{
if ($action eq 'sort' || $action eq 'create')
{
$OUT .= " fileinto :copy \"$mbox1\";\n";
$OUT .= " keep;\n";
$OUT .= " stop;\n";
}
elsif ($action eq 'forward')
{
my $addr = $deliver; $addr =~ s/"/\\"/g;
$OUT .= " redirect :copy \"$addr\";\n";
$OUT .= " keep;\n";
$OUT .= " stop;\n";
}
elsif ($action eq 'delete')
{
$OUT .= " discard;\n";
$OUT .= " stop;\n";
}
else
{
$OUT .= " # unsupported action \"$action\"; keeping in INBOX\n";
$OUT .= " keep;\n";
$OUT .= " stop;\n";
}
}
else
{
# two deliveries (copy + second action)
if ($action eq 'sort' || $action eq 'create')
{
$OUT .= " fileinto :copy \"$mbox1\";\n";
}
elsif ($action eq 'forward')
{
my $addr = $deliver; $addr =~ s/"/\\"/g;
$OUT .= " redirect :copy \"$addr\";\n";
}
elsif ($action eq 'delete')
{
$OUT .= " discard;\n";
}
else
{
$OUT .= " # unsupported primary action \"$action\"\n";
}
if ($action2 eq 'sort')
{
$OUT .= " fileinto \"$mbox2\";\n";
}
elsif ($action2 eq 'forward')
{
my $addr2 = $deliver2; $addr2 =~ s/"/\\"/g;
$OUT .= " redirect \"$addr2\";\n";
}
elsif ($action2 eq 'inbox')
{
$OUT .= " keep;\n";
}
else
{
$OUT .= " # unsupported secondary action \"$action2\"\n";
}
$OUT .= " stop;\n";
}
$OUT .= "\}\n";
$OUT .= "# End of User rule $pmRule\n";
}#foreach rule
}#if rules exist
}

View File

@@ -0,0 +1,7 @@
{
$OUT .= "\n";
$OUT .= "# ---- to the inbox (implicit keep if no rules matched) ------------------\n";
$OUT .= "keep;\n";
$OUT .= "\n";
$OUT .= "# ---- end of rules ------------------\n";
}

View File

@@ -0,0 +1,9 @@
{
$OUT .=<<"END"
plugin {
sieve = file:/home/e-smith/files/users/%u/.sievefilter
}
END
}

View File

@@ -0,0 +1,9 @@
{
$OUT .=<<"END"
plugin {
sieve_extensions = +fileinto +copy +regex +mime +body +duplicate
}
END
}

View File

@@ -35,6 +35,29 @@ my $pdb;
#my $hdb
#my $ddb
# Extra routine
sub do_extras {
#my $fred = 1/0;
my $c = shift;
if (defined $c->param("ApplyRules")){
$user = $c->get_panel_user();
$res = qx(sieve-filter -W -u $user -e ~$user/.sievefilter INBOX expunge 2>&1 | tee /var/log/sieve-filter.log | grep -v "warning" | grep -v "left message in mailbox 'INBOX'" | wc -l);
$ms_data{command_result} = ($res == 0) ? 'none' : $res;
$c->do_display('TABLE');
} elsif (defined $c->param("ReSerialise")) {
} elsif (defined $c->param("GenerateRule")) {
$res = qx(signal-event mailsorting-conf)
}
#my $referer = $c->req->headers->referrer;
#if ($referer) {
## Redirect back to the referring URL
#$c->redirect_to($referer);
#} else {
# }
}
# Validation routines - parameters for each panel
sub validate_TABLE {
@@ -106,18 +129,61 @@ my $pdb;
sub get_data_for_panel_TABLE {
# Return a hash with the fields required which will be loaded into the shared data
my $c = shift;
$cdb = esmith::ConfigDB::UTF8->open();
$adb = esmith::AccountsDB::UTF8->open || die "Couldn't open accounts db";
my $PanelUser = $c->get_panel_user();
# Count number of emails in top level INBOX
my $inbox_dir;
if ($PanelUser eq 'admin'){
$inbox_dir = "/home/e-smith/Maildir/cur/";
} else {
$inbox_dir = "/home/e-smith/files/users/$PanelUser/Maildir/cur/";
}
opendir(my $dh, $inbox_dir) or die "Cannot open directory: $!";
my @files = grep { -f "$inbox_dir/$_" && !/^\.{1,2}$/ } readdir($dh);
closedir($dh);
my $file_count = scalar @files;
# Get a list of the users.
my @users = $adb->get('admin');
push @users, $adb->users();
my @actualusers;
foreach my $user (@users) {
push @actualusers, [ $user->key()." - ".$user->prop('FirstName') . " " . $user->prop('LastName'),$user->key()];
}
my $actualusers_ref = \@actualusers; # array reference of key => firstname.lastname hashrefs
my %ret = (
'Data1'=>'Data for TABLE', #Example
# fields from Inputs in TABLE $fields['TABLE']
'account'=>$c->get_panel_user(),
'account'=> $PanelUser,
'username'=> $c->get_full_name(),
'FilterType' => $c->get_filtertype($PanelUser),
'Geekmode' => $adb->get_prop($PanelUser, "Geekmode") || FALSE,
'EmailCount' => $file_count,
'users' => $actualusers_ref
);
return %ret;
}
sub get_filtertype{
my $c = shift;
my $PanelUser = shift;
$cdb = esmith::ConfigDB::UTF8->open();
$adb = esmith::AccountsDB::UTF8->open || die "Couldn't open accounts db";
#Either the filtertype is set per user or it is global
my $globalFilterType = $cdb->get_prop('qmail', "FilterType");
if (! $globalFilterType) {
return $adb->get_prop($PanelUser,'FilterType') || ''
} else {
return $globalFilterType;
}
}
sub get_data_for_panel_RULES {
# Return a hash with the fields required which will be loaded into the shared data
my $cdb = esmith::ConfigDB::UTF8->open();
my $c = shift;
my $pdb = esmith::ConfigDB::UTF8->open('processmail') or die "Could not open processmail DB\n";
my $PanelUser = $c->get_panel_user();
@@ -142,10 +208,18 @@ sub get_data_for_panel_RULES {
my $rule_info = get_rule_from_db($c, $rule);
# and add heading message
$rule_info->{topmessage} = $c->l('ms_You_can_change_the_order');
my $FilterType = $c->get_filtertype($PanelUser);
my $FilterRule = 'No filter configured';
my $DBRemoveRule = "Db entries:".$c->esmith_db_record_to_multiline($pdb,$key,$PanelUser);
$FilterRule = "\nProcmail Rule:\n--------------\n".$c->get_procmail_rule($pdb,$key,$PanelUser) if $FilterType eq 'procmail';
$FilterRule = "\nMaildrop Rule:\n--------------\n".$c->get_maildrop_rule($pdb,$key,$PanelUser) if $FilterType eq 'maildrop';
$FilterRule = "\nDovecot-Sieve Rule:\n----------------------\n".$c->get_sieve_rule($pdb,$key,$PanelUser) if $FilterType eq 'sieve';
# Add/override any fields specific to the panel as needed
my %ret = (
%$rule_info,
'FilterRule'=> $FilterRule
);
return %ret;
@@ -154,12 +228,21 @@ sub get_data_for_panel_RULES {
sub get_data_for_panel_REMOVE {
# Return a hash with the fields required which will be loaded into the shared data
my $c = shift;
my $cdb = esmith::ConfigDB::UTF8->open();
my $pdb = esmith::ConfigDB::UTF8->open('processmail') or die "Could not open processmail DB\n";
my $PanelUser = $c->get_panel_user();
my $key = $c->param('Selected');
my $FilterType = $c->get_filtertype($PanelUser);
my $FilterRule = 'No filter configured';
my $DBRemoveRule = "Db entries:".$c->esmith_db_record_to_multiline($pdb,$key);
$FilterRule = "\nProcmail Rule:\n--------------\n".$c->get_procmail_rule($pdb,$key,$PanelUser) if $FilterType eq 'procmail';
$FilterRule = "\nMaildrop Rule:\n--------------\n".$c->get_maildrop_rule($pdb,$key,$PanelUser) if $FilterType eq 'maildrop';
$FilterRule = "\nDovecot-Sieve Rule:\n----------------------\n".$c->get_sieve_rule($pdb,$key,$PanelUser) if $FilterType eq 'sieve';
my %ret = (
# fields from Inputs in REMOVE $fields['REMOVE']
'RemoveRule'=>"Db entries:\n------------\n".$c->esmith_db_record_to_multiline($pdb,$key)."\n\nProcmail Rule:\n--------------".$c->get_procmail_rule($pdb,$key),
'RemoveRule'=> $DBRemoveRule,
'FilterRule' => $FilterRule
);
return %ret;
}
@@ -338,6 +421,7 @@ sub get_getAllRules {
my $PanelUser = $c->get_panel_user();
my $rec = $pdb->get($rule); # || return "Rule:$rule not found";
$rec->delete;
$c->app->log->info("Running: "."/sbin/e-smith/signal-event mailsorting-conf $PanelUser");
unless ( system ("/sbin/e-smith/signal-event mailsorting-conf $PanelUser") == 0 )
{ return $self->error('ERROR_UPDATING'); }
return 'ok';
@@ -355,7 +439,21 @@ sub create_link{
sub get_panel_user
{
my $c = shift;
return $c->session->{username};
my $adb = esmith::AccountsDB::UTF8->open();
if (!$c->is_admin){
return $c->session->{username};
} else {
if (my $thisUser = $c->param('account')) {
#Check it is a valid user
if ($adb->get_prop($thisUser, "FirstName")){
return $thisUser
} else {
return $c->session->{username}
}
} else {
return $c->session->{username}
}
}
}
sub get_full_name{
@@ -520,6 +618,7 @@ sub save_rule
foreach ("criterion","criterion2","action","action2","copy","basis","basis2","deliver","deliver2" )
{ $pdb->set_prop($rule, "$_", $filtered{$_}); }
$c->app->log->info("Running: "."/sbin/e-smith/signal-event mailsorting-conf $PanelUser");
unless ( system ("/sbin/e-smith/signal-event mailsorting-conf $PanelUser") == 0 )
{ return 'ERROR_UPDATING'; }
@@ -538,7 +637,7 @@ sub esmith_db_record_to_multiline {
next unless defined $value && $value ne '';
push @lines, "$field: $value";
}
return join("\n", @lines);
return "$key:"."\n------------\n".join("\n", @lines);
}
sub get_esmith_db_record_hashref {
@@ -549,54 +648,107 @@ sub get_esmith_db_record_hashref {
return \%props; # return a hashref
}
sub get_maildrop_rule {
my $c = shift;
my ($pdb, $key,$PanelUser) = @_;
return $c->extract_rule_from_file("~$PanelUser/.mailfilter",$key)
}
sub expand_tilde {
my ($filename) = @_;
# Special case for ~admin and ~admin/...
$filename =~ s{\A~admin(?:/|\z)}{/home/e-smith/};
# Generic ~ and ~user handling
$filename =~ s{\A~([^/]*)}
{ $1 ? (getpwnam($1))[7] : ($ENV{HOME} || (getpwuid($<))[7]) }ex;
return $filename;
}
sub extract_rule_from_file {
my ($c,$filename, $rule_id) = @_;
$c->app->log->info("get rule:$filename");
# Expand ~ to full home directory path if present
$filename = expand_tilde($filename);
$c->app->log->info("get rule:$filename");
# Read entire file content
open my $fh, '<', $filename or die "Could not open file '$filename': $!";
local $/; # Enable slurp mode to read whole file
my $content = <$fh>;
close $fh;
# Prepare start and end markers for the rule
my $start_marker = "# User rule $rule_id";
my $end_marker = "# End of User rule $rule_id";
# Extract the text block between the markers
if ($content =~ /(\Q$start_marker\E.*?\Q$end_marker\E)/s) {
return $1; # return matched block including markers
} else {
return "No matching rule found $filename $rule_id"
}
}
sub get_sieve_rule {
my $c = shift;
my ($pdb, $key,$PanelUser) = @_;
return $c->extract_rule_from_file("~$PanelUser/.sievefilter",$key)
}
sub get_procmail_rule {
my $c = shift;
# Logic taken from template expansion
my ($pdb, $key) = @_;
my $basis = $pdb->get_prop($key, "basis") || '';
my $criterion = $pdb->get_prop( $key, "criterion") || '';
my $basis2 = $pdb->get_prop( $key, "basis2") || '';
my $secondtest_orig = $pdb->get_prop( $key, "basis2") || '';
my $criterion2 = $pdb->get_prop( $key, "criterion2") || '';
my $deliver = $pdb->get_prop( $key, "deliver") || '';
my $deliver2 = $pdb->get_prop( $key, "deliver2") || '';
my $copy = $pdb->get_prop( $key, "copy") || '';
my $action = $pdb->get_prop( $key, "action") || '';
my $action2 = $pdb->get_prop( $key, "action2") || '';
# Process basis fields
foreach my $b (\$basis, \$basis2) {
$$b = $c->process_basis($$b);
}
# Handle spaces in deliver addresses
unless (($zarafa1 eq 'enabled') || ($zarafa2 eq 'enabled')) {
$_ =~ s/ /\\ /g for ($deliver, $deliver2);
}
# Build delivery paths
$deliver = $c->build_delivery_path($action, $deliver, $USERNAME);
$deliver2 = $c->build_delivery_path($action2, $deliver2, $USERNAME) if $action2;
# Construct second test line
my $secondtest = $secondtest_orig ? "* $basis2$criterion2\n" : '';
# Build rule string
my $rule = "\n";
if ($copy eq 'no') {
$rule .= ":0\n* $basis$criterion\n$secondtest$deliver\n";
}
elsif ($copy eq 'yes' && $action2 eq 'inbox') {
$rule .= ":0 c\n* $basis$criterion\n$secondtest$deliver\n";
}
else {
$rule .= ":0\n* $basis$criterion\n$secondtest\{\n"
. " :0 c\n $deliver\n\n :0\n $deliver2\n\}\n";
}
return $rule;
my ($pdb, $key,$PanelUser) = @_;
return $c->extract_rule_from_file("~$PanelUser/.procmailrc",$key)
}
# No longer needed - rule extracted from .procmailrc file.
#my $c = shift;
## Logic taken from template expansion (dangerous!)
#my ($pdb, $key) = @_;
#my $basis = $pdb->get_prop($key, "basis") || '';
#my $criterion = $pdb->get_prop( $key, "criterion") || '';
#my $basis2 = $pdb->get_prop( $key, "basis2") || '';
#my $secondtest_orig = $pdb->get_prop( $key, "basis2") || '';
#my $criterion2 = $pdb->get_prop( $key, "criterion2") || '';
#my $deliver = $pdb->get_prop( $key, "deliver") || '';
#my $deliver2 = $pdb->get_prop( $key, "deliver2") || '';
#my $copy = $pdb->get_prop( $key, "copy") || '';
#my $action = $pdb->get_prop( $key, "action") || '';
#my $action2 = $pdb->get_prop( $key, "action2") || '';
## Process basis fields
#foreach my $b (\$basis, \$basis2) {
#$$b = $c->process_basis($$b);
#}
## Handle spaces in deliver addresses
#unless (($zarafa1 eq 'enabled') || ($zarafa2 eq 'enabled')) {
#$_ =~ s/ /\\ /g for ($deliver, $deliver2);
#}
## Build delivery paths
#$deliver = $c->build_delivery_path($action, $deliver, $USERNAME);
#$deliver2 = $c->build_delivery_path($action2, $deliver2, $USERNAME) if $action2;
## Construct second test line
#my $secondtest = $secondtest_orig ? "* $basis2$criterion2\n" : '';
## Build rule string
#my $rule = "\n";
#if ($copy eq 'no') {
#$rule .= ":0\n* $basis$criterion\n$secondtest$deliver\n";
#}
#elsif ($copy eq 'yes' && $action2 eq 'inbox') {
#$rule .= ":0 c\n* $basis$criterion\n$secondtest$deliver\n";
#}
#else {
#$rule .= ":0\n* $basis$criterion\n$secondtest\{\n"
#. " :0 c\n $deliver\n\n :0\n $deliver2\n\}\n";
#}
#return $rule;
#}
sub process_basis {
my ($c,$basis) = @_;

View File

@@ -10,10 +10,11 @@ package SrvMngr::Controller::Mailsorting;
# navigation : 6000 1200
# menucat : U
#
# name : mailsorting, method : get, url : /mailsorting, ctlact : Mailsorting#main
# name : mailsortingu, method : post, url : /mailsortingu, ctlact : Mailsorting#do_update
# name : mailsortingd, method : get, url : /mailsortingd, ctlact : Mailsorting#do_display
# name : mailsorting, method : get, url : /mailsorting, ctlact : Mailsorting#main
# name : mailsortingu, method : post, url : /mailsortingu, ctlact : Mailsorting#do_update
# name : mailsortingd, method : get, url : /mailsortingd, ctlact : Mailsorting#do_display
# name : mailsortinge, method : post, url : /mailsortinge, ctlact : Mailsorting#do_update
# name : mailsortingx, method : get, url : /mailsortingx, ctlact : Mailsorting#do_extras
#
# routes : end
#
@@ -52,7 +53,7 @@ my $ndb;
my $hdb;
my $ddb;
my %ms_data;
our %ms_data;
require '/usr/share/smanager/lib/SrvMngr/Controller/Mailsorting-Custom.pm'; #The code that is to be added by the developer
@@ -121,6 +122,12 @@ sub do_update {
my $c = shift;
$c->app->log->info($c->log_req);
$c->app->log->info("Updating:".$c->param('user'));
#$c->app->log->info($c->dumper($c->req->body_params->to_hash));
#$c->app->log->info($c->dumper($c->req->query_params->to_hash));
#$c->app->log->info($c->dumper($c->req->params->to_hash));
my $modul = '';
#The most common ones - you might want to comment out any not used.
@@ -250,6 +257,11 @@ sub do_display {
my ($c,$trt) = @_;
$c->app->log->info($c->log_req);
#$c->app->log->info($c->dumper($c->req->body_params->to_hash));
#$c->app->log->info($c->dumper($c->req->query_params->to_hash));
#$c->app->log->info($c->dumper($c->req->params->to_hash));
#The most common ones - you might want to comment out any not used.
$cdb = esmith::ConfigDB::UTF8->open() || die("Couldn't open config db");

View File

@@ -26,7 +26,7 @@
'ms_deliver2' => 'deliver',
'ms_basis2' => 'basis',
'ms_Mail_sorting_rules' => 'Mail sorting rules',
'ms_User_Name' => 'User Name',
'ms_User_Name' => 'Full Name',
'ms_folder' => 'Folder (if sorting)',
'ms_action' => '2nd Action',
'ms_copy' => 'Copy',
@@ -51,4 +51,13 @@
'ms_deliver2_email' => '2nd action Delivery email (if forwarding)',
'ms_key' => 'Order of rule execution',
'ms_new_record' => 'Select the part of the email to be tested. You can match against part of an email address, header, subject, or the email size. Size is in bytes. Delete, sort or Forward any matches. The second match is optional, but if used both rules must match',
'ms_ERROR_FORWARD_NO_EMAIL' => 'No email address provided for forwarding',
'ms_ERROR_FORWARD_NO_EMAIL' => 'No email address provided for forwarding',
'ms_No_filtering' => 'No email filtering is configured',
'ms_This_account_is_in_geek_mode' => 'This accounts email filtering is solely according to the contents of .procmailrc, .mailfilter or .sievefilter no automatic generation of those files will occur',
'ms_Filtering_is_being_done_by' => 'Email filtering is using: ',
'ms_Number_of_Emails' => 'Number of Emails in INBOX: ',
'ms_Re_Serialise' => 'Re Serialise the Rules',
'ms_Apply_Sieve' => 'Apply rules to INBOX',
'ms_Result_of_command' => 'Number emails affected: ',
'ms_Filter_contents' => 'Corresponding filter contents',
'ms_Generate_Filter' => 'Generate filter rule'

View File

@@ -32,6 +32,16 @@ Generated by: SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.
.inline-buttons .link:active {
background-color: #c0c0c0; /* Even darker shade on click */
}
textarea {
field-sizing: content;
min-width: 400px;
font-family: Verdana, Tahoma, sans-serif;
font-size: 11.5px;
font-weight: normal;
color: black;
}
.Mailsorting-panel {}

View File

@@ -3,3 +3,45 @@
//
$(document).ready(function() {
});
document.addEventListener('DOMContentLoaded', function() {
// When the select changes, copy selected text (full name) to username field
//document.getElementById('account_select').addEventListener('change', function() {
//var select = this;
//var selectedOption = select.options[select.selectedIndex];
//document.getElementById('username_text').value = selectedOption.text;
//});
// On button click, reload page with ?user=<accountname>
const applyBtn = document.getElementById('applyBtn');
if (applyBtn) {
applyBtn.addEventListener('click', function() {
const select = document.getElementById('account_select');
const account = select ? select.value : '';
const url = new URL('smanager/mailsorting', window.location.origin);
if (account) {
url.searchParams.set('account', account);
}
window.location.href = url.toString();
});
}
const backBtn = document.getElementById('backBtn');
if (backBtn) {
backBtn.addEventListener('click', function() {
const url = new URL('smanager/mailsorting', window.location.origin);
window.location.href = url.toString();
});
}
const generateBtn = document.getElementById('generateBtn');
if (generateBtn) {
generateBtn.addEventListener('click', function() {
const url = new URL('smanager/mailsortingx?GenerateRule', window.location.origin);
//alert(url.toString());
window.location.href = url.toString();
});
}
});

View File

@@ -42,7 +42,7 @@
</div>
<br />
%}
%#Routing to partials according to trt parameter.
%#This ought to be cascading if/then/elsif, but is easier to just stack the if/then's rather like a case statement'

View File

@@ -2,10 +2,6 @@
%# Generated by SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.12.3 at 2025-05-04 12:59:05
%#
<div id="Mailsorting-REMOVE" class="partial Mailsorting-REMOVE">
<script>
window.onload = function() {
SelectInput();
};
</script>
% if (config->{debug} == 1) {
<pre>
@@ -18,6 +14,7 @@
%= hidden_field 'trt' => $ms_data->{trt}
%# die("here");
%= hidden_field 'Selected' => $c->param('Selected')
%= hidden_field ''account'' => $c->param('account')
%# Inputs etc in here.
@@ -29,7 +26,14 @@
%=l('ms_Rule_contents')
</span><span class=data>
% param 'RemoveRule' => $ms_data->{RemoveRule} unless param 'RemoveRule';
%= text_area 'RemoveRule', cols=>60, rows=>20, Readonly=>'true'
%= text_area 'RemoveRule', cols=>60, rows=>12, Readonly=>'true'
</span><br>
<span class=label>
%=l('ms_Filter_contents')
</span><span class=data>
% param 'FilterRule' => $ms_data->{FilterRule} unless param 'FilterRule';
%= text_area 'FilterRule', cols=>60, rows=>12, Readonly=>'true'
</span><br>
<span class='data'>

View File

@@ -2,11 +2,6 @@
%# Generated by SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.12.3 at 2025-05-04 12:59:05
%#
<div id="Mailsorting-RULES" class="partial Mailsorting-RULES">
<script>
window.onload = function() {
SelectInput();
};
</script>
% if (config->{debug} == 1) {
<pre>
%= dumper $ms_data
@@ -17,6 +12,8 @@
% param 'trt' => $ms_data->{trt} unless param 'trt';
%= hidden_field 'trt' => $ms_data->{trt}
%= hidden_field 'oldkey' => $ms_data->{oldkey}
%= hidden_field 'account' => $c->param('account')
%# Inputs etc in here.
<h1 class='head'><%=l('ms_Mail_sorting_rules')%></h1>
@@ -113,10 +110,29 @@
% param 'key' => $ms_data->{key} unless param 'key';
%= text_field 'key', size => '50', class => 'textinput key' , pattern=>'.*' , placeholder=>'key', title =>'Pattern regex mismatch', id => 'key_text'
<br></span></p>
<br /><br />
<!--
<p><span class=label>
%=l('')
</span><span class=data>
<button type="button" id="generateBtn"><%=l('ms_Generate_Filter')%></button>
<br></span></p>
-->
<p><span class=label>
%=l('ms_Filter_contents')
</span><span class=data>
% param 'FilterRule' => $ms_data->{FilterRule} unless param 'FilterRule';
%= text_area 'FilterRule', cols=>60, rows=>12, Readonly=>'true'
</span></p>
<span class='data'>
%= submit_button l('ms_Save'), class => 'action subm12'
</span>
%# Probably finally by a submit.
%end

View File

@@ -2,11 +2,6 @@
%# Generated by SM2Gen version:0.9(20Jan2025) Chameleon version:4.5.4 On Python:3.12.3 at 2025-05-04 12:59:05
%#
<div id="Mailsorting-TABLE" class="partial Mailsorting-TABLE">
<script>
window.onload = function() {
SelectInput();
};
</script>
% if (config->{debug} == 1) {
<pre>
%= dumper $ms_data
@@ -20,6 +15,7 @@
%= form_for "mailsortingu" => (method => 'POST') => begin
% param 'trt' => $ms_data->{trt} unless param 'trt';
%= hidden_field 'trt' => $ms_data->{trt}
%= hidden_field 'account' => $c->{account}
%# Inputs etc in here.
<h1 class='head'><%=l('ms_Mail_sorting_rules')%></h1>
@@ -27,28 +23,85 @@
<p class='paragraph para1'>
%=l('ms_Rules_are_executed_as_email')
</p>
% if (! $c->is_admin){
% # Single user mode
<p><span class=label>
%=l('ms_Account')
</span><span class=data>
% param 'account' => $ms_data->{account} unless param 'account';
%= text_field 'account', size => '50', class => 'textinput account' , readonly => 'readonly', pattern=>'.*' , placeholder=>'account', title =>'Pattern regex mismatch', id => 'account_text'
<br></span></p>
<p>
<span class="label">
%= l('ms_User_Name')
</span>
<span class="data">
% param 'username' => $ms_data->{username} unless param 'username';
%= text_field 'username', size => '50', class => 'textinput username', readonly => 'readonly', pattern=>'.*', placeholder=>'username', title =>'Pattern regex mismatch', id => 'username_text'
<br>
</span>
</p>
% } else {
% # Called by Admin
<p>
<span class="label">
%= l('ms_Account')
</span>
<span class="data">
% param 'account' => $ms_data->{account} unless param 'account';
%= select_field 'account' => $ms_data->{users}, class => 'input', id => 'account_select'
<button type="button" id="applyBtn">Switch User</button>
<button type="button" id="backBtn">Back to admin</button>
<br>
</span>
</p>
% }
<p><span class=label>
%=l('ms_Account')
</span><span class=data>
% param 'account' => $ms_data->{account} unless param 'account';
%= text_field 'account', size => '50', class => 'textinput account' , pattern=>'.*' , placeholder=>'account', title =>'Pattern regex mismatch', id => 'account_text'
<br></span></p>
<p><span class=label>
%=l('ms_User_Name')
</span><span class=data>
% param 'username' => $ms_data->{username} unless param 'username';
%= text_field 'username', size => '50', class => 'textinput username' , pattern=>'.*' , placeholder=>'username', title =>'Pattern regex mismatch', id => 'username_text'
<br></span></p>
% if ($ms_data->{Geekmode}){
<p class='paragraph para1'>
%= l('ms_This_account_is_in_geek_mode')
</p>
%}
% my $filterstr = $ms_data->{FilterType} eq 'procmail' ? 'Procmail' :
% $ms_data->{FilterType} eq 'maildrop' ? 'Maildrop' :
% $ms_data->{FilterType} eq 'sieve' ? 'Dovecot Sieve' :
% l('ms_No_filtering');
<p class='paragraph para1'>
%== $c->l('ms_Filtering_is_being_done_by')."<b>".$filterstr."</b>";
</p>
<p class='paragraph para1'>
%== $c->l('ms_Number_of_Emails')."<b>".$ms_data->{EmailCount}."</b>";
</p>
% if (defined $ms_data->{command_result}) {
<p class='paragraph para1'>
<%= $c->l('ms_Result_of_command') %><b><%= $ms_data->{command_result} %></b>
</p>
% }
<br />
<div class = 'inline-buttons'>
<a href='mailsortingd?trt=RULES' class='link link1'>
%= l('ms_Add_new_rule')
</a>
% if ($filterstr eq "Dovecot Sieve"){
<a href='mailsortingx?ApplyRules' class='link link1'>
%= l('ms_Apply_Sieve');
</a>
% }
%# <a href='mailsortingx?ReSerialise' class='link link1'>
%#= l('ms_Re_Serialise');
%# </a>
</div>
%#= link_to l('ms_Add_new_rule'), 'mailsortinge?trt=RULES' , class=>'link link1'
<h2 class='subh'><%=l('ms_Current_rules')%></h2>

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 4
%define release 5
Version: %{version}
Release: %{release}%{?dist}
License: GPL
@@ -32,6 +32,9 @@ SME Server enhancement to enable procmail or maildrop filtering for users.
Optionally provides user panels where users can create mail rules for themselves
%changelog
* Sat Oct 11 2025 Brian Read <brianr@koozali.org> 11.0.0-5.sme
- Add Dovecot Sieve generation and application [SME: 13232]
* Mon Oct 06 2025 Brian Read <brianr@koozali.org> 11.0.0-4.sme
- Add UTF8 and avoid potential DB caching problems [SME: 13209]