diff --git a/plugins/managesieve/config.inc.php.dist b/plugins/managesieve/config.inc.php.dist
index 28029e3b1d..4b11fe5eba 100644
--- a/plugins/managesieve/config.inc.php.dist
+++ b/plugins/managesieve/config.inc.php.dist
@@ -106,6 +106,21 @@ $config['managesieve_vacation_addresses_init'] = false;
// This option enables automatic filling of :from field on initial vacation form creation.
$config['managesieve_vacation_from_init'] = false;
+// Enables separate management interface for spam configuration
+// 0 - no separate section (default),
+// 1 - add Spam section,
+// 2 - add Spam section, but hide Filters section
+$config['managesieve_spam'] = 0;
+
+// The header used in messages to identify the spam score
+$config['managesieve_spam_header'] = 'x-spam-score';
+
+// The default threshold when to classify a message as spam
+$config['managesieve_spam_threshold'] = 5;
+
+// The default folder to move messages to when the filter kicks in
+$config['managesieve_spam_folder'] = 'INBOX.Junk';
+
// Supported methods of notify extension. Default: 'mailto'
$config['managesieve_notify_methods'] = ['mailto'];
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
index 0138343f4a..149efe2c4e 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
@@ -107,7 +107,7 @@ function start($mode = null)
$this->rc->session->remove('managesieve_current');
}
- if ($mode != 'vacation' && $mode != 'forward') {
+ if ($mode != 'vacation' && $mode != 'forward' && $mode != 'spam') {
if (!empty($_GET['_set']) || !empty($_POST['_set'])) {
$script_name = rcube_utils::get_input_string('_set', rcube_utils::INPUT_GPC, true);
}
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_spam.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_spam.php
new file mode 100644
index 0000000000..1bdab442a0
--- /dev/null
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_spam.php
@@ -0,0 +1,528 @@
+start('spam');
+
+ // Handle ajax requests
+ if ($action = rcube_utils::get_input_string('_act', rcube_utils::INPUT_GPC)) {
+ if ($action == 'spamacladd') {
+ $aid = rcube_utils::get_input_string('_aid', rcube_utils::INPUT_POST);
+ $id = $this->genid();
+ $content = $this->acl_div($id);
+ $this->rc->output->command('managesieve_actionfill', $content, $id, $aid);
+ $this->rc->output->send();return;
+ }
+
+ }
+
+ // find current spam rule
+ if (!$error) {
+ $this->spam_rule();
+ $this->spam_post();
+ }
+ else {
+ }
+
+ $this->plugin->add_label('spam.saving');
+ $this->rc->output->add_handlers([
+ 'spamform' => [$this, 'spam_form'],
+ ]);
+
+ $this->rc->output->set_pagetitle($this->plugin->gettext('spam'));
+ $this->rc->output->send('managesieve.spam');
+ }
+
+ /**
+ * Find and load sieve script with/for spam rule
+ *
+ * @params string Optional script name, however ignored
+ *
+ * @return int Connection status: 0 on success, >0 on failure
+ */
+ protected function load_script($script_name = NULL)
+ {
+ if ($this->script_name !== null) {
+ return 0;
+ }
+
+ $list = $this->list_scripts();
+ $master = $this->rc->config->get('managesieve_kolab_master');
+ $included = [];
+
+ $this->script_name = false;
+
+ // first try the active script(s)...
+ if (!empty($this->active)) {
+ // Note: there can be more than one active script on KEP:14-enabled server
+ foreach ($this->active as $script) {
+ if ($this->sieve->load($script)) {
+ foreach ($this->sieve->script->as_array() as $rule) {
+ if ($this->isSpamRule($rule)) {
+ $this->script_name = $script;
+ return 0;
+ }
+ }
+ }
+ }
+
+ // ...else try scripts included in active script (not for KEP:14)
+ foreach ($included as $script) {
+ if ($this->sieve->load($script)) {
+ foreach ($this->sieve->script->as_array() as $rule) {
+ if ($this->isSpamRule($rule)) {
+ $this->script_name = $script;
+ return 0;
+ }
+ }
+ }
+ }
+ }
+
+ // try all other scripts
+ if (!empty($list)) {
+ // else try included scripts
+ foreach (array_diff($list, $included, $this->active) as $script) {
+ if ($this->sieve->load($script)) {
+ foreach ($this->sieve->script->as_array() as $rule) {
+ if ($this->isSpamRule($rule)) {
+ $this->script_name = $script;
+ return 0;
+ }
+ }
+ }
+ }
+
+ // none of the scripts contains existing spam rule
+ // use any (first) active or just existing script (in that order)
+ if (!empty($this->active)) {
+ $this->sieve->load($this->script_name = $this->active[0]);
+ }
+ else {
+ $this->sieve->load($this->script_name = $list[0]);
+ }
+ }
+
+ return $this->sieve->error();
+ }
+
+ /* Given an rule, determines if the rule is a spam filter rule and
+ * can be rendered by this engine
+ * @param Array $rule Associated array with rule definitiion
+ * @return Bool True if is a spam filter rule
+ */
+ private function isSpamRule($rule) {
+
+ $spam_header = (string) ( $this->rc->config->get('managesieve_spam_header') ?? rcube_sieve_spam::DEFAULT_HEADER );
+
+ if (
+ !empty($rule['actions']) &&
+ !empty($rule['actions'][0]['type']) &&
+ !empty($rule['tests'][0]['test']) &&
+ !empty($rule['tests'][0]['arg1']) &&
+ $rule['actions'][0]['type'] == 'fileinto' &&
+ $rule['tests'][0]['test'] == 'header' &&
+ strtolower($rule['tests'][0]['arg1']) == strtolower($spam_header)) {
+
+ return true;
+ }
+ return false;
+
+ }
+
+ private function spam_rule()
+ {
+ $spam_threshold = (string) ( $this->rc->config->get('managesieve_spam_threshold') ?? rcube_sieve_spam::DEFAULT_THRESHOLD);
+
+
+ if ($this->script_name === false || $this->script_name === null || !$this->sieve->load($this->script_name)) {
+ return;
+ }
+
+ $list = [];
+ $active = in_array($this->script_name, $this->active);
+ $acl_allow = [];
+
+ // find (first) simple spam rule. We use the header identiefer and action fileinto
+ // as 'markers' to identify the script
+ foreach ($this->script as $idx => $rule) {
+ if (empty($this->spam) && $this->isSpamRule($rule)) {
+ $this->spam = array_merge($rule['actions'][0], [
+ 'idx' => $idx,
+ 'disabled' => $rule['disabled'] || !$active,
+ 'name' => $rule['name'],
+ 'tests' => $rule['tests'],
+ 'threshold' => $rule['tests'][1]['arg2'] ?? $spam_threshold,
+ ]);
+
+ foreach($rule['tests'] as $current) {
+ if ($current['test'] == 'header' &&
+ $current['arg1'] == 'from' &&
+ $current['type'] == 'contains') {
+ $acl_allow[] = $current['arg2'];
+ }
+ }
+
+ }
+ else if ($active) {
+ $list[$idx] = $rule['name'];
+ }
+ }
+
+ $this->spam['acl_allow'] = $acl_allow;
+ $this->spam['list'] = $list;
+ }
+
+ private function spam_post()
+ {
+ if (empty($_POST)) {
+ return;
+ }
+
+ $spam_header = (string) ($this->rc->config->get('managesieve_spam_heaser') ?? rcube_sieve_spam::DEFAULT_HEADER );
+ $spam_folder = (string) ($this->rc->config->get('managesieve_spam_folder') ?? rcube_sieve_spam::DEFAULT_FOLDER );
+ $spam_threshold = (string) ($this->rc->config->get('managesieve_spam_threshold') ?? rcube_sieve_spam::DEFAULT_THRESHOLD );
+
+ $threshold = rcube_utils::get_input_string('spam_threshold', rcube_utils::INPUT_POST) ?? $spam_threshold;
+
+ $status = rcube_utils::get_input_string('spam_status', rcube_utils::INPUT_POST);
+ $acl_allow = rcube_utils::get_input_value('_acl_allow', rcube_utils::INPUT_POST);
+
+ if (empty($error)) {
+ $rule = $this->spam;
+
+ $spam_tests[] = [
+ 'test' => 'header',
+ 'type' => 'matches',
+ 'not' => true,
+ 'arg1' => $spam_header,
+ 'arg2' => ["-*"],
+
+ ];
+
+
+ $spam_tests[] = [
+ 'test' => 'header',
+ 'type' => 'value-ge',
+ 'arg1' => $spam_header,
+ 'arg2' => [$threshold],
+ 'comparator' => 'i;ascii-numeric',
+
+ ];
+
+ foreach($acl_allow as $item) {
+ $item = trim($item);
+ if (strlen($item) >0) {
+ $spam_tests[] = [
+ 'test' => 'header',
+ 'type' => 'contains',
+ 'not' => true,
+ 'arg1' => 'from',
+ 'arg2' => $item,
+ ];
+ }
+
+ }
+
+ $rule['type'] = 'if';
+ $rule['name'] = "Spam filter settings";
+
+ $rule['disabled'] = $status == 'off';
+ $rule['tests'] = $spam_tests;
+ $rule['join'] = (count($spam_tests) > 1);
+
+ $rule['actions'] = [
+ [
+ 'type' => 'fileinto',
+ 'target' => $spam_folder,
+ ],
+ [
+ 'type' => 'stop'
+ ]];
+
+ if ($this->merge_rule($rule, $this->spam, $this->script_name)) {
+ $this->rc->output->show_message('managesieve.spamsaved', 'confirmation');
+ $this->rc->output->send();
+ }
+ }
+
+ if (empty($error)) {
+ $error = 'managesieve.saveerror';
+ }
+
+ $this->rc->output->show_message($error, 'error');
+ $this->rc->output->send();
+ }
+
+ /**
+ * Build the form elements for a spam-settings form. It consists of three parts,
+ * the threshold setting, enabled/disabled setting and "ACL Allowlist" settings
+ */
+ public function spam_form($attrib)
+ {
+
+ $spam_threshold = (string) $this->rc->config->get('managesieve_spam_folder') ?? rcube_sieve_spam::DEFAULT_THRESHOLD;
+
+
+ // build FORM tag
+ $form_id = !empty($attrib['id']) ? $attrib['id'] : 'form';
+ $out = $this->rc->output->request_form([
+ 'id' => $form_id,
+ 'name' => $form_id,
+ 'method' => 'post',
+ 'task' => 'settings',
+ 'action' => 'plugin.managesieve-spam',
+ 'noclose' => true,
+ 'class' => 'propform',
+ ] + $attrib
+ );
+
+ $this->rc->output->add_label(
+ 'managesieve.acldeleteconfirm'
+ );
+
+
+ // form elements
+ $status = new html_select(['name' => 'spam_status', 'id' => 'spam_status', 'class' => 'custom-select']);
+
+ $status->add($this->plugin->gettext('spam.on'), 'on');
+ $status->add($this->plugin->gettext('spam.off'), 'off');
+
+ $threshold = new html_inputfield(['name' => 'spam_threshold', 'id' => 'spam_threshold', 'class' => 'form-control' ]);
+
+ if (!isset($this->spam['threshold']) || !is_numeric($this->spam['threshold'])) {
+ $this->spam['threshold'] = $spam_threshold;
+ }
+
+ $table = new html_table(['cols' => 2]);
+
+ $threshold_slider = ''
+ . '
'
+ . ' ';
+
+
+ $table->add('title', html::label('spam_threshold', $this->plugin->gettext('spam.threshold')));
+ $table->add(null, $threshold_slider);
+
+ $table->add('title', html::label('spam_status', $this->plugin->gettext('spam.status')));
+ $table->add(null, $status->show(!isset($this->spam['disabled']) || $this->spam['disabled'] ? 'off' : 'on'));
+
+ $out .= html::tag('fieldset', '', html::tag('legend', null, $this->plugin->gettext('spam')) . $table->show($attrib));
+
+
+ $table = new html_table(['cols' => 2]);
+
+ $rows_num = 1;
+ $divs = '
'; + + $out .= html::tag('input', [ + 'type' => 'text', + 'name' => "_acl_allow[$id]", + 'id' => 'acl_allow' . $id, + 'value' => $value, + 'size' => 35, + 'class' => 'form-control', + 'style' => 'width: 100%', + ]); + + $add_label = rcube::Q($this->plugin->gettext('add')); + $del_label = rcube::Q($this->plugin->gettext('del')); + $out .= ' | '; + $out .= sprintf('' + . '%s', $id, $add_label, $id, $add_label); + $out .= sprintf('' + . '%s', $id, $del_label, $id, ($rows_num < 2 ? ' disabled' : ''), $del_label); + $out .= ' | '; + + $out .= '