Browse Source

Merge pull request #4966 from mailcow/fix/bs5

BS5 UI fixes
Niklas Meyer 2 years ago
parent
commit
27c87de4ed
37 changed files with 522 additions and 97 deletions
  1. 1 0
      data/web/admin.php
  2. 0 0
      data/web/css/build/013-datatables.css
  3. 11 0
      data/web/css/build/014-mailcow.css
  4. 4 0
      data/web/css/build/015-responsive.css
  5. 80 1
      data/web/css/themes/lumen-bootstrap.css
  6. 8 0
      data/web/css/themes/mailcow-darkmode.css
  7. 1 0
      data/web/debug.php
  8. BIN
      data/web/fonts/source-sans-pro-v21-latin-300.woff
  9. BIN
      data/web/fonts/source-sans-pro-v21-latin-300.woff2
  10. BIN
      data/web/fonts/source-sans-pro-v21-latin-300italic.woff
  11. BIN
      data/web/fonts/source-sans-pro-v21-latin-300italic.woff2
  12. BIN
      data/web/fonts/source-sans-pro-v21-latin-700.woff
  13. BIN
      data/web/fonts/source-sans-pro-v21-latin-700.woff2
  14. BIN
      data/web/fonts/source-sans-pro-v21-latin-700italic.woff
  15. BIN
      data/web/fonts/source-sans-pro-v21-latin-700italic.woff2
  16. BIN
      data/web/fonts/source-sans-pro-v21-latin-italic.woff
  17. BIN
      data/web/fonts/source-sans-pro-v21-latin-italic.woff2
  18. BIN
      data/web/fonts/source-sans-pro-v21-latin-regular.woff
  19. BIN
      data/web/fonts/source-sans-pro-v21-latin-regular.woff2
  20. 33 0
      data/web/inc/functions.customize.inc.php
  21. 47 46
      data/web/inc/functions.mailbox.inc.php
  22. 10 2
      data/web/js/build/013-mailcow.js
  23. 30 0
      data/web/js/site/admin.js
  24. 94 15
      data/web/js/site/debug.js
  25. 10 0
      data/web/js/site/edit.js
  26. 77 11
      data/web/js/site/mailbox.js
  27. 14 3
      data/web/js/site/quarantine.js
  28. 5 0
      data/web/js/site/queue.js
  29. 25 0
      data/web/js/site/user.js
  30. 16 3
      data/web/json_api.php
  31. 7 0
      data/web/lang/lang.de-de.json
  32. 7 0
      data/web/lang/lang.en-gb.json
  33. 14 0
      data/web/templates/admin/tab-config-customize.twig
  34. 2 2
      data/web/templates/admin/tab-routing.twig
  35. 12 2
      data/web/templates/debug.twig
  36. 4 2
      data/web/templates/edit/mailbox.twig
  37. 10 10
      data/web/templates/mailbox.twig

+ 1 - 0
data/web/admin.php

@@ -103,6 +103,7 @@ $template_data = [
   'rsettings' => $rsettings,
   'rsettings' => $rsettings,
   'rspamd_regex_maps' => $rspamd_regex_maps,
   'rspamd_regex_maps' => $rspamd_regex_maps,
   'logo_specs' => customize('get', 'main_logo_specs'),
   'logo_specs' => customize('get', 'main_logo_specs'),
+  'ip_check' => customize('get', 'ip_check'),
   'password_complexity' => password_complexity('get'),
   'password_complexity' => password_complexity('get'),
   'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
   'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
   'lang_admin' => json_encode($lang['admin']),
   'lang_admin' => json_encode($lang['admin']),

+ 0 - 0
data/web/css/build/015-datatables.css → data/web/css/build/013-datatables.css


+ 11 - 0
data/web/css/build/013-mailcow.css → data/web/css/build/014-mailcow.css

@@ -370,3 +370,14 @@ button[aria-expanded='true'] > .caret {
 .btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
 .btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
     background-color: #f0f0f0 !important;
     background-color: #f0f0f0 !important;
 }
 }
+
+
+div.dataTables_wrapper div.dataTables_filter {
+  text-align: left;
+}
+div.dataTables_wrapper div.dataTables_length {
+  text-align: right;
+}
+.dataTables_paginate, .dataTables_length, .dataTables_filter {
+  margin: 10px 0!important;
+}

+ 4 - 0
data/web/css/build/014-responsive.css → data/web/css/build/015-responsive.css

@@ -199,6 +199,10 @@
     display: none !important;
     display: none !important;
   }
   }
 
 
+  div.dataTables_wrapper div.dataTables_length {
+    text-align: left;
+  }
+
 }
 }
 
 
 @media (max-width: 350px) {
 @media (max-width: 350px) {

+ 80 - 1
data/web/css/themes/lumen-bootstrap.css

@@ -11,7 +11,86 @@
  * Copyright 2011-2021 Twitter, Inc.
  * Copyright 2011-2021 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  */
  */
-@import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,300;0,400;0,700;1,400&display=swap");
+
+/* source-sans-pro-300 - latin */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: normal;
+  font-weight: 300;
+  src: url('/fonts/source-sans-pro-v21-latin-300.eot'); /* IE9 Compat Modes */
+  src: local(''),
+       url('/fonts/source-sans-pro-v21-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/fonts/source-sans-pro-v21-latin-300.woff2') format('woff2'), /* Super Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-300.woff') format('woff'), /* Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */
+       url('/fonts/source-sans-pro-v21-latin-300.svg#SourceSansPro') format('svg'); /* Legacy iOS */
+}
+/* source-sans-pro-300italic - latin */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: italic;
+  font-weight: 300;
+  src: url('/fonts/source-sans-pro-v21-latin-300italic.eot'); /* IE9 Compat Modes */
+  src: local(''),
+       url('/fonts/source-sans-pro-v21-latin-300italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/fonts/source-sans-pro-v21-latin-300italic.woff2') format('woff2'), /* Super Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-300italic.woff') format('woff'), /* Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-300italic.ttf') format('truetype'), /* Safari, Android, iOS */
+       url('/fonts/source-sans-pro-v21-latin-300italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
+}
+/* source-sans-pro-regular - latin */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: normal;
+  font-weight: 400;
+  src: url('/fonts/source-sans-pro-v21-latin-regular.eot'); /* IE9 Compat Modes */
+  src: local(''),
+       url('/fonts/source-sans-pro-v21-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/fonts/source-sans-pro-v21-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-regular.woff') format('woff'), /* Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
+       url('/fonts/source-sans-pro-v21-latin-regular.svg#SourceSansPro') format('svg'); /* Legacy iOS */
+}
+/* source-sans-pro-italic - latin */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: italic;
+  font-weight: 400;
+  src: url('/fonts/source-sans-pro-v21-latin-italic.eot'); /* IE9 Compat Modes */
+  src: local(''),
+       url('/fonts/source-sans-pro-v21-latin-italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/fonts/source-sans-pro-v21-latin-italic.woff2') format('woff2'), /* Super Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-italic.woff') format('woff'), /* Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-italic.ttf') format('truetype'), /* Safari, Android, iOS */
+       url('/fonts/source-sans-pro-v21-latin-italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
+}
+/* source-sans-pro-700 - latin */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: normal;
+  font-weight: 700;
+  src: url('/fonts/source-sans-pro-v21-latin-700.eot'); /* IE9 Compat Modes */
+  src: local(''),
+       url('/fonts/source-sans-pro-v21-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/fonts/source-sans-pro-v21-latin-700.woff2') format('woff2'), /* Super Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-700.woff') format('woff'), /* Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-700.ttf') format('truetype'), /* Safari, Android, iOS */
+       url('/fonts/source-sans-pro-v21-latin-700.svg#SourceSansPro') format('svg'); /* Legacy iOS */
+}
+/* source-sans-pro-700italic - latin */
+@font-face {
+  font-family: 'Source Sans Pro';
+  font-style: italic;
+  font-weight: 700;
+  src: url('/fonts/source-sans-pro-v21-latin-700italic.eot'); /* IE9 Compat Modes */
+  src: local(''),
+       url('/fonts/source-sans-pro-v21-latin-700italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+       url('/fonts/source-sans-pro-v21-latin-700italic.woff2') format('woff2'), /* Super Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-700italic.woff') format('woff'), /* Modern Browsers */
+       url('/fonts/source-sans-pro-v21-latin-700italic.ttf') format('truetype'), /* Safari, Android, iOS */
+       url('/fonts/source-sans-pro-v21-latin-700italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
+}
+
 :root {
 :root {
   --bs-blue: #158cba;
   --bs-blue: #158cba;
   --bs-indigo: #6610f2;
   --bs-indigo: #6610f2;

+ 8 - 0
data/web/css/themes/mailcow-darkmode.css

@@ -358,3 +358,11 @@ table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
     background: #333;
     background: #333;
 }
 }
 
 
+span.mail-address-item {
+    background-color: #333;
+    border-radius: 4px;
+    border: 1px solid #555;
+    padding: 2px 7px;
+    display: inline-block;
+    margin: 2px 6px 2px 0;
+}

+ 1 - 0
data/web/debug.php

@@ -65,6 +65,7 @@ $template_data = [
   'solr_uptime' => round($solr_status['status']['dovecot-fts']['uptime'] / 1000 / 60 / 60),
   'solr_uptime' => round($solr_status['status']['dovecot-fts']['uptime'] / 1000 / 60 / 60),
   'clamd_status' => $clamd_status,
   'clamd_status' => $clamd_status,
   'containers' => $containers,
   'containers' => $containers,
+  'ip_check' => customize('get', 'ip_check'),
   'lang_admin' => json_encode($lang['admin']),
   'lang_admin' => json_encode($lang['admin']),
   'lang_debug' => json_encode($lang['debug']),
   'lang_debug' => json_encode($lang['debug']),
   'lang_datatables' => json_encode($lang['datatables']),
   'lang_datatables' => json_encode($lang['datatables']),

BIN
data/web/fonts/source-sans-pro-v21-latin-300.woff


BIN
data/web/fonts/source-sans-pro-v21-latin-300.woff2


BIN
data/web/fonts/source-sans-pro-v21-latin-300italic.woff


BIN
data/web/fonts/source-sans-pro-v21-latin-300italic.woff2


BIN
data/web/fonts/source-sans-pro-v21-latin-700.woff


BIN
data/web/fonts/source-sans-pro-v21-latin-700.woff2


BIN
data/web/fonts/source-sans-pro-v21-latin-700italic.woff


BIN
data/web/fonts/source-sans-pro-v21-latin-700italic.woff2


BIN
data/web/fonts/source-sans-pro-v21-latin-italic.woff


BIN
data/web/fonts/source-sans-pro-v21-latin-italic.woff2


BIN
data/web/fonts/source-sans-pro-v21-latin-regular.woff


BIN
data/web/fonts/source-sans-pro-v21-latin-regular.woff2


+ 33 - 0
data/web/inc/functions.customize.inc.php

@@ -160,6 +160,25 @@ function customize($_action, $_item, $_data = null) {
             'msg' => 'ui_texts'
             'msg' => 'ui_texts'
           );
           );
         break;
         break;
+        case 'ip_check':
+          $ip_check = ($_data['ip_check_opt_in'] == "1") ? 1 : 0;
+          try {
+            $redis->set('IP_CHECK', $ip_check);
+          }
+          catch (RedisException $e) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_item, $_data),
+              'msg' => array('redis_error', $e)
+            );
+            return false;
+          }
+          $_SESSION['return'][] = array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_action, $_item, $_data),
+            'msg' => 'ip_check_opt_in_modified'
+          );
+        break;
       }
       }
     break;
     break;
     case 'delete':
     case 'delete':
@@ -276,6 +295,20 @@ function customize($_action, $_item, $_data = null) {
             return false;
             return false;
           }
           }
         break;
         break;
+        case 'ip_check':
+          try {
+            $ip_check = ($ip_check = $redis->get('IP_CHECK')) ? $ip_check : 0;
+            return $ip_check;
+          }
+          catch (RedisException $e) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_item, $_data),
+              'msg' => array('redis_error', $e)
+            );
+            return false;
+          }
+        break;
       }
       }
     break;
     break;
   }
   }

+ 47 - 46
data/web/inc/functions.mailbox.inc.php

@@ -2879,67 +2879,68 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 $_SESSION['return'][] = array(
                 $_SESSION['return'][] = array(
                   'type' => 'danger',
                   'type' => 'danger',
                   'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                   'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                  'msg' => 'access_denied'
+                  'msg' => 'extended_sender_acl_denied'
                 );
                 );
-                return false;
               }
               }
-              $extra_acls = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['extended_sender_acl']));
-              foreach ($extra_acls as $i => &$extra_acl) {
-                if (empty($extra_acl)) {
-                  continue;
-                }
-                if (substr($extra_acl, 0, 1) === "@") {
-                  $extra_acl = ltrim($extra_acl, '@');
-                }
-                if (!filter_var($extra_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name($extra_acl)) {
-                  $_SESSION['return'][] = array(
-                    'type' => 'danger',
-                    'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                    'msg' => array('extra_acl_invalid', htmlspecialchars($extra_acl))
-                  );
-                  unset($extra_acls[$i]);
-                  continue;
-                }
-                $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
-                if (filter_var($extra_acl, FILTER_VALIDATE_EMAIL)) {
-                  $extra_acl_domain = idn_to_ascii(substr(strstr($extra_acl, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
-                  if (in_array($extra_acl_domain, $domains)) {
-                    $_SESSION['return'][] = array(
-                      'type' => 'danger',
-                      'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                      'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
-                    );
-                    unset($extra_acls[$i]);
+              else {
+                $extra_acls = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['extended_sender_acl']));
+                foreach ($extra_acls as $i => &$extra_acl) {
+                  if (empty($extra_acl)) {
                     continue;
                     continue;
                   }
                   }
-                }
-                else {
-                  if (in_array($extra_acl, $domains)) {
+                  if (substr($extra_acl, 0, 1) === "@") {
+                    $extra_acl = ltrim($extra_acl, '@');
+                  }
+                  if (!filter_var($extra_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name($extra_acl)) {
                     $_SESSION['return'][] = array(
                     $_SESSION['return'][] = array(
                       'type' => 'danger',
                       'type' => 'danger',
                       'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                       'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                      'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
+                      'msg' => array('extra_acl_invalid', htmlspecialchars($extra_acl))
                     );
                     );
                     unset($extra_acls[$i]);
                     unset($extra_acls[$i]);
                     continue;
                     continue;
                   }
                   }
-                  $extra_acl = '@' . $extra_acl;
+                  $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
+                  if (filter_var($extra_acl, FILTER_VALIDATE_EMAIL)) {
+                    $extra_acl_domain = idn_to_ascii(substr(strstr($extra_acl, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
+                    if (in_array($extra_acl_domain, $domains)) {
+                      $_SESSION['return'][] = array(
+                        'type' => 'danger',
+                        'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                        'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
+                      );
+                      unset($extra_acls[$i]);
+                      continue;
+                    }
+                  }
+                  else {
+                    if (in_array($extra_acl, $domains)) {
+                      $_SESSION['return'][] = array(
+                        'type' => 'danger',
+                        'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                        'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
+                      );
+                      unset($extra_acls[$i]);
+                      continue;
+                    }
+                    $extra_acl = '@' . $extra_acl;
+                  }
                 }
                 }
-              }
-              $extra_acls = array_filter($extra_acls);
-              $extra_acls = array_values($extra_acls);
-              $extra_acls = array_unique($extra_acls);
-              $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `logged_in_as` = :username");
-              $stmt->execute(array(
-                ':username' => $username
-              ));
-              foreach ($extra_acls as $sender_acl_external) {
-                $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`, `external`)
-                  VALUES (:sender_acl, :username, 1)");
+                $extra_acls = array_filter($extra_acls);
+                $extra_acls = array_values($extra_acls);
+                $extra_acls = array_unique($extra_acls);
+                $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `logged_in_as` = :username");
                 $stmt->execute(array(
                 $stmt->execute(array(
-                  ':sender_acl' => $sender_acl_external,
                   ':username' => $username
                   ':username' => $username
                 ));
                 ));
+                foreach ($extra_acls as $sender_acl_external) {
+                  $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`, `external`)
+                    VALUES (:sender_acl, :username, 1)");
+                  $stmt->execute(array(
+                    ':sender_acl' => $sender_acl_external,
+                    ':username' => $username
+                  ));
+                }
               }
               }
             }
             }
             if (isset($_data['sender_acl'])) {
             if (isset($_data['sender_acl'])) {

+ 10 - 2
data/web/js/build/013-mailcow.js

@@ -12,14 +12,22 @@ $(document).ready(function() {
     $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
     $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
   }
   }
 
 
-  $(".generate_password").click(function( event ) {
+  $(".generate_password").click(async function( event ) {   
+    try { 
+      var password_policy = await window.fetch("/api/v1/get/passwordpolicy", { method:'GET', cache:'no-cache' });
+      var password_policy = await password_policy.json();
+      random_passwd_length = password_policy.length;
+    } catch(err) {
+      var random_passwd_length = 8;
+    }
+
     event.preventDefault();
     event.preventDefault();
     $('[data-hibp]').trigger('input');
     $('[data-hibp]').trigger('input');
     if (typeof($(this).closest("form").data('pwgen-length')) == "number") {
     if (typeof($(this).closest("form").data('pwgen-length')) == "number") {
       var random_passwd = GPW.pronounceable($(this).closest("form").data('pwgen-length'))
       var random_passwd = GPW.pronounceable($(this).closest("form").data('pwgen-length'))
     }
     }
     else {
     else {
-      var random_passwd = GPW.pronounceable(8)
+      var random_passwd = GPW.pronounceable(random_passwd_length)
     }
     }
     $(this).closest("form").find('[data-pwgen-field]').attr('type', 'text');
     $(this).closest("form").find('[data-pwgen-field]').attr('type', 'text');
     $(this).closest("form").find('[data-pwgen-field]').val(random_passwd);
     $(this).closest("form").find('[data-pwgen-field]').val(random_passwd);

+ 30 - 0
data/web/js/site/admin.js

@@ -70,8 +70,13 @@ jQuery(function($){
     }
     }
 
 
     $('#domainadminstable').DataTable({
     $('#domainadminstable').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -143,8 +148,13 @@ jQuery(function($){
     }
     }
 
 
     $('#oauth2clientstable').DataTable({
     $('#oauth2clientstable').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -206,8 +216,13 @@ jQuery(function($){
     }
     }
 
 
     $('#adminstable').DataTable({
     $('#adminstable').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -272,8 +287,13 @@ jQuery(function($){
     }
     }
 
 
     $('#forwardinghoststable').DataTable({
     $('#forwardinghoststable').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -330,8 +350,13 @@ jQuery(function($){
     }
     }
 
 
     $('#relayhoststable').DataTable({
     $('#relayhoststable').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -402,8 +427,13 @@ jQuery(function($){
     }
     }
 
 
     $('#transportstable').DataTable({
     $('#transportstable').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",

+ 94 - 15
data/web/js/site/debug.js

@@ -51,7 +51,40 @@ $(document).ready(function() {
     showVersionModal("Version " + mailcow_info.version_tag, mailcow_info.version_tag);
     showVersionModal("Version " + mailcow_info.version_tag, mailcow_info.version_tag);
   })
   })
   // get public ips
   // get public ips
-  get_public_ips();
+  $("#host_show_ip").click(function(){  
+    $("#host_show_ip").find(".text").addClass("d-none");
+    $("#host_show_ip").find(".spinner-border").removeClass("d-none");
+
+    window.fetch("/api/v1/get/status/host/ip", { method:'GET', cache:'no-cache' }).then(function(response) {
+      return response.json();
+    }).then(function(data) {
+      console.log(data);
+
+      // display host ips
+      if (data.ipv4)
+        $("#host_ipv4").text(data.ipv4);
+      if (data.ipv6)
+        $("#host_ipv6").text(data.ipv6);
+
+      $("#host_show_ip").addClass("d-none");
+      $("#host_show_ip").find(".text").removeClass("d-none");
+      $("#host_show_ip").find(".spinner-border").addClass("d-none");
+      $("#host_ipv4").removeClass("d-none");
+      $("#host_ipv6").removeClass("d-none");
+      $("#host_ipv6").removeClass("text-danger");
+      $("#host_ipv4").addClass("d-block");
+      $("#host_ipv6").addClass("d-block");
+    }).catch(function(error){
+      console.log(error);
+      
+      $("#host_ipv6").removeClass("d-none");
+      $("#host_ipv6").addClass("d-block");
+      $("#host_ipv6").addClass("text-danger");
+      $("#host_ipv6").text(lang_debug.error_show_ip);
+      $("#host_show_ip").find(".text").removeClass("d-none");
+      $("#host_show_ip").find(".spinner-border").addClass("d-none");
+    });
+  });
   update_container_stats();
   update_container_stats();
 });
 });
 jQuery(function($){
 jQuery(function($){
@@ -86,8 +119,13 @@ jQuery(function($){
     }
     }
 
 
     $('#autodiscover_log').DataTable({
     $('#autodiscover_log').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -143,8 +181,13 @@ jQuery(function($){
     }
     }
 
 
     $('#postfix_log').DataTable({
     $('#postfix_log').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -185,8 +228,13 @@ jQuery(function($){
     }
     }
 
 
     $('#watchdog_log').DataTable({
     $('#watchdog_log').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -231,8 +279,13 @@ jQuery(function($){
     }
     }
 
 
     $('#api_log').DataTable({
     $('#api_log').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -284,8 +337,13 @@ jQuery(function($){
     }
     }
 
 
     $('#rl_log').DataTable({
     $('#rl_log').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -375,8 +433,13 @@ jQuery(function($){
     }
     }
 
 
     $('#ui_logs').DataTable({
     $('#ui_logs').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -446,8 +509,13 @@ jQuery(function($){
     }
     }
 
 
     $('#sasl_logs').DataTable({
     $('#sasl_logs').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -494,8 +562,13 @@ jQuery(function($){
     }
     }
 
 
     $('#acme_log').DataTable({
     $('#acme_log').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -531,8 +604,13 @@ jQuery(function($){
     }
     }
 
 
     $('#netfilter_log').DataTable({
     $('#netfilter_log').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -573,8 +651,13 @@ jQuery(function($){
     }
     }
 
 
     $('#sogo_log').DataTable({
     $('#sogo_log').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -615,8 +698,13 @@ jQuery(function($){
     }
     }
 
 
     $('#dovecot_log').DataTable({
     $('#dovecot_log').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -718,8 +806,13 @@ jQuery(function($){
     }
     }
 
 
     $('#rspamd_history').DataTable({
     $('#rspamd_history').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order: [[0, 'desc']],
       order: [[0, 'desc']],
       ajax: {
       ajax: {
@@ -1224,20 +1317,6 @@ function update_container_stats(timeout=5){
   // run again in n seconds
   // run again in n seconds
   setTimeout(update_container_stats, timeout * 1000);
   setTimeout(update_container_stats, timeout * 1000);
 }
 }
-// get public ips
-function get_public_ips(){
-  window.fetch("/api/v1/get/status/host/ip", {method:'GET',cache:'no-cache'}).then(function(response) {
-    return response.json();
-  }).then(function(data) {
-    console.log(data);
-
-    // display host ips
-    if (data.ipv4)
-      $("#host_ipv4").text(data.ipv4);
-    if (data.ipv6)
-      $("#host_ipv6").text(data.ipv6);
-  });
-}
 // format hosts uptime seconds to readable string
 // format hosts uptime seconds to readable string
 function formatUptime(seconds){
 function formatUptime(seconds){
   seconds = Number(seconds);
   seconds = Number(seconds);

+ 10 - 0
data/web/js/site/edit.js

@@ -78,8 +78,13 @@ jQuery(function($){
   }
   }
   function draw_wl_policy_domain_table() {
   function draw_wl_policy_domain_table() {
     $('#wl_policy_domain_table').DataTable({
     $('#wl_policy_domain_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -133,8 +138,13 @@ jQuery(function($){
   }
   }
   function draw_bl_policy_domain_table() {
   function draw_bl_policy_domain_table() {
     $('#bl_policy_domain_table').DataTable({
     $('#bl_policy_domain_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",

+ 77 - 11
data/web/js/site/mailbox.js

@@ -433,8 +433,13 @@ jQuery(function($){
     }
     }
 
 
     var table = $('#domain_table').DataTable({
     var table = $('#domain_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -619,9 +624,13 @@ jQuery(function($){
     }
     }
 
 
     $('#templates_domain_table').DataTable({
     $('#templates_domain_table').DataTable({
-			responsive : true,
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order:[[2, 'desc']],
       order:[[2, 'desc']],
       ajax: {
       ajax: {
@@ -817,16 +826,26 @@ jQuery(function($){
     }
     }
 
 
     $('#mailbox_table').DataTable({
     $('#mailbox_table').DataTable({
-			responsive : true,
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
         url: "/api/v1/get/mailbox/reduced",
         url: "/api/v1/get/mailbox/reduced",
         dataSrc: function(json){
         dataSrc: function(json){
           $.each(json, function (i, item) {
           $.each(json, function (i, item) {
-            item.quota = item.quota_used + "/" + item.quota;
+            item.quota = {
+              sortBy: item.quota_used,
+              value: item.quota
+            }
+            item.quota.value = (item.quota.value == 0 ? "∞" : humanFileSize(item.quota.value));
+            item.quota.value = humanFileSize(item.quota_used) + "/" + item.quota.value;
+
             item.max_quota_for_mbox = humanFileSize(item.max_quota_for_mbox);
             item.max_quota_for_mbox = humanFileSize(item.max_quota_for_mbox);
             item.last_mail_login = item.last_imap_login + '/' + item.last_pop3_login + '/' + item.last_smtp_login;
             item.last_mail_login = item.last_imap_login + '/' + item.last_pop3_login + '/' + item.last_smtp_login;
             /*
             /*
@@ -931,14 +950,10 @@ jQuery(function($){
           },
           },
           {
           {
             title: lang.domain_quota,
             title: lang.domain_quota,
-            data: 'quota',
+            data: 'quota.value',
             responsivePriority: 8,
             responsivePriority: 8,
-            defaultContent: '',
-            render: function (data, type) {
-              data = data.split("/");
-              var of_q = (data[1] == 0 ? "∞" : humanFileSize(data[1]));
-              return humanFileSize(data[0]) + " / " + of_q;
-            }
+            defaultContent: '',  
+            orderData: 23
           },
           },
           {
           {
             title: lang.last_mail_login,
             title: lang.last_mail_login,
@@ -1064,6 +1079,13 @@ jQuery(function($){
             responsivePriority: 6,
             responsivePriority: 6,
             defaultContent: ''
             defaultContent: ''
           },
           },
+          {
+            title: "",
+            data: 'quota.sortBy',
+            responsivePriority: 8,
+            defaultContent: '',
+            className: "d-none"
+          },
       ]
       ]
     });
     });
   }
   }
@@ -1075,9 +1097,13 @@ jQuery(function($){
     }
     }
 
 
     $('#templates_mbox_table').DataTable({
     $('#templates_mbox_table').DataTable({
-			responsive : true,
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order:[[2, 'desc']],
       order:[[2, 'desc']],
       ajax: {
       ajax: {
@@ -1287,8 +1313,13 @@ jQuery(function($){
     }
     }
 
 
     $('#resource_table').DataTable({
     $('#resource_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -1413,8 +1444,13 @@ jQuery(function($){
     }
     }
     
     
     $('#bcc_table').DataTable({
     $('#bcc_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order:[[2, 'desc']],
       order:[[2, 'desc']],
       ajax: {
       ajax: {
@@ -1510,8 +1546,13 @@ jQuery(function($){
     }
     }
 
 
     $('#recipient_map_table').DataTable({
     $('#recipient_map_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order:[[2, 'desc']],
       order:[[2, 'desc']],
       ajax: {
       ajax: {
@@ -1594,8 +1635,13 @@ jQuery(function($){
     }
     }
 
 
     $('#tls_policy_table').DataTable({
     $('#tls_policy_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order:[[2, 'desc']],
       order:[[2, 'desc']],
       ajax: {
       ajax: {
@@ -1688,8 +1734,13 @@ jQuery(function($){
     }
     }
 
 
     $('#alias_table').DataTable({
     $('#alias_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order:[[2, 'desc']],
       order:[[2, 'desc']],
       ajax: {
       ajax: {
@@ -1829,8 +1880,13 @@ jQuery(function($){
     }
     }
 
 
     $('#aliasdomain_table').DataTable({
     $('#aliasdomain_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -1911,8 +1967,13 @@ jQuery(function($){
     }
     }
 
 
     $('#sync_job_table').DataTable({
     $('#sync_job_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order:[[2, 'desc']],
       order:[[2, 'desc']],
       ajax: {
       ajax: {
@@ -2051,9 +2112,14 @@ jQuery(function($){
     }
     }
 
 
     var table = $('#filter_table').DataTable({
     var table = $('#filter_table').DataTable({
+			responsive: true,
       autoWidth: false,
       autoWidth: false,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       order:[[2, 'desc']],
       order:[[2, 'desc']],
       ajax: {
       ajax: {

+ 14 - 3
data/web/js/site/quarantine.js

@@ -14,8 +14,13 @@ jQuery(function($){
   });
   });
   function draw_quarantine_table() {
   function draw_quarantine_table() {
     $('#quarantinetable').DataTable({
     $('#quarantinetable').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -129,9 +134,15 @@ jQuery(function($){
             title: lang.received,
             title: lang.received,
             data: 'created',
             data: 'created',
             defaultContent: '',
             defaultContent: '',
-            render: function (data,type) {
-              var date = new Date(data ? data * 1000 : 0); 
-              return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
+            createdCell: function(td, cellData) {    
+              $(td).attr({
+                "data-order": cellData,
+                "data-sort": cellData
+              });
+              
+              var date = new Date(cellData ? cellData * 1000 : 0); 
+              var dateString = date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
+              $(td).html(dateString);
             }
             }
           },
           },
           {
           {

+ 5 - 0
data/web/js/site/queue.js

@@ -35,8 +35,13 @@ jQuery(function($){
     }
     }
 
 
     $('#queuetable').DataTable({
     $('#queuetable').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",

+ 25 - 0
data/web/js/site/user.js

@@ -135,8 +135,13 @@ jQuery(function($){
     }
     }
 
 
     $('#tla_table').DataTable({
     $('#tla_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -216,8 +221,13 @@ jQuery(function($){
     }
     }
 
 
     $('#sync_job_table').DataTable({
     $('#sync_job_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -366,8 +376,13 @@ jQuery(function($){
     }
     }
 
 
     $('#app_passwd_table').DataTable({
     $('#app_passwd_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -456,8 +471,13 @@ jQuery(function($){
     }
     }
 
 
     $('#wl_policy_mailbox_table').DataTable({
     $('#wl_policy_mailbox_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",
@@ -521,8 +541,13 @@ jQuery(function($){
     }
     }
 
 
     $('#bl_policy_mailbox_table').DataTable({
     $('#bl_policy_mailbox_table').DataTable({
+			responsive: true,
       processing: true,
       processing: true,
       serverSide: false,
       serverSide: false,
+      stateSave: true,
+      dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
+           "tr" +
+           "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
       language: lang_datatables,
       language: lang_datatables,
       ajax: {
       ajax: {
         type: "GET",
         type: "GET",

+ 16 - 3
data/web/json_api.php

@@ -561,6 +561,15 @@ if (isset($_GET['query'])) {
                   echo '{}';
                   echo '{}';
                 }
                 }
               break;
               break;
+              default:
+                $password_complexity_rules = password_complexity('get');
+                if ($password_complexity_rules !== false) {
+                  process_get_return($password_complexity_rules);
+                }
+                else {
+                  echo '{}';
+                }
+              break;
             }
             }
           break;
           break;
 
 
@@ -1544,14 +1553,15 @@ if (isset($_GET['query'])) {
                   } 
                   } 
                   else if ($extra == "ip") {
                   else if ($extra == "ip") {
                     // get public ips
                     // get public ips
+                    
                     $curl = curl_init();
                     $curl = curl_init();
-                    curl_setopt($curl, CURLOPT_URL, 'http://ipv4.mailcow.email');
                     curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
                     curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
                     curl_setopt($curl, CURLOPT_POST, 0);
                     curl_setopt($curl, CURLOPT_POST, 0);
+                    curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
+                    curl_setopt($curl, CURLOPT_TIMEOUT, 15);
+                    curl_setopt($curl, CURLOPT_URL, 'http://ipv4.mailcow.email');
                     $ipv4 = curl_exec($curl);
                     $ipv4 = curl_exec($curl);
                     curl_setopt($curl, CURLOPT_URL, 'http://ipv6.mailcow.email');
                     curl_setopt($curl, CURLOPT_URL, 'http://ipv6.mailcow.email');
-                    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
-                    curl_setopt($curl, CURLOPT_POST, 0);
                     $ipv6 = curl_exec($curl);
                     $ipv6 = curl_exec($curl);
                     $ips = array(
                     $ips = array(
                       "ipv4" => $ipv4,
                       "ipv4" => $ipv4,
@@ -1913,6 +1923,9 @@ if (isset($_GET['query'])) {
         case "ui_texts":
         case "ui_texts":
           process_edit_return(customize('edit', 'ui_texts', $attr));
           process_edit_return(customize('edit', 'ui_texts', $attr));
         break;
         break;
+        case "ip_check":
+          process_edit_return(customize('edit', 'ip_check', $attr));
+        break;
         case "self":
         case "self":
           if ($_SESSION['mailcow_cc_role'] == "domainadmin") {
           if ($_SESSION['mailcow_cc_role'] == "domainadmin") {
             process_edit_return(domain_admin('edit', $attr));
             process_edit_return(domain_admin('edit', $attr));

+ 7 - 0
data/web/lang/lang.de-de.json

@@ -204,6 +204,9 @@
         "include_exclude": "Ein- und Ausschlüsse",
         "include_exclude": "Ein- und Ausschlüsse",
         "include_exclude_info": "Ohne Auswahl werden <b>alle Mailboxen</b> adressiert.",
         "include_exclude_info": "Ohne Auswahl werden <b>alle Mailboxen</b> adressiert.",
         "includes": "Diese Empfänger einschließen",
         "includes": "Diese Empfänger einschließen",
+        "ip_check": "IP Check",
+        "ip_check_disabled": "IP check ist deaktiviert. Unter dem angegebenen Pfad kann es aktiviert werden<br> <strong>System > Konfiguration > Einstellungen > UI-Anpassung</strong>",
+        "ip_check_opt_in": "Opt-In für die Nutzung der Drittanbieter-Dienste <strong>ipv4.mailcow.email</strong> und <strong>ipv6.mailcow.email</strong> zur Auflösung externer IP-Adressen.",
         "is_mx_based": "MX-basiert",
         "is_mx_based": "MX-basiert",
         "last_applied": "Zuletzt angewendet",
         "last_applied": "Zuletzt angewendet",
         "license_info": "Eine Lizenz ist nicht erforderlich, hilft jedoch der Entwicklung mailcows.<br><a href=\"https://www.servercow.de/mailcow#sal\" target=\"_blank\" alt=\"SAL Bestellung\">Hier kann die mailcow-GUID registriert werden.</a> Alternativ ist <a href=\"https://www.servercow.de/mailcow#support\" target=\"_blank\" alt=\"SAL Bestellung\">die Bestellung von Support-Paketen möglich</a>.",
         "license_info": "Eine Lizenz ist nicht erforderlich, hilft jedoch der Entwicklung mailcows.<br><a href=\"https://www.servercow.de/mailcow#sal\" target=\"_blank\" alt=\"SAL Bestellung\">Hier kann die mailcow-GUID registriert werden.</a> Alternativ ist <a href=\"https://www.servercow.de/mailcow#support\" target=\"_blank\" alt=\"SAL Bestellung\">die Bestellung von Support-Paketen möglich</a>.",
@@ -363,6 +366,7 @@
         "domain_not_empty": "Domain %s ist nicht leer",
         "domain_not_empty": "Domain %s ist nicht leer",
         "domain_not_found": "Domain %s nicht gefunden",
         "domain_not_found": "Domain %s nicht gefunden",
         "domain_quota_m_in_use": "Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein",
         "domain_quota_m_in_use": "Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein",
+        "extended_sender_acl_denied": "Keine Rechte zum setzen von externen Absenderadressen",
         "extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig",
         "extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig",
         "extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain",
         "extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain",
         "fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s",
         "fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s",
@@ -494,6 +498,7 @@
         "current_time": "Systemzeit",
         "current_time": "Systemzeit",
         "disk_usage": "Festplattennutzung",
         "disk_usage": "Festplattennutzung",
         "docs": "Dokumente",
         "docs": "Dokumente",
+        "error_show_ip": "konnte die öffentlichen IP Adressen nicht auflösen",
         "external_logs": "Externe Logs",
         "external_logs": "Externe Logs",
         "history_all_servers": "History (alle Server)",
         "history_all_servers": "History (alle Server)",
         "in_memory_logs": "In-memory Logs",
         "in_memory_logs": "In-memory Logs",
@@ -506,6 +511,7 @@
         "online_users": "Benutzer online",
         "online_users": "Benutzer online",
         "restart_container": "Neustart",
         "restart_container": "Neustart",
         "service": "Dienst",
         "service": "Dienst",
+        "show_ip": "Zeige öffentliche IP",
         "size": "Größe",
         "size": "Größe",
         "solr_dead": "Solr startet, ist deaktiviert oder temporär nicht erreichbar.",
         "solr_dead": "Solr startet, ist deaktiviert oder temporär nicht erreichbar.",
         "solr_status": "Solr Status",
         "solr_status": "Solr Status",
@@ -1001,6 +1007,7 @@
         "forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt",
         "forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt",
         "global_filter_written": "Filterdatei wurde erfolgreich geschrieben",
         "global_filter_written": "Filterdatei wurde erfolgreich geschrieben",
         "hash_deleted": "Hash wurde gelöscht",
         "hash_deleted": "Hash wurde gelöscht",
+        "ip_check_opt_in_modified": "IP Check wurde erfolgreich gespeichert",
         "item_deleted": "Objekt %s wurde entfernt",
         "item_deleted": "Objekt %s wurde entfernt",
         "item_released": "Objekt %s freigegeben",
         "item_released": "Objekt %s freigegeben",
         "items_deleted": "Objekt(e) %s wurde(n) erfolgreich entfernt",
         "items_deleted": "Objekt(e) %s wurde(n) erfolgreich entfernt",

+ 7 - 0
data/web/lang/lang.en-gb.json

@@ -206,6 +206,9 @@
         "include_exclude": "Include/Exclude",
         "include_exclude": "Include/Exclude",
         "include_exclude_info": "By default - with no selection - <b>all mailboxes</b> are addressed",
         "include_exclude_info": "By default - with no selection - <b>all mailboxes</b> are addressed",
         "includes": "Include these recipients",
         "includes": "Include these recipients",
+        "ip_check": "IP Check",
+        "ip_check_disabled": "IP check is disabled. You can enable it under<br> <strong>System > Configuration > Options > Customize</strong>",
+        "ip_check_opt_in": "Opt-In for using third party service <strong>ipv4.mailcow.email</strong> and <strong>ipv6.mailcow.email</strong> to resolve external IP addresses.",
         "is_mx_based": "MX based",
         "is_mx_based": "MX based",
         "last_applied": "Last applied",
         "last_applied": "Last applied",
         "license_info": "A license is not required but helps further development.<br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Register your GUID here</a> or <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Support order\">buy support for your mailcow installation.</a>",
         "license_info": "A license is not required but helps further development.<br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Register your GUID here</a> or <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Support order\">buy support for your mailcow installation.</a>",
@@ -363,6 +366,7 @@
         "domain_not_empty": "Cannot remove non-empty domain %s",
         "domain_not_empty": "Cannot remove non-empty domain %s",
         "domain_not_found": "Domain %s not found",
         "domain_not_found": "Domain %s not found",
         "domain_quota_m_in_use": "Domain quota must be greater or equal to %s MiB",
         "domain_quota_m_in_use": "Domain quota must be greater or equal to %s MiB",
+        "extended_sender_acl_denied": "missing ACL to set external sender addresses",
         "extra_acl_invalid": "External sender address \"%s\" is invalid",
         "extra_acl_invalid": "External sender address \"%s\" is invalid",
         "extra_acl_invalid_domain": "External sender \"%s\" uses an invalid domain",
         "extra_acl_invalid_domain": "External sender \"%s\" uses an invalid domain",
         "fido2_verification_failed": "FIDO2 verification failed: %s",
         "fido2_verification_failed": "FIDO2 verification failed: %s",
@@ -497,6 +501,7 @@
         "current_time": "System Time",
         "current_time": "System Time",
         "disk_usage": "Disk usage",
         "disk_usage": "Disk usage",
         "docs": "Docs",
         "docs": "Docs",
+        "error_show_ip": "Could not resolve the public IP addresses",
         "external_logs": "External logs",
         "external_logs": "External logs",
         "history_all_servers": "History (all servers)",
         "history_all_servers": "History (all servers)",
         "in_memory_logs": "In-memory logs",
         "in_memory_logs": "In-memory logs",
@@ -509,6 +514,7 @@
         "online_users": "Users online",
         "online_users": "Users online",
         "restart_container": "Restart",
         "restart_container": "Restart",
         "service": "Service",
         "service": "Service",
+        "show_ip": "Show public IP",
         "size": "Size",
         "size": "Size",
         "solr_dead": "Solr is starting, disabled or died.",
         "solr_dead": "Solr is starting, disabled or died.",
         "solr_status": "Solr status",
         "solr_status": "Solr status",
@@ -1013,6 +1019,7 @@
         "forwarding_host_removed": "Forwarding host %s has been removed",
         "forwarding_host_removed": "Forwarding host %s has been removed",
         "global_filter_written": "Filter was successfully written to file",
         "global_filter_written": "Filter was successfully written to file",
         "hash_deleted": "Hash deleted",
         "hash_deleted": "Hash deleted",
+        "ip_check_opt_in_modified": "IP check was saved successfully",
         "item_deleted": "Item %s successfully deleted",
         "item_deleted": "Item %s successfully deleted",
         "item_released": "Item %s released",
         "item_released": "Item %s released",
         "items_deleted": "Item %s successfully deleted",
         "items_deleted": "Item %s successfully deleted",

+ 14 - 0
data/web/templates/admin/tab-config-customize.twig

@@ -33,6 +33,20 @@
           </div>
           </div>
         </div>
         </div>
       {% endif %}
       {% endif %}
+      <legend style="padding-top:20px" unselectable="on">{{ lang.admin.ip_check }}</legend><hr />
+      <div id="ip_check">
+        <form class="form" data-id="ip_check" role="form" method="post">
+          <div class="mb-4">
+            <input class="form-check-input" type="checkbox" value="1" name="ip_check_opt_in" id="ip_check_opt_in" {% if ip_check == 1 %}checked{% endif %}>
+            <label class="form-check-label" for="ip_check_opt_in">
+              {{ lang.admin.ip_check_opt_in|raw }}
+            </label>
+          </div>
+          <p><div class="btn-group">
+            <button class="btn btn-sm btn-xs-half d-block d-sm-inline btn-success" data-action="edit_selected" data-item="admin" data-id="ip_check" data-reload="no" data-api-url='edit/ip_check' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
+          </div></p>
+        </form>
+      </div>
       <legend>{{ lang.admin.app_links }}</legend><hr />
       <legend>{{ lang.admin.app_links }}</legend><hr />
       <p class="text-muted">{{ lang.admin.merged_vars_hint|raw }}</p>
       <p class="text-muted">{{ lang.admin.merged_vars_hint|raw }}</p>
       <form class="form-inline" data-id="app_links" role="form" method="post">
       <form class="form-inline" data-id="app_links" role="form" method="post">

+ 2 - 2
data/web/templates/admin/tab-routing.twig

@@ -36,7 +36,7 @@
             </div>
             </div>
             <div class="mb-4">
             <div class="mb-4">
               <label for="rlyhost_password">{{ lang.admin.password }}</label>
               <label for="rlyhost_password">{{ lang.admin.password }}</label>
-              <input class="form-control" id="rlyhost_password" name="password">
+              <input class="form-control" id="rlyhost_password" name="password" type="password">
             </div>
             </div>
             <button class="btn btn-sm d-block d-sm-inline btn-success" data-action="add_item" data-id="rlyhost" data-api-url='add/relayhost' data-api-attr='{}' href="#"><i class="bi bi-plus-lg"></i> {{ lang.admin.add }}</button>
             <button class="btn btn-sm d-block d-sm-inline btn-success" data-action="add_item" data-id="rlyhost" data-api-url='add/relayhost' data-api-attr='{}' href="#"><i class="bi bi-plus-lg"></i> {{ lang.admin.add }}</button>
           </form>
           </form>
@@ -86,7 +86,7 @@
             </div>
             </div>
             <div class="mb-4">
             <div class="mb-4">
               <label for="transport_password">{{ lang.admin.password }}</label>
               <label for="transport_password">{{ lang.admin.password }}</label>
-              <input class="form-control" id="transport_password" name="password">
+              <input class="form-control" id="transport_password" name="password" type="password">
             </div>
             </div>
             <div class="mb-2">
             <div class="mb-2">
               <label>
               <label>

+ 12 - 2
data/web/templates/debug.twig

@@ -52,8 +52,18 @@
                       <tr>
                       <tr>
                         <td>IPs</td>
                         <td>IPs</td>
                         <td class="text-break">
                         <td class="text-break">
-                          <span class="d-block" id="host_ipv4">-</span>
-                          <span class="d-block" id="host_ipv6">-</span>
+                          {% if ip_check == 1 %}
+                            <span class="d-none" id="host_ipv4">-</span>
+                            <span class="d-none mb-2" id="host_ipv6">-</span>
+                            <button class="d-block btn btn-primary btn-sm" id="host_show_ip">
+                              <span class="text">{{ lang.debug.show_ip }}</span>
+                              <div class="spinner-border spinner-border-sm d-none" role="status">
+                                <span class="visually-hidden">Loading...</span>
+                              </div>  
+                            </button>
+                          {% else %}
+                            <span class="d-block">{{ lang.admin.ip_check_disabled|raw }}</span>
+                          {% endif %}
                         </td>
                         </td>
                       </tr>
                       </tr>
                       <tr>
                       <tr>

+ 4 - 2
data/web/templates/edit/mailbox.twig

@@ -200,8 +200,10 @@
           {% if sender_acl_handles.external_sender_aliases %}
           {% if sender_acl_handles.external_sender_aliases %}
             {% set ext_sender_acl = sender_acl_handles.external_sender_aliases|join(', ') %}
             {% set ext_sender_acl = sender_acl_handles.external_sender_aliases|join(', ') %}
           {% endif %}
           {% endif %}
-          <input type="text" class="form-control" name="extended_sender_acl" value="{{ ext_sender_acl }}" placeholder="user1@example.com, user2@example.org, @example.com, ...">
-          <small class="text-muted">{{ lang.edit.extended_sender_acl_info|raw }}</small>
+          {% if acl.extend_sender_acl and acl.extend_sender_acl == 1 %}
+            <input type="text" class="form-control" name="extended_sender_acl" value="{{ ext_sender_acl }}" placeholder="user1@example.com, user2@example.org, @example.com, ...">
+            <small class="text-muted">{{ lang.edit.extended_sender_acl_info|raw }}</small>
+          {% endif %}
         </div>
         </div>
       </div>
       </div>
       <div class="row">
       <div class="row">

+ 10 - 10
data/web/templates/mailbox.twig

@@ -4,18 +4,18 @@
 <div id="mail-content" class="responsive-tabs">
 <div id="mail-content" class="responsive-tabs">
   <ul class="nav nav-tabs" role="tablist">
   <ul class="nav nav-tabs" role="tablist">
     <li class="nav-item dropdown" role="presentation">
     <li class="nav-item dropdown" role="presentation">
-    <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.domains }}</a>
-    <ul class="dropdown-menu">
-      <li><button class="dropdown-item" aria-selected="false" aria-controls="tab-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domains">{{ lang.mailbox.domains }}</button></li>
-      <li><button class="dropdown-item {% if mailcow_cc_role != 'admin' %} d-none{% endif %}" aria-selected="false" aria-controls="tab-templates-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-templates-domains">{{ lang.mailbox.templates }}</button></li>
-    </ul>
+      <a class="nav-link dropdown-toggle active" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.domains }}</a>
+      <ul class="dropdown-menu">
+        <li><button class="dropdown-item" aria-selected="false" aria-controls="tab-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domains">{{ lang.mailbox.domains }}</button></li>
+        <li><button class="dropdown-item {% if mailcow_cc_role != 'admin' %} d-none{% endif %}" aria-selected="false" aria-controls="tab-templates-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-templates-domains">{{ lang.mailbox.templates }}</button></li>
+      </ul>
     </li>
     </li>
     <li class="nav-item dropdown" role="presentation">
     <li class="nav-item dropdown" role="presentation">
-    <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.mailboxes }}</a>
-    <ul class="dropdown-menu">
-      <li><button class="dropdown-item" aria-selected="false" aria-controls="tab-mailboxes" role="tab" data-bs-toggle="tab" data-bs-target="#tab-mailboxes">{{ lang.mailbox.mailboxes }}</button></li>
-      <li><button class="dropdown-item {% if mailcow_cc_role != 'admin' %} d-none{% endif %}" aria-selected="false" aria-controls="tab-templates-mbox" role="tab" data-bs-toggle="tab" data-bs-target="#tab-templates-mbox">{{ lang.mailbox.templates }}</button></li>
-    </ul>
+      <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.mailboxes }}</a>
+      <ul class="dropdown-menu">
+        <li><button class="dropdown-item" aria-selected="false" aria-controls="tab-mailboxes" role="tab" data-bs-toggle="tab" data-bs-target="#tab-mailboxes">{{ lang.mailbox.mailboxes }}</button></li>
+        <li><button class="dropdown-item {% if mailcow_cc_role != 'admin' %} d-none{% endif %}" aria-selected="false" aria-controls="tab-templates-mbox" role="tab" data-bs-toggle="tab" data-bs-target="#tab-templates-mbox">{{ lang.mailbox.templates }}</button></li>
+      </ul>
     </li>
     </li>
     <li class="nav-item" role="presentation"><button class="nav-link" aria-controls="tab-resources" role="tab" data-bs-toggle="tab" data-bs-target="#tab-resources">{{ lang.mailbox.resources }}</button></li>
     <li class="nav-item" role="presentation"><button class="nav-link" aria-controls="tab-resources" role="tab" data-bs-toggle="tab" data-bs-target="#tab-resources">{{ lang.mailbox.resources }}</button></li>
     <li class="nav-item dropdown">
     <li class="nav-item dropdown">