2
0
Эх сурвалжийг харах

[Web] Allow to set global sieve filters

andryyy 5 жил өмнө
parent
commit
e1897b0631

+ 88 - 0
data/web/inc/functions.mailbox.inc.php

@@ -62,6 +62,85 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             'msg' => array('mailbox_modified', htmlspecialchars($_SESSION['mailcow_cc_username']))
           );
         break;
+        case 'global_filter':
+          if ($_SESSION['mailcow_cc_role'] != "admin") {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => 'access_denied'
+            );
+            return false;
+          }
+          $sieve = new Sieve\SieveParser();
+          $script_data = $_data['script_data'];
+          $script_data = str_replace("\r\n", "\n", $script_data); // windows -> unix
+          $script_data = str_replace("\r", "\n", $script_data);   // remaining -> unix
+          $filter_type = $_data['filter_type'];
+          try {
+            $sieve->parse($script_data);
+          }
+          catch (Exception $e) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => array('sieve_error', $e->getMessage())
+            );
+            return false;
+          }
+          if ($filter_type == 'prefilter') {
+            try {
+              if (file_exists('/global_sieve/before')) {
+                $filter_handle = fopen('/global_sieve/before', 'w');
+                if (!$filter_handle) {
+                  throw new Exception($lang['danger']['file_open_error']);
+                }
+                fwrite($filter_handle, $script_data);
+                fclose($filter_handle);
+              }
+            }
+            catch (Exception $e) {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_data_log),
+                'msg' => array('global_filter_write_error', htmlspecialchars($e->getMessage()))
+              );
+              return false;
+            }
+          }
+          elseif ($filter_type == 'postfilter') {
+            try {
+              if (file_exists('/global_sieve/after')) {
+                $filter_handle = fopen('/global_sieve/after', 'w');
+                if (!$filter_handle) {
+                  throw new Exception($lang['danger']['file_open_error']);
+                }
+                fwrite($filter_handle, $script_data);
+                fclose($filter_handle);
+              }
+            }
+            catch (Exception $e) {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_data_log),
+                'msg' => array('global_filter_write_error', htmlspecialchars($e->getMessage()))
+              );
+              return false;
+            }
+          }
+          else {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => 'invalid_filter_type'
+            );
+            return false;
+          }
+          $_SESSION['return'][] = array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+            'msg' => 'global_filter_written'
+          );
+          return true;
         case 'filter':
           $sieve = new Sieve\SieveParser();
           if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
@@ -2653,6 +2732,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
           return $filters;
         break;
+        case 'global_filter_details':
+          $global_filters = array();
+          if ($_SESSION['mailcow_cc_role'] != "admin") {
+            return false;
+          }
+          $global_filters['prefilter'] = file_get_contents('/global_sieve/before');
+          $global_filters['postfilter'] = file_get_contents('/global_sieve/after');
+          return $global_filters;
+        break;
         case 'filter_details':
           $filter_details = array();
           if (!is_numeric($_data)) {

+ 1 - 1
data/web/inc/functions.rspamd.inc.php

@@ -233,7 +233,7 @@ function rspamd($_action, $_data = null) {
             $map_content = trim($_data['rspamd_map_data']);
             $map_handle = fopen('/rspamd_custom_maps/' . $map, 'w');
             if (!$map_handle) {
-              throw new Exception('File cannot be opened for writing.');
+              throw new Exception($lang['danger']['file_open_error']);
             }
             fwrite($map_handle, $map_content . PHP_EOL);
             fclose($map_handle);

+ 7 - 4
data/web/js/site/mailbox.js

@@ -146,12 +146,15 @@ $(document).ready(function() {
   });
   // Disable submit button on script change
 	$('.textarea-code').on('keyup', function() {
-    $('#add_filter_btns > #add_sieve_script').attr({"disabled": true});
+    // Disable all "save" buttons, could be a "related button only" function, todo
+    $('.add_sieve_script').attr({"disabled": true});
 	});
   // Validate script data
-  $("#validate_sieve").click(function( event ) {
+  $(".validate_sieve").click(function( event ) {
     event.preventDefault();
-    var script = $('#script_data').val();
+    var validation_button = $(this);
+    // Get script_data textarea content from form the button was clicked in
+    var script = $('textarea[name="script_data"]', $(this).parents('form:first')).val();
     $.ajax({
       dataType: 'json',
       url: "/inc/ajax/sieve_validation.php",
@@ -161,7 +164,7 @@ $(document).ready(function() {
         var response = (data.responseText);
         response_obj = JSON.parse(response);
         if (response_obj.type == "success") {
-          $('#add_filter_btns > #add_sieve_script').attr({"disabled": false});
+          $(validation_button).next().attr({"disabled": false});
         }
         mailcow_alert_box(response_obj.msg, response_obj.type);
       },

+ 33 - 0
data/web/json_api.php

@@ -167,6 +167,9 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "filter":
             process_add_return(mailbox('add', 'filter', $attr));
           break;
+          case "global-filter":
+            process_add_return(mailbox('add', 'global_filter', $attr));
+          break;
           case "domain-policy":
             process_add_return(policy('add', 'domain', $attr));
           break;
@@ -332,6 +335,36 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
             }
           break;
 
+          case "global_filters":
+            $global_filters = mailbox('get', 'global_filter_details');
+            switch ($object) {
+              case "all":
+                if (!empty($global_filters)) {
+                  process_get_return($global_filters);
+                }
+                else {
+                  echo '{}';
+                }
+              break;
+              case "prefilter":
+                if (!empty($global_filters['prefilter'])) {
+                  process_get_return($global_filters['prefilter']);
+                }
+                else {
+                  echo '{}';
+                }
+              break;
+              case "postfilter":
+                if (!empty($global_filters['postfilter'])) {
+                  process_get_return($global_filters['postfilter']);
+                }
+                else {
+                  echo '{}';
+                }
+              break;
+            }
+          break;
+
           case "rl-domain":
             switch ($object) {
               case "all":

+ 1 - 1
data/web/lang/lang.cs.json

@@ -594,7 +594,7 @@
         "last_run": "Naposledy spuštěno",
         "excludes": "Vyloučené",
         "last_run_reset": "Plánovat další",
-        "sieve_info": "Můžete uložit více filtrů pro každého uživatele, ale současně může být aktivní pouze jeden prefilter a jeden postfilter.<br>\r\nKaždý filtr bude proveden v daném pořadí. Ani chyba při vykonávání skriptu nebo snaha o pozdržení nezastaví vykonání dalších skriptů.<br>\r\n<a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before\" target=\"_blank\">Global sieve prefilter</a> → Prefilter → Uživatelské skripty → Postfilter → <a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after\" target=\"_blank\">Global sieve postfilter</a>",
+        "sieve_info": "Můžete uložit více filtrů pro každého uživatele, ale současně může být aktivní pouze jeden prefilter a jeden postfilter.<br>\r\nKaždý filtr bude proveden v daném pořadí. Ani chyba při vykonávání skriptu nebo snaha o pozdržení nezastaví vykonání dalších skriptů.<br><br>Global sieve prefilter → Prefilter → Uživatelské skripty → Postfilter → Global sieve postfilter",
         "sogo_visible": "Alias dostupný v SOGo",
         "sogo_visible_y": "Zobrazit alias v SOGo",
         "sogo_visible_n": "Skrýt alias v SOGo",

+ 5 - 1
data/web/lang/lang.de.json

@@ -24,6 +24,8 @@
         "apps": "Apps"
     },
     "danger": {
+        "invalid_filter_type": "Ungültiger Filtertyp",
+        "file_open_error": "Datei kann nicht zum Schreiben geöffnet werden",
         "transport_dest_exists": "Transport Maps Ziel \"%s\" existiert bereits",
         "unlimited_quota_acl": "Unendliche Quota untersagt durch ACL",
         "mysql_error": "MySQL Fehler: %s",
@@ -60,6 +62,7 @@
         "settings_map_invalid": "Regel ID %s ist ungültig",
         "app_passwd_id_invalid": "App Passwort ID %s ist ungültig",
         "global_map_invalid": "Rspamd Map %s ist ungültig",
+        "global_filter_write_error": "Kann Filterdatei nicht schreiben: %s",
         "global_map_write_error": "Kann globale Map ID %s nicht schreiben: %s",
         "invalid_host": "Ungültiger Host: %s",
         "relayhost_invalid": "Mapeintrag %s ist ungültig",
@@ -132,6 +135,7 @@
         "extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain"
     },
     "success": {
+        "global_filter_written": "Filterdatei wurde erfolreich geschrieben",
         "learned_ham": "ID %s wurde erfolreich als Ham gelernt",
         "verified_totp_login": "TOTP Anmeldung verifiziert",
         "verified_u2f_login": "U2F Anmeldung verifiziert",
@@ -632,7 +636,7 @@
         "last_run": "Letzte Ausführung",
         "last_run_reset": "Als nächstes ausführen",
         "excludes": "Ausschlüsse",
-        "sieve_info": "Es können mehrere Filter pro Benutzer existieren, aber nur ein Filter eines Typs (Pre-/Postfilter) kann gleichzeitig aktiv sein.<br>\r\nDie Ausführung erfolgt in nachstehender Reihenfolge. Ein fehlgeschlagenes Script sowie der Befehl \"keep;\" stoppen die weitere Verarbeitung <b>nicht</b>.<br>\r\n<a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before\" target=\"_blank\">Global sieve prefilter</a> → Prefilter → User scripts → Postfilter → <a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after\" target=\"_blank\">Global sieve postfilter</a>",
+        "sieve_info": "Es können mehrere Filter pro Benutzer existieren, aber nur ein Filter eines Typs (Pre-/Postfilter) kann gleichzeitig aktiv sein.<br>\r\nDie Ausführung erfolgt in nachstehender Reihenfolge. Ein fehlgeschlagenes Script sowie der Befehl \"keep;\" stoppen die weitere Verarbeitung <b>nicht</b>.<br><br>Global sieve prefilter → Prefilter → User scripts → Postfilter → Global sieve postfilter",
         "sogo_visible": "Alias Sichtbarkeit in SOGo",
         "sogo_visible_y": "Alias in SOGo anzeigen",
         "sogo_visible_n": "Alias in SOGo verbergen",

+ 5 - 1
data/web/lang/lang.en.json

@@ -24,6 +24,8 @@
         "hibp_ok": "No match found."
     },
     "danger": {
+        "invalid_filter_type": "Invalid filter type",
+        "file_open_error": "File cannot be opened for writing",
         "transport_dest_exists": "Transport destination \"%s\" exists",
         "unlimited_quota_acl": "Unlimited quota prohibited by ACL",
         "mysql_error": "MySQL error: %s",
@@ -61,6 +63,7 @@
         "app_passwd_id_invalid": "App password ID %s invalid",
         "global_map_invalid": "Global map ID %s invalid",
         "global_map_write_error": "Could not write global map ID %s: %s",
+        "global_filter_write_error": "Could not write filter file: %s",
         "invalid_host": "Invalid host specified: %s",
         "relayhost_invalid": "Map entry %s is invalid",
         "dkim_domain_or_sel_invalid": "DKIM domain or selector invalid: %s",
@@ -132,6 +135,7 @@
         "extra_acl_invalid_domain": "External sender \"%s\" uses an invalid domain"
     },
     "success": {
+        "global_filter_written": "Filter was successfully written to file",
         "learned_ham": "Successfully learned ID % as ham",
         "verified_totp_login": "Verified TOTP login",
         "verified_u2f_login": "Verified U2F login",
@@ -634,7 +638,7 @@
         "last_run": "Last run",
         "excludes": "Excludes",
         "last_run_reset": "Schedule next",
-        "sieve_info": "You can store multiple filters per user, but only one prefilter and one postfilter can be active at the same time.<br>\r\nEach filter will be processed in the described order. Neither a failed script nor an issued \"keep;\" will stop processing of further scripts.<br>\r\n<a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before\" target=\"_blank\">Global sieve prefilter</a> → Prefilter → User scripts → Postfilter → <a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after\" target=\"_blank\">Global sieve postfilter</a>",
+        "sieve_info": "You can store multiple filters per user, but only one prefilter and one postfilter can be active at the same time.<br>\r\nEach filter will be processed in the described order. Neither a failed script nor an issued \"keep;\" will stop processing of further scripts.<br><br>Global sieve prefilter → Prefilter → User scripts → Postfilter → Global sieve postfilter",
         "sogo_visible": "Alias is visible in SOGo",
         "sogo_visible_y": "Show alias in SOGo",
         "sogo_visible_n": "Hide alias in SOGo",

+ 1 - 1
data/web/lang/lang.ru.json

@@ -633,7 +633,7 @@
     "running": "В процессе выполнения",
     "set_postfilter": "Использовать как постфильтр",
     "set_prefilter": "Использовать как предв. фильтр",
-    "sieve_info": "Вы можете сохранить несколько фильтров для каждого пользователя, но только один предварительный фильтр и один постфильтр могут быть активными одновременно.<br>\r\n Каждый фильтр будет обработан в описанном порядке. Не сломаный скрипт, не <code>keep;</code> не остановит обработку дальнейших скриптов.<br>\r\n <a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before\" target=\"_blank\">Global sieve prefilter</a> → Prefilter → User scripts → Postfilter → <a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after\" target=\"_blank\">Global sieve postfilter</a>",
+    "sieve_info": "Вы можете сохранить несколько фильтров для каждого пользователя, но только один предварительный фильтр и один постфильтр могут быть активными одновременно.<br>\r\n Каждый фильтр будет обработан в описанном порядке. Не сломаный скрипт, не <code>keep;</code> не остановит обработку дальнейших скриптов.<br><br>Global sieve prefilter → Prefilter → User scripts → Postfilter → Global sieve postfilter",
     "sieve_preset_1": "Discard mail with probable dangerous file types",
     "sieve_preset_2": "Always mark the e-mail of a specific sender as seen",
     "sieve_preset_3": "Discard silently, stop all further sieve processing",

+ 1 - 1
data/web/lang/lang.sk.json

@@ -634,7 +634,7 @@
         "last_run": "Posledné spustenie",
         "excludes": "Vyraďuje",
         "last_run_reset": "Naplánovať ďalší",
-        "sieve_info": "Môžete uchovávať viacero filtrov pre užívateľa, ale iba jeden prefilter a jeden postfilter môže byť aktívny v daný okamih.<br>\r\n Každý filter bude spracovaný v nastavenom poradí. Ani zlyhanie skriptu alebo zadržanie nezastaví spracovanie ďalších skriptov.<br>\r\n<a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before\" target=\"_blank\">Globálny sieve prefilter</a> → Prefilter → Skripty užívateľa → Postfilter → <a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after\" target=\"_blank\">Globálny sieve postfilter</a>",
+        "sieve_info": "Môžete uchovávať viacero filtrov pre užívateľa, ale iba jeden prefilter a jeden postfilter môže byť aktívny v daný okamih.<br>\r\n Každý filter bude spracovaný v nastavenom poradí. Ani zlyhanie skriptu alebo zadržanie nezastaví spracovanie ďalších skriptov.<br><br>Globálny sieve prefilter → Prefilter → Skripty užívateľa → Postfilter → Globálny sieve postfilter",
         "sogo_visible": "Alias je viditeľný v SOGo",
         "sogo_visible_y": "Ukázať alias v SOGo",
         "sogo_visible_n": "Skryť alias v SOGo",

+ 1 - 1
data/web/lang/lang.sv.json

@@ -634,7 +634,7 @@
         "last_run": "Senaste körningen",
         "excludes": "Exkluderar",
         "last_run_reset": "Schemalägg nästa",
-        "sieve_info": "Du kan skapa flera filter per användare, men bara ett förfilter och ett postfilter kan vara aktiva samtidigt.<br>\r\nVarje filter bearbetas i den beskrivna ordningen nedan. Varken ett misslyckat filter som kör fel, eller kommandot \"keep:\" stoppar bearbetningen av övriga filter.<br>\r\n<a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_before\" target=\"_blank\">Globala sieve förfilter</a> → Prefilter → User scripts → Postfilter → <a href=\"https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/global_sieve_after\" target=\"_blank\">Global sieve postfilter</a>",
+        "sieve_info": "Du kan skapa flera filter per användare, men bara ett förfilter och ett postfilter kan vara aktiva samtidigt.<br>\r\nVarje filter bearbetas i den beskrivna ordningen nedan. Varken ett misslyckat filter som kör fel, eller kommandot \"keep:\" stoppar bearbetningen av övriga filter.<br><br>Globala sieve förfilter → Prefilter → User scripts → Postfilter → Global sieve postfilter",
         "sogo_visible": "Visa detta alias i SOGo",
         "sogo_visible_y": "Visa alias i SOGo",
         "sogo_visible_n": "Dölj alias i SOGo",

+ 46 - 1
data/web/mailbox.php

@@ -328,7 +328,9 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
                 </ul>
               </div>
             </div>
-            <p style="margin:10px" class="help-block"><?=$lang['mailbox']['sieve_info'];?></p>
+            <div class="panel-body">
+              <p class="help-block"><?=$lang['mailbox']['sieve_info'];?></p><br>
+            </div>
             <!-- <div class="mass-actions-mailbox" data-actions-header="true"></div> -->
             <div class="table-responsive">
               <table class="table table-striped" id="filter_table"></table>
@@ -349,6 +351,49 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
                 <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addFilterModalAdmin"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_filter'];?></a>
               </div>
             </div>
+            <div class="panel-body">
+              <?php
+              $global_filters = mailbox('get', 'global_filter_details');
+              ?>
+              <div class="row">
+                <div class="col-lg-6">
+                <h5>Global Prefilter</h5>
+                <form class="form-horizontal" data-cached-form="false" role="form" data-id="add_prefilter">
+                  <div class="form-group">
+                    <div class="col-sm-12">
+                      <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code script_data" rows="10" name="script_data" required><?=$global_filters['prefilter'];?></textarea>
+                    </div>
+                  </div>
+                  <div class="form-group">
+                    <div class="col-sm-10 add_filter_btns">
+                      <div class="btn-group">
+                        <button class="btn btn-sm btn-default validate_sieve" href="#"><?=$lang['add']['validate'];?></button>
+                        <button class="btn btn-sm btn-success add_sieve_script" data-action="add_item" data-id="add_prefilter" data-api-url='add/global-filter' data-api-attr='{"filter_type":"prefilter"}' href="#" disabled><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+                      </div>
+                    </div>
+                  </div>
+                </form>
+                </div>
+                <div class="col-lg-6">
+                <h5>Global Postfilter</h5>
+                <form class="form-horizontal" data-cached-form="false" role="form" data-id="add_postfilter">
+                  <div class="form-group">
+                    <div class="col-sm-12">
+                      <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code script_data" rows="10" name="script_data" required><?=$global_filters['postfilter'];?></textarea>
+                    </div>
+                  </div>
+                  <div class="form-group">
+                    <div class="col-sm-10 add_filter_btns">
+                      <div class="btn-group">
+                        <button class="btn btn-sm btn-default validate_sieve" href="#"><?=$lang['add']['validate'];?></button>
+                        <button class="btn btn-sm btn-success add_sieve_script" data-action="add_item" data-id="add_postfilter" data-api-url='add/global-filter' data-api-attr='{"filter_type":"postfilter"}' href="#" disabled><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+                      </div>
+                    </div>
+                  </div>
+                </form>
+                </div>
+              </div>
+            </div>
           </div>
         </div>
 

+ 4 - 4
data/web/modals/mailbox.php

@@ -592,7 +592,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
           <div class="form-group">
             <label class="control-label col-sm-2" for="script_data">Script:</label>
             <div class="col-sm-10">
-              <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" id="script_data" name="script_data" required></textarea>
+              <textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code script_data" rows="20" name="script_data" required></textarea>
             </div>
           </div>
           <div class="form-group">
@@ -604,9 +604,9 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
             </div>
           </div>
           <div class="form-group">
-            <div class="col-sm-offset-2 col-sm-10" id="add_filter_btns">
-              <button class="btn btn-default" id="validate_sieve" href="#"><?=$lang['add']['validate'];?></button>
-              <button class="btn btn-success" id="add_sieve_script" data-action="add_item" data-id="add_filter" data-api-url='add/filter' data-api-attr='{}' href="#" disabled><?=$lang['admin']['add'];?></button>
+            <div class="col-sm-offset-2 col-sm-10 add_filter_btns">
+              <button class="btn btn-default validate_sieve" href="#"><?=$lang['add']['validate'];?></button>
+              <button class="btn btn-success add_sieve_script" data-action="add_item" data-id="add_filter" data-api-url='add/filter' data-api-attr='{}' href="#" disabled><?=$lang['admin']['add'];?></button>
             </div>
           </div>
         </form>