瀏覽代碼

[Web] Set pageLength to pagination_size + repect savedState...
Fix width in quarantine table.

realizelol 2 年之前
父節點
當前提交
6ff3f3f044

+ 104 - 102
data/web/css/site/quarantine.css

@@ -1,102 +1,104 @@
-.pagination a {
-  text-decoration: none !important;
-}
-
-.panel.panel-default {
-  overflow: visible !important;
-}
-
-.table-responsive {
-  overflow: visible !important;
-}
-
-.table-responsive {
-  overflow-x: scroll !important;
-}
-
-.footer-add-item {
-  display: block;
-  text-align: center;
-  font-style: italic;
-  padding: 10px;
-  background: #F5F5F5;
-}
-
-@media (min-width: 992px) {
-  .container {
-    width: 100%;
-  }
-}
-@media (min-width: 1920px) {
-  .container {
-      width: 80%;
-  }
-}
-
-.mass-actions-quarantine {
-  user-select: none;
-}
-
-.inputMissingAttr {
-  border-color: #FF4136;
-}
-
-.modal#qidDetailModal p {
-  word-break: break-all;
-}
-
-span#qid_detail_score {
-  font-weight: 700;
-  margin-left: 5px;
-}
-
-span.rspamd-symbol {
-  display: inline-block;
-  margin: 2px 6px 2px 0;
-  border-radius: 4px;
-  padding: 0 7px;
-}
-
-span.rspamd-symbol.positive {
-  background: #4CAF50;
-  border: 1px solid #4CAF50;
-  color: white;
-}
-
-span.rspamd-symbol.negative {
-  background: #ff4136;
-  border: 1px solid #ff4136;
-  color: white;
-}
-
-span.rspamd-symbol.neutral {
-  background: #f5f5f5;
-  color: #333;
-  border: 1px solid #ccc;
-}
-
-span.rspamd-symbol span.score {
-  font-weight: 700;
-}
-
-span.mail-address-item {
-  background-color: #f5f5f5;
-  border-radius: 4px;
-  border: 1px solid #ccc;
-  padding: 2px 7px;
-  display: inline-block;
-  margin: 2px 6px 2px 0;
-}
-
-table tbody tr {
-  cursor: pointer;
-}
-
-table tbody tr td input[type="checkbox"] {
-  cursor: pointer;
-}
-.label-rspamd-action {
-  font-size:110%;
-  margin:20px;
-}
-
+.pagination a {
+  text-decoration: none !important;
+}
+
+.panel.panel-default {
+  overflow: visible !important;
+}
+
+.table-responsive {
+  overflow: visible !important;
+}
+
+.table-responsive {
+  overflow-x: scroll !important;
+}
+
+.footer-add-item {
+  display: block;
+  text-align: center;
+  font-style: italic;
+  padding: 10px;
+  background: #F5F5F5;
+}
+
+@media (min-width: 992px) {
+  .container {
+    width: 100%;
+  }
+}
+@media (min-width: 1920px) {
+  .container {
+      width: 80%;
+  }
+}
+
+.mass-actions-quarantine {
+  user-select: none;
+}
+
+.inputMissingAttr {
+  border-color: #FF4136;
+}
+
+.modal#qidDetailModal p {
+  word-break: break-all;
+}
+
+span#qid_detail_score {
+  font-weight: 700;
+  margin-left: 5px;
+}
+
+span.rspamd-symbol {
+  display: inline-block;
+  margin: 2px 6px 2px 0;
+  border-radius: 4px;
+  padding: 0 7px;
+}
+
+span.rspamd-symbol.positive {
+  background: #4CAF50;
+  border: 1px solid #4CAF50;
+  color: white;
+}
+
+span.rspamd-symbol.negative {
+  background: #ff4136;
+  border: 1px solid #ff4136;
+  color: white;
+}
+
+span.rspamd-symbol.neutral {
+  background: #f5f5f5;
+  color: #333;
+  border: 1px solid #ccc;
+}
+
+span.rspamd-symbol span.score {
+  font-weight: 700;
+}
+
+span.mail-address-item {
+  background-color: #f5f5f5;
+  border-radius: 4px;
+  border: 1px solid #ccc;
+  padding: 2px 7px;
+  display: inline-block;
+  margin: 2px 6px 2px 0;
+}
+
+table tbody tr {
+  cursor: pointer;
+}
+
+table tbody tr td input[type="checkbox"] {
+  cursor: pointer;
+}
+.label-rspamd-action {
+  font-size:110%;
+  margin:20px;
+}
+.senders-mw220 {
+  max-width: 220px;
+}

+ 737 - 731
data/web/js/site/admin.js

@@ -1,731 +1,737 @@
-// Base64 functions
-var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
-jQuery(function($){
-  // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
-  var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
-  function jq(myid) {return "#" + myid.replace( /(:|\.|\[|\]|,|=|@)/g, "\\$1" );}
-  function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
-  function validateRegex(e){var t=e.split("/"),n=e,r="";t.length>1&&(n=t[1],r=t[2]);try{return new RegExp(n,r),!0}catch(e){return!1}}
-  function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
-  function hashCode(t){for(var n=0,r=0;r<t.length;r++)n=t.charCodeAt(r)+((n<<5)-n);return n}
-  function intToRGB(t){var n=(16777215&t).toString(16).toUpperCase();return"00000".substring(0,6-n.length)+n}
-  $("#dkim_missing_keys").on('click', function(e) {
-    e.preventDefault();
-     var domains = [];
-     $('.dkim_missing').each(function() {
-       domains.push($(this).val());
-     });
-     $('#dkim_add_domains').val(domains);
-  });
-  $(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
-  $("#mass_exclude").change(function(){ $("#mass_include").selectpicker('deselectAll'); });
-  $("#mass_include").change(function(){ $("#mass_exclude").selectpicker('deselectAll'); });
-  $("#mass_disarm").click(function() { $("#mass_send").attr("disabled", !this.checked); });
-  $(".admin-ays-dialog").click(function() { return confirm(lang.ays); });
-  $(".validate_rspamd_regex").click(function( event ) {
-    event.preventDefault();
-    var regex_map_id = $(this).data('regex-map');
-    var regex_data = $(jq(regex_map_id)).val().split(/\r?\n/);
-    var regex_valid = true;
-    for(var i = 0;i < regex_data.length;i++){
-      if(regex_data[i].startsWith('#') || !regex_data[i]){
-        continue;
-      }
-      if(!validateRegex(regex_data[i])) {
-        mailcow_alert_box('Cannot build regex from line ' + (i+1), 'danger');
-        var regex_valid = false;
-        break;
-      }
-      if(!regex_data[i].startsWith('/') || !/\/[ims]?$/.test(regex_data[i])){
-        mailcow_alert_box('Line ' + (i+1) + ' is invalid', 'danger');
-        var regex_valid = false;
-        break;
-      }
-    }
-    if (regex_valid) {
-      mailcow_alert_box('Regex OK', 'success');
-      $('button[data-id="' + regex_map_id + '"]').attr({"disabled": false});
-    }
-  });
-	$('.textarea-code').on('keyup', function() {
-    $('.submit_rspamd_regex').attr({"disabled": true});
-	});
-  $("#show_rspamd_global_filters").click(function() {
-    $.get("inc/ajax/show_rspamd_global_filters.php");
-    $("#confirm_show_rspamd_global_filters").hide();
-    $("#rspamd_global_filters").removeClass("d-none");
-  });
-  $("#super_delete").click(function() { return confirm(lang.queue_ays); });
-  
-  $(".refresh_table").on('click', function(e) {
-    e.preventDefault();
-    var table_name = $(this).data('table');
-    $('#' + table_name).DataTable().ajax.reload();
-  });
-  function draw_domain_admins() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#domainadminstable') ) {
-      $('#domainadminstable').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#domainadminstable').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/domain-admin/all",
-        dataSrc: function(data){
-          return process_table_data(data, 'domainadminstable');
-        }
-      },
-      columns: [
-          {
-            // placeholder, so checkbox will not block child row toggle
-            title: '',
-            data: null,
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: '',
-            data: 'chkbox',
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: lang.username,
-            data: 'username',
-            defaultContent: ''
-          },
-          {
-            title: lang.admin_domains,
-            data: 'selected_domains',
-            defaultContent: '',
-          },
-          {
-            title: "TFA",
-            data: 'tfa_active',
-            defaultContent: '',
-            render: function (data, type) {
-              if(data == 1) return '<i class="bi bi-check-lg"></i>';
-              else return '<i class="bi bi-x-lg"></i>'
-            }
-          },
-          {
-            title: lang.active,
-            data: 'active',
-            defaultContent: '',
-            render: function (data, type) {
-              if(data == 1) return '<i class="bi bi-check-lg"></i>';
-              else return '<i class="bi bi-x-lg"></i>'
-            }
-          },
-          {
-            title: lang.action,
-            data: 'action',
-            className: 'text-md-end dt-sm-head-hidden dt-body-right',
-            defaultContent: ''
-          },
-      ],
-      initComplete: function(settings, json){
-      }
-    });
-  }
-  function draw_oauth2_clients() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#oauth2clientstable') ) {
-      $('#oauth2clientstable').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#oauth2clientstable').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/oauth2-client/all",
-        dataSrc: function(data){
-          return process_table_data(data, 'oauth2clientstable');
-        }
-      },
-      columns: [
-          {
-            // placeholder, so checkbox will not block child row toggle
-            title: '',
-            data: null,
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: '',
-            data: 'chkbox',
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: 'ID',
-            data: 'id',
-            defaultContent: ''
-          },
-          {
-            title: lang.oauth2_client_id,
-            data: 'client_id',
-            defaultContent: ''
-          },
-          {
-            title: lang.oauth2_client_secret,
-            data: 'client_secret',
-            defaultContent: ''
-          },
-          {
-            title: lang.oauth2_redirect_uri,
-            data: 'redirect_uri',
-            defaultContent: ''
-          },
-          {
-            title: lang.action,
-            data: 'action',
-            className: 'text-md-end dt-sm-head-hidden dt-body-right',
-            defaultContent: ''
-          },
-      ]
-    });
-  }
-  function draw_admins() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#adminstable') ) {
-      $('#adminstable').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#adminstable').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/admin/all",
-        dataSrc: function(data){
-          return process_table_data(data, 'adminstable');
-        }
-      },
-      columns: [
-          {
-            // placeholder, so checkbox will not block child row toggle
-            title: '',
-            data: null,
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: '',
-            data: 'chkbox',
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: lang.username,
-            data: 'username',
-            defaultContent: ''
-          },
-          {
-            title: "TFA",
-            data: 'tfa_active',
-            defaultContent: '',
-            render: function (data, type) {
-              if(data == 1) return '<i class="bi bi-check-lg"></i>';
-              else return '<i class="bi bi-x-lg"></i>'
-            }
-          },
-          {
-            title: lang.active,
-            data: 'active',
-            defaultContent: '',
-            render: function (data, type) {
-              if(data == 1) return '<i class="bi bi-check-lg"></i>';
-              else return '<i class="bi bi-x-lg"></i>'
-            }
-          },
-          {
-            title: lang.action,
-            data: 'action',
-            defaultContent: '',
-            className: 'text-md-end dt-sm-head-hidden dt-body-right'
-          },
-      ]
-    });
-  }
-  function draw_fwd_hosts() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#forwardinghoststable') ) {
-      $('#forwardinghoststable').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#forwardinghoststable').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/fwdhost/all",
-        dataSrc: function(data){
-          return process_table_data(data, 'forwardinghoststable');
-        }
-      },
-      columns: [
-          {
-            // placeholder, so checkbox will not block child row toggle
-            title: '',
-            data: null,
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: '',
-            data: 'chkbox',
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: lang.host,
-            data: 'host',
-            defaultContent: ''
-          },
-          {
-            title: lang.source,
-            data: 'source',
-            defaultContent: ''
-          },
-          {
-            title: lang.spamfilter,
-            data: 'keep_spam',
-            defaultContent: '',
-            render: function(data, type){
-              return 'yes'==data?'<i class="bi bi-x-lg"></i>':'no'==data&&'<i class="bi bi-check-lg"></i>';
-            }
-          },
-          {
-            title: lang.action,
-            data: 'action',
-            className: 'text-md-end dt-sm-head-hidden dt-body-right',
-            defaultContent: ''
-          },
-      ]
-    });
-  }
-  function draw_relayhosts() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#relayhoststable') ) {
-      $('#relayhoststable').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#relayhoststable').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/relayhost/all",
-        dataSrc: function(data){
-          return process_table_data(data, 'relayhoststable');
-        }
-      },
-      columns: [
-          {
-            // placeholder, so checkbox will not block child row toggle
-            title: '',
-            data: null,
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: '',
-            data: 'chkbox',
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: 'ID',
-            data: 'id',
-            defaultContent: ''
-          },
-          {
-            title: lang.host,
-            data: 'hostname',
-            defaultContent: ''
-          },
-          {
-            title: lang.username,
-            data: 'username',
-            defaultContent: ''
-          },
-          {
-            title: lang.in_use_by,
-            data: 'in_use_by',
-            defaultContent: ''
-          },
-          {
-            title: lang.active,
-            data: 'active',
-            defaultContent: '',
-            render: function (data, type) {
-              if(data == 1) return '<i class="bi bi-check-lg"></i>';
-              else return '<i class="bi bi-x-lg"></i>'
-            }
-          },
-          {
-            title: lang.action,
-            data: 'action',
-            className: 'text-md-end dt-sm-head-hidden dt-body-right',
-            defaultContent: ''
-          },
-      ]
-    });
-  }
-  function draw_transport_maps() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#transportstable') ) {
-      $('#transportstable').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#transportstable').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/transport/all",
-        dataSrc: function(data){
-          return process_table_data(data, 'transportstable');
-        }
-      },
-      columns: [
-          {
-            // placeholder, so checkbox will not block child row toggle
-            title: '',
-            data: null,
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: '',
-            data: 'chkbox',
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: 'ID',
-            data: 'id',
-            defaultContent: ''
-          },
-          {
-            title: lang.destination,
-            data: 'destination',
-            defaultContent: ''
-          },
-          {
-            title: lang.nexthop,
-            data: 'nexthop',
-            defaultContent: ''
-          },
-          {
-            title: lang.username,
-            data: 'username',
-            defaultContent: ''
-          },
-          {
-            title: lang.active,
-            data: 'active',
-            defaultContent: '',
-            render: function (data, type) {
-              if(data == 1) return '<i class="bi bi-check-lg"></i>';
-              else return '<i class="bi bi-x-lg"></i>'
-            }
-          },
-          {
-            title: lang.action,
-            data: 'action',
-            className: 'text-md-end dt-sm-head-hidden dt-body-right',
-            defaultContent: ''
-          },
-      ]
-    });
-  }
-
-  function process_table_data(data, table) {
-    if (table == 'relayhoststable') {
-      $.each(data, function (i, item) {
-        item.action = '<div class="btn-group">' +
-          '<a href="#" data-bs-toggle="modal" data-bs-target="#testTransportModal" data-transport-id="' + encodeURI(item.id) + '" data-transport-type="sender-dependent" class="btn btn-xs btn-xs-third btn-secondary"><i class="bi bi-caret-right-fill"></i> Test</a>' +
-          '<a href="/edit/relayhost/' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-third btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
-          '<a href="#" data-action="delete_selected" data-id="single-rlyhost" data-api-url="delete/relayhost" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-third btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
-          '</div>';
-        if (item.used_by_mailboxes == '') { item.in_use_by = item.used_by_domains; }
-        else if (item.used_by_domains == '') { item.in_use_by = item.used_by_mailboxes; }
-        else { item.in_use_by = item.used_by_mailboxes + '<hr style="margin:5px 0px 5px 0px;">' + item.used_by_domains; }
-        item.chkbox = '<input type="checkbox" data-id="rlyhosts" name="multi_select" value="' + item.id + '" />';
-      });
-    } else if (table == 'transportstable') {
-      $.each(data, function (i, item) {
-        if (item.is_mx_based) {
-          item.destination = '<i class="bi bi-info-circle-fill text-info mx-info" data-bs-toggle="tooltip" title="' + lang.is_mx_based + '"></i> <code>' + item.destination + '</code>';
-        }
-        if (item.username) {
-          item.username = '<i style="color:#' + intToRGB(hashCode(item.nexthop)) + ';" class="bi bi-square-fill"></i> ' + item.username;
-        }
-        item.action = '<div class="btn-group">' +
-          '<a href="#" data-bs-toggle="modal" data-bs-target="#testTransportModal" data-transport-id="' + encodeURI(item.id) + '" data-transport-type="transport-map" class="btn btn-xs btn-xs-third btn-secondary"><i class="bi bi-caret-right-fill"></i> Test</a>' +
-          '<a href="/edit/transport/' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-third btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
-          '<a href="#" data-action="delete_selected" data-id="single-transport" data-api-url="delete/transport" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-third btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
-          '</div>';
-        item.chkbox = '<input type="checkbox" data-id="transports" name="multi_select" value="' + item.id + '" />';
-      });
-    } else if (table == 'queuetable') {
-      $.each(data, function (i, item) {
-        item.chkbox = '<input type="checkbox" data-id="mailqitems" name="multi_select" value="' + item.queue_id + '" />';
-        rcpts = $.map(item.recipients, function(i) {
-          return escapeHtml(i);
-        });
-        item.recipients = rcpts.join('<hr style="margin:1px!important">');
-        item.action = '<div class="btn-group">' +
-          '<a href="#" data-bs-toggle="modal" data-bs-target="#showQueuedMsg" data-queue-id="' + encodeURI(item.queue_id) + '" class="btn btn-xs btn-secondary">' + lang.queue_show_message + '</a>' +
-          '</div>';
-      });
-    } else if (table == 'forwardinghoststable') {
-      $.each(data, function (i, item) {
-        item.action = '<div class="btn-group">' +
-          '<a href="#" data-action="delete_selected" data-id="single-fwdhost" data-api-url="delete/fwdhost" data-item="' + encodeURI(item.host) + '" class="btn btn-xs btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
-          '</div>';
-        item.chkbox = '<input type="checkbox" data-id="fwdhosts" name="multi_select" value="' + item.host + '" />';
-      });
-    } else if (table == 'oauth2clientstable') {
-      $.each(data, function (i, item) {
-        item.action = '<div class="btn-group">' +
-          '<a href="/edit.php?oauth2client=' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
-          '<a href="#" data-action="delete_selected" data-id="single-oauth2-client" data-api-url="delete/oauth2-client" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
-          '</div>';
-        item.scope = "profile";
-        item.grant_types = 'refresh_token password authorization_code';
-        item.chkbox = '<input type="checkbox" data-id="oauth2_clients" name="multi_select" value="' + item.id + '" />';
-      });
-    } else if (table == 'domainadminstable') {
-      $.each(data, function (i, item) {
-        item.selected_domains = escapeHtml(item.selected_domains);
-        item.selected_domains = item.selected_domains.toString().replace(/,/g, "<br>");
-        item.chkbox = '<input type="checkbox" data-id="domain_admins" name="multi_select" value="' + item.username + '" />';
-        item.action = '<div class="btn-group">' +
-          '<a href="/edit/domainadmin/' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-third btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
-          '<a href="#" data-action="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-third btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
-          '<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-xs-third btn-success"><i class="bi bi-person-fill"></i> Login</a>' +
-          '</div>';
-      });
-    } else if (table == 'adminstable') {
-      $.each(data, function (i, item) {
-        if (admin_username.toLowerCase() == item.username.toLowerCase()) {
-          item.usr = '<i class="bi bi-person-check"></i> ' + item.username;
-        } else {
-          item.usr = item.username;
-        }
-        item.chkbox = '<input type="checkbox" data-id="admins" name="multi_select" value="' + item.username + '" />';
-        item.action = '<div class="btn-group">' +
-          '<a href="/edit/admin/' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
-          '<a href="#" data-action="delete_selected" data-id="single-admin" data-api-url="delete/admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
-          '</div>';
-      });
-    }
-    return data
-  };
-
-  // detect element visibility changes
-  function onVisible(element, callback) {
-    $(document).ready(function() {
-      element_object = document.querySelector(element);
-      if (element_object === null) return;
-
-      new IntersectionObserver((entries, observer) => {
-        entries.forEach(entry => {
-          if(entry.intersectionRatio > 0) {
-            callback(element_object);
-          }
-        });
-      }).observe(element_object);
-    });
-  }
-  // Draw Table if tab is active
-  onVisible("[id^=adminstable]", () => draw_admins());
-  onVisible("[id^=domainadminstable]", () => draw_domain_admins());
-  onVisible("[id^=oauth2clientstable]", () => draw_oauth2_clients());
-  onVisible("[id^=forwardinghoststable]", () => draw_fwd_hosts());
-  onVisible("[id^=relayhoststable]", () => draw_relayhosts());
-  onVisible("[id^=transportstable]", () => draw_transport_maps());
-
-
-  $('body').on('click', 'span.footable-toggle', function () {
-    event.stopPropagation();
-  })
-
-  // API IP check toggle
-  $("#skip_ip_check_ro").click(function( event ) {
-   $("#skip_ip_check_ro").not(this).prop('checked', false);
-    if ($("#skip_ip_check_ro:checked").length > 0) {
-      $('#allow_from_ro').prop('disabled', true);
-    }
-    else {
-      $("#allow_from_ro").removeAttr('disabled');
-    }
-  });
-  $("#skip_ip_check_rw").click(function( event ) {
-   $("#skip_ip_check_rw").not(this).prop('checked', false);
-    if ($("#skip_ip_check_rw:checked").length > 0) {
-      $('#allow_from_rw').prop('disabled', true);
-    }
-    else {
-      $("#allow_from_rw").removeAttr('disabled');
-    }
-  });
-  // Relayhost
-  $('#testRelayhostModal').on('show.bs.modal', function (e) {
-    $('#test_relayhost_result').text("-");
-    button = $(e.relatedTarget)
-    if (button != null) {
-      $('#relayhost_id').val(button.data('relayhost-id'));
-    }
-  })
-  $('#test_relayhost').on('click', function (e) {
-    e.preventDefault();
-    prev = $('#test_relayhost').text();
-    $(this).prop("disabled",true);
-    $(this).html('<i class="bi bi-arrow-repeat icon-spin"></i> ');
-    $.ajax({
-        type: 'GET',
-        url: 'inc/ajax/relay_check.php',
-        dataType: 'text',
-        data: $('#test_relayhost_form').serialize(),
-        complete: function (data) {
-          $('#test_relayhost_result').html(data.responseText);
-          $('#test_relayhost').prop("disabled",false);
-          $('#test_relayhost').text(prev);
-        }
-    });
-  })
-  // Transport
-  $('#testTransportModal').on('show.bs.modal', function (e) {
-    $('#test_transport_result').text("-");
-    button = $(e.relatedTarget)
-    if (button != null) {
-      $('#transport_id').val(button.data('transport-id'));
-      $('#transport_type').val(button.data('transport-type'));
-    }
-  })
-  $('#test_transport').on('click', function (e) {
-    e.preventDefault();
-    prev = $('#test_transport').text();
-    $(this).prop("disabled",true);
-    $(this).html('<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div> ');
-    $.ajax({
-        type: 'GET',
-        url: 'inc/ajax/transport_check.php',
-        dataType: 'text',
-        data: $('#test_transport_form').serialize(),
-        complete: function (data) {
-          $('#test_transport_result').html(data.responseText);
-          $('#test_transport').prop("disabled",false);
-          $('#test_transport').text(prev);
-        }
-    });
-  })
-  // DKIM private key modal
-  $('#showDKIMprivKey').on('show.bs.modal', function (e) {
-    $('#priv_key_pre').text("-");
-    p_related = $(e.relatedTarget)
-    if (p_related != null) {
-      var decoded_key = Base64.decode((p_related.data('priv-key')));
-      $('#priv_key_pre').text(decoded_key);
-    }
-  })
-  // FIDO2 friendly name modal
-  $('#fido2ChangeFn').on('show.bs.modal', function (e) {
-    rename_link = $(e.relatedTarget)
-    if (rename_link != null) {
-      $('#fido2_cid').val(rename_link.data('cid'));
-      $('#fido2_subject_desc').text(Base64.decode(rename_link.data('subject')));
-    }
-  })
-  // App links
-  function add_table_row(table_id, type) {
-    var row = $('<tr />');
-    if (type == "app_link") {
-      cols = '<td><input class="input-sm input-xs-lg form-control" data-id="app_links" type="text" name="app" required></td>';
-      cols += '<td><input class="input-sm input-xs-lg form-control" data-id="app_links" type="text" name="href" required></td>';
-      cols += '<td><a href="#" role="button" class="btn btn-sm btn-xs-lg btn-secondary h-100 w-100" type="button">' + lang.remove_row + '</a></td>';
-    } else if (type == "f2b_regex") {
-      cols = '<td><input style="text-align:center" class="input-sm input-xs-lg form-control" data-id="f2b_regex" type="text" value="+" disabled></td>';
-      cols += '<td><input class="input-sm input-xs-lg form-control regex-input" data-id="f2b_regex" type="text" name="regex" required></td>';
-      cols += '<td><a href="#" role="button" class="btn btn-sm btn-xs-lg btn-secondary h-100 w-100" type="button">' + lang.remove_row + '</a></td>';
-    }
-    row.append(cols);
-    table_id.append(row);
-  }
-  $('#app_link_table').on('click', 'tr a', function (e) {
-    e.preventDefault();
-    $(this).parents('tr').remove();
-  });
-  $('#f2b_regex_table').on('click', 'tr a', function (e) {
-    e.preventDefault();
-    $(this).parents('tr').remove();
-  });
-  $('#add_app_link_row').click(function() {
-      add_table_row($('#app_link_table'), "app_link");
-  });
-  $('#add_f2b_regex_row').click(function() {
-      add_table_row($('#f2b_regex_table'), "f2b_regex");
-  });
-});
+// Base64 functions
+var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
+jQuery(function($){
+  // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
+  var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
+  function jq(myid) {return "#" + myid.replace( /(:|\.|\[|\]|,|=|@)/g, "\\$1" );}
+  function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
+  function validateRegex(e){var t=e.split("/"),n=e,r="";t.length>1&&(n=t[1],r=t[2]);try{return new RegExp(n,r),!0}catch(e){return!1}}
+  function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
+  function hashCode(t){for(var n=0,r=0;r<t.length;r++)n=t.charCodeAt(r)+((n<<5)-n);return n}
+  function intToRGB(t){var n=(16777215&t).toString(16).toUpperCase();return"00000".substring(0,6-n.length)+n}
+  $("#dkim_missing_keys").on('click', function(e) {
+    e.preventDefault();
+     var domains = [];
+     $('.dkim_missing').each(function() {
+       domains.push($(this).val());
+     });
+     $('#dkim_add_domains').val(domains);
+  });
+  $(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
+  $("#mass_exclude").change(function(){ $("#mass_include").selectpicker('deselectAll'); });
+  $("#mass_include").change(function(){ $("#mass_exclude").selectpicker('deselectAll'); });
+  $("#mass_disarm").click(function() { $("#mass_send").attr("disabled", !this.checked); });
+  $(".admin-ays-dialog").click(function() { return confirm(lang.ays); });
+  $(".validate_rspamd_regex").click(function( event ) {
+    event.preventDefault();
+    var regex_map_id = $(this).data('regex-map');
+    var regex_data = $(jq(regex_map_id)).val().split(/\r?\n/);
+    var regex_valid = true;
+    for(var i = 0;i < regex_data.length;i++){
+      if(regex_data[i].startsWith('#') || !regex_data[i]){
+        continue;
+      }
+      if(!validateRegex(regex_data[i])) {
+        mailcow_alert_box('Cannot build regex from line ' + (i+1), 'danger');
+        var regex_valid = false;
+        break;
+      }
+      if(!regex_data[i].startsWith('/') || !/\/[ims]?$/.test(regex_data[i])){
+        mailcow_alert_box('Line ' + (i+1) + ' is invalid', 'danger');
+        var regex_valid = false;
+        break;
+      }
+    }
+    if (regex_valid) {
+      mailcow_alert_box('Regex OK', 'success');
+      $('button[data-id="' + regex_map_id + '"]').attr({"disabled": false});
+    }
+  });
+  $('.textarea-code').on('keyup', function() {
+    $('.submit_rspamd_regex').attr({"disabled": true});
+  });
+  $("#show_rspamd_global_filters").click(function() {
+    $.get("inc/ajax/show_rspamd_global_filters.php");
+    $("#confirm_show_rspamd_global_filters").hide();
+    $("#rspamd_global_filters").removeClass("d-none");
+  });
+  $("#super_delete").click(function() { return confirm(lang.queue_ays); });
+
+  $(".refresh_table").on('click', function(e) {
+    e.preventDefault();
+    var table_name = $(this).data('table');
+    $('#' + table_name).DataTable().ajax.reload();
+  });
+  function draw_domain_admins() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#domainadminstable') ) {
+      $('#domainadminstable').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#domainadminstable').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/domain-admin/all",
+        dataSrc: function(data){
+          return process_table_data(data, 'domainadminstable');
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: lang.username,
+          data: 'username',
+          defaultContent: ''
+        },
+        {
+          title: lang.admin_domains,
+          data: 'selected_domains',
+          defaultContent: '',
+        },
+        {
+          title: "TFA",
+          data: 'tfa_active',
+          defaultContent: '',
+            render: function (data, type) {
+            if(data == 1) return '<i class="bi bi-check-lg"></i>';
+            else return '<i class="bi bi-x-lg"></i>';
+          }
+        },
+        {
+          title: lang.active,
+          data: 'active',
+          defaultContent: '',
+          render: function (data, type) {
+            if(data == 1) return '<i class="bi bi-check-lg"></i>';
+            else return '<i class="bi bi-x-lg"></i>';
+          }
+        },
+        {
+          title: lang.action,
+          data: 'action',
+          className: 'text-md-end dt-sm-head-hidden dt-body-right',
+          defaultContent: ''
+        },
+      ],
+      initComplete: function(settings, json){
+      }
+    });
+  }
+  function draw_oauth2_clients() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#oauth2clientstable') ) {
+      $('#oauth2clientstable').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#oauth2clientstable').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/oauth2-client/all",
+        dataSrc: function(data){
+          return process_table_data(data, 'oauth2clientstable');
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: 'ID',
+          data: 'id',
+          defaultContent: ''
+        },
+        {
+          title: lang.oauth2_client_id,
+          data: 'client_id',
+          defaultContent: ''
+        },
+        {
+          title: lang.oauth2_client_secret,
+          data: 'client_secret',
+          defaultContent: ''
+        },
+        {
+          title: lang.oauth2_redirect_uri,
+          data: 'redirect_uri',
+          defaultContent: ''
+        },
+        {
+          title: lang.action,
+          data: 'action',
+          className: 'text-md-end dt-sm-head-hidden dt-body-right',
+          defaultContent: ''
+        },
+      ]
+    });
+  }
+  function draw_admins() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#adminstable') ) {
+      $('#adminstable').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#adminstable').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/admin/all",
+        dataSrc: function(data){
+          return process_table_data(data, 'adminstable');
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: lang.username,
+          data: 'username',
+          defaultContent: ''
+        },
+        {
+          title: "TFA",
+          data: 'tfa_active',
+          defaultContent: '',
+          render: function (data, type) {
+            if(data == 1) return '<i class="bi bi-check-lg"></i>';
+            else return '<i class="bi bi-x-lg"></i>';
+          }
+        },
+        {
+          title: lang.active,
+          data: 'active',
+          defaultContent: '',
+          render: function (data, type) {
+            if(data == 1) return '<i class="bi bi-check-lg"></i>';
+            else return '<i class="bi bi-x-lg"></i>';
+          }
+        },
+        {
+          title: lang.action,
+          data: 'action',
+          defaultContent: '',
+          className: 'text-md-end dt-sm-head-hidden dt-body-right'
+        },
+      ]
+    });
+  }
+  function draw_fwd_hosts() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#forwardinghoststable') ) {
+      $('#forwardinghoststable').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#forwardinghoststable').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/fwdhost/all",
+        dataSrc: function(data){
+          return process_table_data(data, 'forwardinghoststable');
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: lang.host,
+          data: 'host',
+          defaultContent: ''
+        },
+        {
+          title: lang.source,
+          data: 'source',
+          defaultContent: ''
+        },
+        {
+          title: lang.spamfilter,
+          data: 'keep_spam',
+          defaultContent: '',
+          render: function(data, type){
+            return 'yes'==data?'<i class="bi bi-x-lg"></i>':'no'==data&&'<i class="bi bi-check-lg"></i>';
+          }
+        },
+        {
+          title: lang.action,
+          data: 'action',
+          className: 'text-md-end dt-sm-head-hidden dt-body-right',
+          defaultContent: ''
+        },
+      ]
+    });
+  }
+  function draw_relayhosts() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#relayhoststable') ) {
+      $('#relayhoststable').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#relayhoststable').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/relayhost/all",
+        dataSrc: function(data){
+          return process_table_data(data, 'relayhoststable');
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: 'ID',
+          data: 'id',
+          defaultContent: ''
+        },
+        {
+          title: lang.host,
+          data: 'hostname',
+          defaultContent: ''
+        },
+        {
+          title: lang.username,
+          data: 'username',
+          defaultContent: ''
+        },
+        {
+          title: lang.in_use_by,
+          data: 'in_use_by',
+          defaultContent: ''
+        },
+        {
+          title: lang.active,
+          data: 'active',
+          defaultContent: '',
+          render: function (data, type) {
+            if(data == 1) return '<i class="bi bi-check-lg"></i>';
+            else return '<i class="bi bi-x-lg"></i>';
+          }
+        },
+        {
+          title: lang.action,
+          data: 'action',
+          className: 'text-md-end dt-sm-head-hidden dt-body-right',
+          defaultContent: ''
+        },
+      ]
+    });
+  }
+  function draw_transport_maps() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#transportstable') ) {
+      $('#transportstable').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#transportstable').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/transport/all",
+        dataSrc: function(data){
+          return process_table_data(data, 'transportstable');
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: 'ID',
+          data: 'id',
+          defaultContent: ''
+        },
+        {
+          title: lang.destination,
+          data: 'destination',
+          defaultContent: ''
+        },
+        {
+          title: lang.nexthop,
+          data: 'nexthop',
+          defaultContent: ''
+        },
+        {
+          title: lang.username,
+          data: 'username',
+          defaultContent: ''
+        },
+        {
+          title: lang.active,
+          data: 'active',
+          defaultContent: '',
+          render: function (data, type) {
+            if(data == 1) return '<i class="bi bi-check-lg"></i>';
+            else return '<i class="bi bi-x-lg"></i>';
+          }
+        },
+        {
+          title: lang.action,
+          data: 'action',
+          className: 'text-md-end dt-sm-head-hidden dt-body-right',
+          defaultContent: ''
+        },
+      ]
+    });
+  }
+
+  function process_table_data(data, table) {
+    if (table == 'relayhoststable') {
+      $.each(data, function (i, item) {
+        item.action = '<div class="btn-group">' +
+          '<a href="#" data-bs-toggle="modal" data-bs-target="#testTransportModal" data-transport-id="' + encodeURI(item.id) + '" data-transport-type="sender-dependent" class="btn btn-xs btn-xs-third btn-secondary"><i class="bi bi-caret-right-fill"></i> Test</a>' +
+          '<a href="/edit/relayhost/' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-third btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
+          '<a href="#" data-action="delete_selected" data-id="single-rlyhost" data-api-url="delete/relayhost" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-third btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+          '</div>';
+        if (item.used_by_mailboxes == '') { item.in_use_by = item.used_by_domains; }
+        else if (item.used_by_domains == '') { item.in_use_by = item.used_by_mailboxes; }
+        else { item.in_use_by = item.used_by_mailboxes + '<hr style="margin:5px 0px 5px 0px;">' + item.used_by_domains; }
+        item.chkbox = '<input type="checkbox" data-id="rlyhosts" name="multi_select" value="' + item.id + '" />';
+      });
+    } else if (table == 'transportstable') {
+      $.each(data, function (i, item) {
+        if (item.is_mx_based) {
+          item.destination = '<i class="bi bi-info-circle-fill text-info mx-info" data-bs-toggle="tooltip" title="' + lang.is_mx_based + '"></i> <code>' + item.destination + '</code>';
+        }
+        if (item.username) {
+          item.username = '<i style="color:#' + intToRGB(hashCode(item.nexthop)) + ';" class="bi bi-square-fill"></i> ' + item.username;
+        }
+        item.action = '<div class="btn-group">' +
+          '<a href="#" data-bs-toggle="modal" data-bs-target="#testTransportModal" data-transport-id="' + encodeURI(item.id) + '" data-transport-type="transport-map" class="btn btn-xs btn-xs-third btn-secondary"><i class="bi bi-caret-right-fill"></i> Test</a>' +
+          '<a href="/edit/transport/' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-third btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
+          '<a href="#" data-action="delete_selected" data-id="single-transport" data-api-url="delete/transport" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-third btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+          '</div>';
+        item.chkbox = '<input type="checkbox" data-id="transports" name="multi_select" value="' + item.id + '" />';
+      });
+    } else if (table == 'queuetable') {
+      $.each(data, function (i, item) {
+        item.chkbox = '<input type="checkbox" data-id="mailqitems" name="multi_select" value="' + item.queue_id + '" />';
+        rcpts = $.map(item.recipients, function(i) {
+          return escapeHtml(i);
+        });
+        item.recipients = rcpts.join('<hr style="margin:1px!important">');
+        item.action = '<div class="btn-group">' +
+          '<a href="#" data-bs-toggle="modal" data-bs-target="#showQueuedMsg" data-queue-id="' + encodeURI(item.queue_id) + '" class="btn btn-xs btn-secondary">' + lang.queue_show_message + '</a>' +
+          '</div>';
+      });
+    } else if (table == 'forwardinghoststable') {
+      $.each(data, function (i, item) {
+        item.action = '<div class="btn-group">' +
+          '<a href="#" data-action="delete_selected" data-id="single-fwdhost" data-api-url="delete/fwdhost" data-item="' + encodeURI(item.host) + '" class="btn btn-xs btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+          '</div>';
+        item.chkbox = '<input type="checkbox" data-id="fwdhosts" name="multi_select" value="' + item.host + '" />';
+      });
+    } else if (table == 'oauth2clientstable') {
+      $.each(data, function (i, item) {
+        item.action = '<div class="btn-group">' +
+          '<a href="/edit.php?oauth2client=' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
+          '<a href="#" data-action="delete_selected" data-id="single-oauth2-client" data-api-url="delete/oauth2-client" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+          '</div>';
+        item.scope = "profile";
+        item.grant_types = 'refresh_token password authorization_code';
+        item.chkbox = '<input type="checkbox" data-id="oauth2_clients" name="multi_select" value="' + item.id + '" />';
+      });
+    } else if (table == 'domainadminstable') {
+      $.each(data, function (i, item) {
+        item.selected_domains = escapeHtml(item.selected_domains);
+        item.selected_domains = item.selected_domains.toString().replace(/,/g, "<br>");
+        item.chkbox = '<input type="checkbox" data-id="domain_admins" name="multi_select" value="' + item.username + '" />';
+        item.action = '<div class="btn-group">' +
+          '<a href="/edit/domainadmin/' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-third btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
+          '<a href="#" data-action="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-third btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+          '<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-xs-third btn-success"><i class="bi bi-person-fill"></i> Login</a>' +
+          '</div>';
+      });
+    } else if (table == 'adminstable') {
+      $.each(data, function (i, item) {
+        if (admin_username.toLowerCase() == item.username.toLowerCase()) {
+          item.usr = '<i class="bi bi-person-check"></i> ' + item.username;
+        } else {
+          item.usr = item.username;
+        }
+        item.chkbox = '<input type="checkbox" data-id="admins" name="multi_select" value="' + item.username + '" />';
+        item.action = '<div class="btn-group">' +
+          '<a href="/edit/admin/' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
+          '<a href="#" data-action="delete_selected" data-id="single-admin" data-api-url="delete/admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+          '</div>';
+      });
+    }
+    return data
+  };
+
+  // detect element visibility changes
+  function onVisible(element, callback) {
+    $(document).ready(function() {
+      element_object = document.querySelector(element);
+      if (element_object === null) return;
+
+      new IntersectionObserver((entries, observer) => {
+        entries.forEach(entry => {
+          if(entry.intersectionRatio > 0) {
+            callback(element_object);
+          }
+        });
+      }).observe(element_object);
+    });
+  }
+  // Draw Table if tab is active
+  onVisible("[id^=adminstable]", () => draw_admins());
+  onVisible("[id^=domainadminstable]", () => draw_domain_admins());
+  onVisible("[id^=oauth2clientstable]", () => draw_oauth2_clients());
+  onVisible("[id^=forwardinghoststable]", () => draw_fwd_hosts());
+  onVisible("[id^=relayhoststable]", () => draw_relayhosts());
+  onVisible("[id^=transportstable]", () => draw_transport_maps());
+
+
+  $('body').on('click', 'span.footable-toggle', function () {
+    event.stopPropagation();
+  })
+
+  // API IP check toggle
+  $("#skip_ip_check_ro").click(function( event ) {
+   $("#skip_ip_check_ro").not(this).prop('checked', false);
+    if ($("#skip_ip_check_ro:checked").length > 0) {
+      $('#allow_from_ro').prop('disabled', true);
+    }
+    else {
+      $("#allow_from_ro").removeAttr('disabled');
+    }
+  });
+  $("#skip_ip_check_rw").click(function( event ) {
+   $("#skip_ip_check_rw").not(this).prop('checked', false);
+    if ($("#skip_ip_check_rw:checked").length > 0) {
+      $('#allow_from_rw').prop('disabled', true);
+    }
+    else {
+      $("#allow_from_rw").removeAttr('disabled');
+    }
+  });
+  // Relayhost
+  $('#testRelayhostModal').on('show.bs.modal', function (e) {
+    $('#test_relayhost_result').text("-");
+    button = $(e.relatedTarget)
+    if (button != null) {
+      $('#relayhost_id').val(button.data('relayhost-id'));
+    }
+  })
+  $('#test_relayhost').on('click', function (e) {
+    e.preventDefault();
+    prev = $('#test_relayhost').text();
+    $(this).prop("disabled",true);
+    $(this).html('<i class="bi bi-arrow-repeat icon-spin"></i> ');
+    $.ajax({
+      type: 'GET',
+      url: 'inc/ajax/relay_check.php',
+      dataType: 'text',
+      data: $('#test_relayhost_form').serialize(),
+      complete: function (data) {
+        $('#test_relayhost_result').html(data.responseText);
+        $('#test_relayhost').prop("disabled",false);
+        $('#test_relayhost').text(prev);
+      }
+    });
+  })
+  // Transport
+  $('#testTransportModal').on('show.bs.modal', function (e) {
+    $('#test_transport_result').text("-");
+    button = $(e.relatedTarget)
+    if (button != null) {
+      $('#transport_id').val(button.data('transport-id'));
+      $('#transport_type').val(button.data('transport-type'));
+    }
+  })
+  $('#test_transport').on('click', function (e) {
+    e.preventDefault();
+    prev = $('#test_transport').text();
+    $(this).prop("disabled",true);
+    $(this).html('<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div> ');
+    $.ajax({
+      type: 'GET',
+      url: 'inc/ajax/transport_check.php',
+      dataType: 'text',
+      data: $('#test_transport_form').serialize(),
+      complete: function (data) {
+        $('#test_transport_result').html(data.responseText);
+        $('#test_transport').prop("disabled",false);
+        $('#test_transport').text(prev);
+      }
+    });
+  })
+  // DKIM private key modal
+  $('#showDKIMprivKey').on('show.bs.modal', function (e) {
+    $('#priv_key_pre').text("-");
+    p_related = $(e.relatedTarget)
+    if (p_related != null) {
+      var decoded_key = Base64.decode((p_related.data('priv-key')));
+      $('#priv_key_pre').text(decoded_key);
+    }
+  })
+  // FIDO2 friendly name modal
+  $('#fido2ChangeFn').on('show.bs.modal', function (e) {
+    rename_link = $(e.relatedTarget)
+    if (rename_link != null) {
+      $('#fido2_cid').val(rename_link.data('cid'));
+      $('#fido2_subject_desc').text(Base64.decode(rename_link.data('subject')));
+    }
+  })
+  // App links
+  function add_table_row(table_id, type) {
+    var row = $('<tr />');
+    if (type == "app_link") {
+      cols = '<td><input class="input-sm input-xs-lg form-control" data-id="app_links" type="text" name="app" required></td>';
+      cols += '<td><input class="input-sm input-xs-lg form-control" data-id="app_links" type="text" name="href" required></td>';
+      cols += '<td><a href="#" role="button" class="btn btn-sm btn-xs-lg btn-secondary h-100 w-100" type="button">' + lang.remove_row + '</a></td>';
+    } else if (type == "f2b_regex") {
+      cols = '<td><input style="text-align:center" class="input-sm input-xs-lg form-control" data-id="f2b_regex" type="text" value="+" disabled></td>';
+      cols += '<td><input class="input-sm input-xs-lg form-control regex-input" data-id="f2b_regex" type="text" name="regex" required></td>';
+      cols += '<td><a href="#" role="button" class="btn btn-sm btn-xs-lg btn-secondary h-100 w-100" type="button">' + lang.remove_row + '</a></td>';
+    }
+    row.append(cols);
+    table_id.append(row);
+  }
+  $('#app_link_table').on('click', 'tr a', function (e) {
+    e.preventDefault();
+    $(this).parents('tr').remove();
+  });
+  $('#f2b_regex_table').on('click', 'tr a', function (e) {
+    e.preventDefault();
+    $(this).parents('tr').remove();
+  });
+  $('#add_app_link_row').click(function() {
+    add_table_row($('#app_link_table'), "app_link");
+  });
+  $('#add_f2b_regex_row').click(function() {
+    add_table_row($('#f2b_regex_table'), "f2b_regex");
+  });
+});

+ 78 - 68
data/web/js/site/debug.js

@@ -34,7 +34,7 @@ $(document).ready(function() {
   });
 
   // set update loop container list
-  containersToUpdate = {}
+  containersToUpdate = {};
   // set default ChartJs Font Color
   Chart.defaults.color = '#999';
   // create host cpu and mem charts
@@ -44,14 +44,13 @@ $(document).ready(function() {
     check_update(mailcow_info.version_tag, mailcow_info.project_url);
   }
   $("#maiclow_version").click(function(){
-    if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin" ||
-       mailcow_info.branch !== "master")
+    if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin" || mailcow_info.branch !== "master")
       return;
 
     showVersionModal("Version " + mailcow_info.version_tag, mailcow_info.version_tag);
   })
   // get public ips
-  $("#host_show_ip").click(function(){  
+  $("#host_show_ip").click(function(){
     $("#host_show_ip").find(".text").addClass("d-none");
     $("#host_show_ip").find(".spinner-border").removeClass("d-none");
 
@@ -76,7 +75,7 @@ $(document).ready(function() {
       $("#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");
@@ -119,10 +118,11 @@ jQuery(function($){
     }
 
     var table = $('#autodiscover_log').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -188,10 +188,11 @@ jQuery(function($){
     }
 
     var table = $('#postfix_log').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -242,10 +243,11 @@ jQuery(function($){
     }
 
     var table = $('#watchdog_log').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -300,10 +302,11 @@ jQuery(function($){
     }
 
     var table =  $('#api_log').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -352,7 +355,7 @@ jQuery(function($){
         }
       ]
     });
-    
+
     table.on('responsive-resize', function (e, datatable, columns){
       hideTableExpandCollapseBtn('#tab-api-logs', '#api_log');
     });
@@ -365,10 +368,11 @@ jQuery(function($){
     }
 
     var table = $('#rl_log').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -455,7 +459,7 @@ jQuery(function($){
         }
       ]
     });
-    
+
     table.on('responsive-resize', function (e, datatable, columns){
       hideTableExpandCollapseBtn('#tab-rl-logs', '#rl_log');
     });
@@ -468,10 +472,11 @@ jQuery(function($){
     }
 
     var table = $('#ui_logs').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -538,7 +543,7 @@ jQuery(function($){
         }
       ]
     });
-    
+
     table.on('responsive-resize', function (e, datatable, columns){
       hideTableExpandCollapseBtn('#tab-ui-logs', '#ui_log');
     });
@@ -551,10 +556,11 @@ jQuery(function($){
     }
 
     var table = $('#sasl_logs').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -598,7 +604,7 @@ jQuery(function($){
         }
       ]
     });
-    
+
     table.on('responsive-resize', function (e, datatable, columns){
       hideTableExpandCollapseBtn('#tab-sasl-logs', '#sasl_logs');
     });
@@ -611,10 +617,11 @@ jQuery(function($){
     }
 
     var table = $('#acme_log').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -647,7 +654,7 @@ jQuery(function($){
         }
       ]
     });
-    
+
     table.on('responsive-resize', function (e, datatable, columns){
       hideTableExpandCollapseBtn('#tab-acme-logs', '#acme_log');
     });
@@ -660,10 +667,11 @@ jQuery(function($){
     }
 
     var table = $('#netfilter_log').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -701,7 +709,7 @@ jQuery(function($){
         }
       ]
     });
-    
+
     table.on('responsive-resize', function (e, datatable, columns){
       hideTableExpandCollapseBtn('#tab-netfilter-logs', '#netfilter_log');
     });
@@ -714,10 +722,11 @@ jQuery(function($){
     }
 
     var table = $('#sogo_log').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -755,7 +764,7 @@ jQuery(function($){
         }
       ]
     });
-    
+
     table.on('responsive-resize', function (e, datatable, columns){
       hideTableExpandCollapseBtn('#tab-sogo-logs', '#sogo_log');
     });
@@ -768,10 +777,11 @@ jQuery(function($){
     }
 
     var table = $('#dovecot_log').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -883,10 +893,11 @@ jQuery(function($){
     }
 
     var table = $('#rspamd_history').DataTable({
-			responsive: true,
+      responsive: true,
       processing: true,
       serverSide: false,
       stateSave: true,
+      pageLength: log_pagination_size,
       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>>",
@@ -983,7 +994,7 @@ jQuery(function($){
         }
       ]
     });
-    
+
     table.on('responsive-resize', function (e, datatable, columns){
       hideTableExpandCollapseBtn('#tab-rspamd-history', '#rspamd_history');
     });
@@ -998,31 +1009,31 @@ jQuery(function($){
         item.rcpt = escapeHtml(item.rcpt_smtp.join(", "));
       }
       item.symbols = Object.keys(item.symbols).sort(function (a, b) {
-        if (item.symbols[a].score === 0) return 1
-        if (item.symbols[b].score === 0) return -1
+        if (item.symbols[a].score === 0) return 1;
+        if (item.symbols[b].score === 0) return -1;
         if (item.symbols[b].score < 0 && item.symbols[a].score < 0) {
-          return item.symbols[a].score - item.symbols[b].score
+          return item.symbols[a].score - item.symbols[b].score;
         }
         if (item.symbols[b].score > 0 && item.symbols[a].score > 0) {
-          return item.symbols[b].score - item.symbols[a].score
+          return item.symbols[b].score - item.symbols[a].score;
         }
-        return item.symbols[b].score - item.symbols[a].score
+        return item.symbols[b].score - item.symbols[a].score;
       }).map(function(key) {
         var sym = item.symbols[key];
         if (sym.score < 0) {
-          sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
+          sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)';
         }
         else if (sym.score === 0) {
-          sym.score_formatted = '(<span><b>' + sym.score + '</b></span>)'
+          sym.score_formatted = '(<span><b>' + sym.score + '</b></span>)';
         }
         else {
-          sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
+          sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)';
         }
         var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
         if (sym.options) {
           str += ' [' + escapeHtml(sym.options.join(", ")) + "]";
         }
-        return str
+        return str;
       }).join('<br>\n');
       item.subject = escapeHtml(item.subject);
       var scan_time = item.time_real.toFixed(3);
@@ -1155,14 +1166,14 @@ jQuery(function($){
         }
       });
     }
-    return data
+    return data;
   };
   $('.add_log_lines').on('click', function (e) {
     e.preventDefault();
-    var log_table= $(this).data("table")
-    var new_nrows = $(this).data("nrows")
-    var post_process = $(this).data("post-process")
-    var log_url = $(this).data("log-url")
+    var log_table= $(this).data("table");
+    var new_nrows = $(this).data("nrows");
+    var post_process = $(this).data("post-process");
+    var log_url = $(this).data("log-url");
     if (log_table === undefined || new_nrows === undefined || post_process === undefined || log_url === undefined) {
       console.log("no data-table or data-nrows or log_url or data-post-process attr found");
       return;
@@ -1184,9 +1195,9 @@ jQuery(function($){
   })
   function hideTableExpandCollapseBtn(tab, table){
     if ($(table).hasClass('collapsed'))
-      $(tab).find(".table_collapse_option").show(); 
+      $(tab).find(".table_collapse_option").show();
     else
-      $(tab).find(".table_collapse_option").hide(); 
+      $(tab).find(".table_collapse_option").hide();
   }
 
   // detect element visibility changes
@@ -1220,7 +1231,6 @@ jQuery(function($){
   onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph());
 
 
-
   // start polling host stats if tab is active
   onVisible("[id^=tab-containers]", () => update_stats());
   // start polling container stats if collapse is active
@@ -1303,9 +1313,9 @@ function update_stats(timeout=5){
       if (mem_chart.data.labels.length > 30) mem_chart.data.labels.shift();
 
       cpu_chart.data.datasets[0].data.push(data.cpu.usage);
-      if (cpu_chart.data.datasets[0].data.length > 30)  cpu_chart.data.datasets[0].data.shift();
+      if (cpu_chart.data.datasets[0].data.length > 30) cpu_chart.data.datasets[0].data.shift();
       mem_chart.data.datasets[0].data.push(data.memory.usage);
-      if (mem_chart.data.datasets[0].data.length > 30)  mem_chart.data.datasets[0].data.shift();
+      if (mem_chart.data.datasets[0].data.length > 30) mem_chart.data.datasets[0].data.shift();
 
       cpu_chart.update();
       mem_chart.update();
@@ -1464,23 +1474,23 @@ function createReadWriteChart(chart_id, read_lable, write_lable){
   };
   var optionsNet = {
     interaction: {
-        mode: 'index'
+      mode: 'index'
     },
     scales: {
       yAxis: {
         min: 0,
         grid: {
-            display: false
+          display: false
         },
         ticks: {
           callback: function(i, index, ticks) {
-             return formatBytes(i);
+            return formatBytes(i);
           }
         }
       },
       xAxis: {
         grid: {
-            display: false
+          display: false
         }
       }
     }
@@ -1528,13 +1538,13 @@ function createHostCpuAndMemChart(){
   };
   var optionsCpu = {
     interaction: {
-        mode: 'index'
+      mode: 'index'
     },
     scales: {
       yAxis: {
         min: 0,
         grid: {
-            display: false
+          display: false
         },
         ticks: {
           callback: function(i, index, ticks) {
@@ -1544,7 +1554,7 @@ function createHostCpuAndMemChart(){
       },
       xAxis: {
         grid: {
-            display: false
+          display: false
         }
       }
     }
@@ -1566,13 +1576,13 @@ function createHostCpuAndMemChart(){
   };
   var optionsMem = {
     interaction: {
-        mode: 'index'
+      mode: 'index'
     },
     scales: {
       yAxis: {
         min: 0,
         grid: {
-            display: false
+          display: false
         },
         ticks: {
           callback: function(i, index, ticks) {
@@ -1582,7 +1592,7 @@ function createHostCpuAndMemChart(){
       },
       xAxis: {
         grid: {
-            display: false
+          display: false
         }
       }
     }
@@ -1678,22 +1688,22 @@ function parseGithubMarkdownLinks(inputText) {
 
   replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
   replacedText = inputText.replace(replacePattern1, (matched, index, original, input_string) => {
-      if (matched.includes('github.com')){
-        // return short link if it's github link
-        last_uri_path = matched.split('/');
-        last_uri_path = last_uri_path[last_uri_path.length - 1];
-
-        // adjust Full Changelog link to match last git version and new git version, if link is a compare link
-        if (matched.includes('/compare/') && mailcow_info.last_version_tag !== ''){
-          matched = matched.replace(last_uri_path,  mailcow_info.last_version_tag + '...' + mailcow_info.version_tag);
-          last_uri_path = mailcow_info.last_version_tag + '...' + mailcow_info.version_tag;
-        }
+    if (matched.includes('github.com')){
+      // return short link if it's github link
+      last_uri_path = matched.split('/');
+      last_uri_path = last_uri_path[last_uri_path.length - 1];
+
+      // adjust Full Changelog link to match last git version and new git version, if link is a compare link
+      if (matched.includes('/compare/') && mailcow_info.last_version_tag !== ''){
+        matched = matched.replace(last_uri_path,  mailcow_info.last_version_tag + '...' + mailcow_info.version_tag);
+        last_uri_path = mailcow_info.last_version_tag + '...' + mailcow_info.version_tag;
+      }
 
-        return '<a href="' + matched + '" target="_blank">' + last_uri_path + '</a><br>';
-      };
+      return '<a href="' + matched + '" target="_blank">' + last_uri_path + '</a><br>';
+    };
 
-      // if it's not a github link, return complete link
-      return '<a href="' + matched + '" target="_blank">' + matched + '</a>';
+    // if it's not a github link, return complete link
+    return '<a href="' + matched + '" target="_blank">' + matched + '</a>';
   });
 
   return replacedText;

+ 222 - 220
data/web/js/site/edit.js

@@ -1,220 +1,222 @@
-$(document).ready(function() {
-  $(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
-  $("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
-  $(".goto_checkbox").click(function( event ) {
-   $("form[data-id='editalias'] .goto_checkbox").not(this).prop('checked', false);
-    if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
-      $('#textarea_alias_goto').prop('disabled', true);
-    }
-    else {
-      $("#textarea_alias_goto").removeAttr('disabled');
-    }
-  });
-  $("#disable_sender_check").click(function( event ) {
-    if ($("form[data-id='editmailbox'] #disable_sender_check:checked").length > 0) {
-      $('#editSelectSenderACL').prop('disabled', true);
-      $('#editSelectSenderACL').selectpicker('refresh');
-    }
-    else {
-      $('#editSelectSenderACL').prop('disabled', false);
-      $('#editSelectSenderACL').selectpicker('refresh');
-    }
-  });
-  if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
-    $('#textarea_alias_goto').prop('disabled', true);
-  }
-
-  $("#mailbox-password-warning-close").click(function( event ) {
-    $('#mailbox-passwd-hidden-info').addClass('hidden');
-    $('#mailbox-passwd-form-groups').removeClass('hidden');
-  });
-  // Sender ACL
-  if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
-    $("#sender_acl_disabled").show();
-  }
-  $('#editSelectSenderACL').change(function() {
-    if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
-      $("#sender_acl_disabled").show();
-    }
-    else {
-      $("#sender_acl_disabled").hide();
-    }
-  });
-  // Resources
-  if ($("#editSelectMultipleBookings").val() == "custom") {
-    $("#multiple_bookings_custom_div").show();
-    $('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
-  }
-  $("#editSelectMultipleBookings").change(function() {
-    $('input[name=multiple_bookings]').val($("#editSelectMultipleBookings").val());
-    if ($('input[name=multiple_bookings]').val() == "custom") {
-      $("#multiple_bookings_custom_div").show();
-    }
-    else {
-      $("#multiple_bookings_custom_div").hide();
-    }
-  });
-  $("#multiple_bookings_custom").bind("change keypress keyup blur", function() {
-    $('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
-  });
-
-  // load tags
-  if ($('#tags').length){
-    var tagsEl = $('#tags').parent().find('.tag-values')[0];
-    console.log($(tagsEl).val())
-    var tags = JSON.parse($(tagsEl).val());
-    $(tagsEl).val("");
-    
-    for (var i = 0; i < tags.length; i++)
-      addTag($('#tags'), tags[i]);
-  }
-});
-
-jQuery(function($){
-  // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
-  function validateEmail(email) {
-    var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-    return re.test(email);
-  }
-  function draw_wl_policy_domain_table() {
-    $('#wl_policy_domain_table').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: '/api/v1/get/policy_wl_domain/' + table_for_domain,
-        dataSrc: function(data){
-          $.each(data, function (i, item) {
-            if (!validateEmail(item.object)) {
-              item.chkbox = '<input type="checkbox" data-id="policy_wl_domain" name="multi_select" value="' + item.prefid + '" />';
-            }
-            else {
-              item.chkbox = '<input type="checkbox" disabled title="' + lang_user.spamfilter_table_domain_policy + '" />';
-            }
-          });
-
-          return data;
-        }
-      },
-      columns: [
-          {
-            // placeholder, so checkbox will not block child row toggle
-            title: '',
-            data: null,
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: '',
-            data: 'chkbox',
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: 'ID',
-            data: 'prefid',
-            defaultContent: ''
-          },
-          {
-            title: lang_user.spamfilter_table_rule,
-            data: 'value',
-            defaultContent: ''
-          },
-          {
-            title: 'Scope',
-            data: 'object',
-            defaultContent: ''
-          }
-      ]
-    });
-  }
-  function draw_bl_policy_domain_table() {
-    $('#bl_policy_domain_table').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: '/api/v1/get/policy_bl_domain/' + table_for_domain,
-        dataSrc: function(data){
-          $.each(data, function (i, item) {
-            if (!validateEmail(item.object)) {
-              item.chkbox = '<input type="checkbox" data-id="policy_bl_domain" name="multi_select" value="' + item.prefid + '" />';
-            }
-            else {
-              item.chkbox = '<input type="checkbox" disabled tooltip="' + lang_user.spamfilter_table_domain_policy + '" />';
-            }
-          });
-
-          return data;
-        }
-      },
-      columns: [
-          {
-            // placeholder, so checkbox will not block child row toggle
-            title: '',
-            data: null,
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: '',
-            data: 'chkbox',
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: 'ID',
-            data: 'prefid',
-            defaultContent: ''
-          },
-          {
-            title: lang_user.spamfilter_table_rule,
-            data: 'value',
-            defaultContent: ''
-          },
-          {
-            title: 'Scope',
-            data: 'object',
-            defaultContent: ''
-          }
-      ]
-    });
-  }
-
-  
-  // detect element visibility changes
-  function onVisible(element, callback) {
-    $(document).ready(function() {
-      element_object = document.querySelector(element);
-      if (element_object === null) return;
-
-      new IntersectionObserver((entries, observer) => {
-        entries.forEach(entry => {
-          if(entry.intersectionRatio > 0) {
-            callback(element_object);
-            observer.disconnect();
-          }
-        });
-      }).observe(element_object);
-    });
-  }
-  // Draw Table if tab is active
-  onVisible("[id^=wl_policy_domain_table]", () => draw_wl_policy_domain_table());
-  onVisible("[id^=bl_policy_domain_table]", () => draw_bl_policy_domain_table());
-});
+$(document).ready(function() {
+  $(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
+  $("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
+  $(".goto_checkbox").click(function( event ) {
+    $("form[data-id='editalias'] .goto_checkbox").not(this).prop('checked', false);
+    if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
+      $('#textarea_alias_goto').prop('disabled', true);
+    }
+    else {
+      $("#textarea_alias_goto").removeAttr('disabled');
+    }
+  });
+  $("#disable_sender_check").click(function( event ) {
+    if ($("form[data-id='editmailbox'] #disable_sender_check:checked").length > 0) {
+      $('#editSelectSenderACL').prop('disabled', true);
+      $('#editSelectSenderACL').selectpicker('refresh');
+    }
+    else {
+      $('#editSelectSenderACL').prop('disabled', false);
+      $('#editSelectSenderACL').selectpicker('refresh');
+    }
+  });
+  if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
+    $('#textarea_alias_goto').prop('disabled', true);
+  }
+
+  $("#mailbox-password-warning-close").click(function( event ) {
+    $('#mailbox-passwd-hidden-info').addClass('hidden');
+    $('#mailbox-passwd-form-groups').removeClass('hidden');
+  });
+  // Sender ACL
+  if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
+    $("#sender_acl_disabled").show();
+  }
+  $('#editSelectSenderACL').change(function() {
+    if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
+      $("#sender_acl_disabled").show();
+    }
+    else {
+      $("#sender_acl_disabled").hide();
+    }
+  });
+  // Resources
+  if ($("#editSelectMultipleBookings").val() == "custom") {
+    $("#multiple_bookings_custom_div").show();
+    $('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
+  }
+  $("#editSelectMultipleBookings").change(function() {
+    $('input[name=multiple_bookings]').val($("#editSelectMultipleBookings").val());
+    if ($('input[name=multiple_bookings]').val() == "custom") {
+      $("#multiple_bookings_custom_div").show();
+    }
+    else {
+      $("#multiple_bookings_custom_div").hide();
+    }
+  });
+  $("#multiple_bookings_custom").bind("change keypress keyup blur", function() {
+    $('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
+  });
+
+  // load tags
+  if ($('#tags').length){
+    var tagsEl = $('#tags').parent().find('.tag-values')[0];
+    console.log($(tagsEl).val())
+    var tags = JSON.parse($(tagsEl).val());
+    $(tagsEl).val("");
+
+    for (var i = 0; i < tags.length; i++)
+      addTag($('#tags'), tags[i]);
+  }
+});
+
+jQuery(function($){
+  // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
+  function validateEmail(email) {
+    var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+    return re.test(email);
+  }
+  function draw_wl_policy_domain_table() {
+    $('#wl_policy_domain_table').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: '/api/v1/get/policy_wl_domain/' + table_for_domain,
+        dataSrc: function(data){
+          $.each(data, function (i, item) {
+            if (!validateEmail(item.object)) {
+              item.chkbox = '<input type="checkbox" data-id="policy_wl_domain" name="multi_select" value="' + item.prefid + '" />';
+            }
+            else {
+              item.chkbox = '<input type="checkbox" disabled title="' + lang_user.spamfilter_table_domain_policy + '" />';
+            }
+          });
+
+          return data;
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: 'ID',
+          data: 'prefid',
+          defaultContent: ''
+        },
+        {
+          title: lang_user.spamfilter_table_rule,
+          data: 'value',
+          defaultContent: ''
+        },
+        {
+          title: 'Scope',
+          data: 'object',
+          defaultContent: ''
+        }
+      ]
+    });
+  }
+  function draw_bl_policy_domain_table() {
+    $('#bl_policy_domain_table').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: '/api/v1/get/policy_bl_domain/' + table_for_domain,
+        dataSrc: function(data){
+          $.each(data, function (i, item) {
+            if (!validateEmail(item.object)) {
+              item.chkbox = '<input type="checkbox" data-id="policy_bl_domain" name="multi_select" value="' + item.prefid + '" />';
+            }
+            else {
+              item.chkbox = '<input type="checkbox" disabled tooltip="' + lang_user.spamfilter_table_domain_policy + '" />';
+            }
+          });
+
+          return data;
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: 'ID',
+          data: 'prefid',
+          defaultContent: ''
+        },
+        {
+          title: lang_user.spamfilter_table_rule,
+          data: 'value',
+          defaultContent: ''
+        },
+        {
+          title: 'Scope',
+          data: 'object',
+          defaultContent: ''
+        }
+      ]
+    });
+  }
+
+
+  // detect element visibility changes
+  function onVisible(element, callback) {
+    $(document).ready(function() {
+      element_object = document.querySelector(element);
+      if (element_object === null) return;
+
+      new IntersectionObserver((entries, observer) => {
+        entries.forEach(entry => {
+          if(entry.intersectionRatio > 0) {
+            callback(element_object);
+            observer.disconnect();
+          }
+        });
+      }).observe(element_object);
+    });
+  }
+  // Draw Table if tab is active
+  onVisible("[id^=wl_policy_domain_table]", () => draw_wl_policy_domain_table());
+  onVisible("[id^=bl_policy_domain_table]", () => draw_bl_policy_domain_table());
+});

File diff suppressed because it is too large
+ 416 - 411
data/web/js/site/mailbox.js


+ 71 - 71
data/web/js/site/qhandler.js

@@ -1,71 +1,71 @@
-jQuery(function($){
-  var qitem = $('legend').data('hash');
-  var qError = $("#qid_error");
-  $.ajax({
-    url: '/inc/ajax/qitem_details.php',
-    data: { hash: qitem },
-    dataType: 'json',
-    success: function(data){
-      $('[data-id="qitems_single"]').each(function(index) {
-        $(this).attr("data-item", qitem);
-      });
-      $('#qid_detail_subj').text(data.subject);
-      $('#qid_detail_hfrom').text(data.header_from);
-      $('#qid_detail_efrom').text(data.env_from);
-      $('#qid_detail_score').html('');
-      $('#qid_detail_symbols').html('');
-      $('#qid_detail_recipients').html('');
-      $('#qid_detail_fuzzy').html('');
-      if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
-        $.each(data.fuzzy_hashes, function (index, value) {
-          $('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
-        });
-      } else {
-        $('#qid_detail_fuzzy').append('-');
-      }
-      if (typeof data.symbols !== 'undefined') {
-        data.symbols.sort(function (a, b) {
-          if (a.score === 0) return 1
-          if (b.score === 0) return -1
-          if (b.score < 0 && a.score < 0) {
-            return a.score - b.score
-          }
-          if (b.score > 0 && a.score > 0) {
-            return b.score - a.score
-          }
-          return b.score - a.score
-        })
-        $.each(data.symbols, function (index, value) {
-          var highlightClass = ''
-          if (value.score > 0) highlightClass = 'negative'
-          else if (value.score < 0) highlightClass = 'positive'
-          else highlightClass = 'neutral'
-          $('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
-        });
-        $('[data-bs-toggle="tooltip"]').tooltip()
-      }
-      if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
-        if (data.action === "add header") {
-          $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
-        } else if (data.action === "reject") {
-          $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
-        } else if (data.action === "rewrite subject") {
-          $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
-        }
-      }
-      if (typeof data.recipients !== 'undefined') {
-        $.each(data.recipients, function(index, value) {
-          var elem = $('<span class="mail-address-item"></span>');
-          elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
-          $('#qid_detail_recipients').append(elem);
-        });
-      }
-    },
-    error: function(data){
-      if (typeof data.error !== 'undefined') {
-        qError.text("Error loading quarantine item");
-        qError.show();
-      }
-    }
-  });
-});
+jQuery(function($){
+  var qitem = $('legend').data('hash');
+  var qError = $("#qid_error");
+  $.ajax({
+    url: '/inc/ajax/qitem_details.php',
+    data: { hash: qitem },
+    dataType: 'json',
+    success: function(data){
+      $('[data-id="qitems_single"]').each(function(index) {
+        $(this).attr("data-item", qitem);
+      });
+      $('#qid_detail_subj').text(data.subject);
+      $('#qid_detail_hfrom').text(data.header_from);
+      $('#qid_detail_efrom').text(data.env_from);
+      $('#qid_detail_score').html('');
+      $('#qid_detail_symbols').html('');
+      $('#qid_detail_recipients').html('');
+      $('#qid_detail_fuzzy').html('');
+      if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
+        $.each(data.fuzzy_hashes, function (index, value) {
+          $('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
+        });
+      } else {
+        $('#qid_detail_fuzzy').append('-');
+      }
+      if (typeof data.symbols !== 'undefined') {
+        data.symbols.sort(function (a, b) {
+          if (a.score === 0) return 1;
+          if (b.score === 0) return -1;
+          if (b.score < 0 && a.score < 0) {
+            return a.score - b.score;
+          }
+          if (b.score > 0 && a.score > 0) {
+            return b.score - a.score;
+          }
+          return b.score - a.score;
+        })
+        $.each(data.symbols, function (index, value) {
+          var highlightClass = '';
+          if (value.score > 0) highlightClass = 'negative';
+          else if (value.score < 0) highlightClass = 'positive';
+          else highlightClass = 'neutral';
+          $('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
+        });
+        $('[data-bs-toggle="tooltip"]').tooltip();
+      }
+      if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
+        if (data.action === "add header") {
+          $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
+        } else if (data.action === "reject") {
+          $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
+        } else if (data.action === "rewrite subject") {
+          $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
+        }
+      }
+      if (typeof data.recipients !== 'undefined') {
+        $.each(data.recipients, function(index, value) {
+          var elem = $('<span class="mail-address-item"></span>');
+          elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
+          $('#qid_detail_recipients').append(elem);
+        });
+      }
+    },
+    error: function(data){
+      if (typeof data.error !== 'undefined') {
+        qError.text("Error loading quarantine item");
+        qError.show();
+      }
+    }
+  });
+});

+ 297 - 286
data/web/js/site/quarantine.js

@@ -1,286 +1,297 @@
-// Base64 functions
-var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
-
-jQuery(function($){
-  acl_data = JSON.parse(acl);
-  // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
-  var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
-  function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
-  function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
-  $(".refresh_table").on('click', function(e) {
-    e.preventDefault();
-    var table_name = $(this).data('table');
-    $('#' + table_name).DataTable().ajax.reload();
-  });
-  function draw_quarantine_table() {
-    var table = $('#quarantinetable').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      initComplete: function(){
-        hideTableExpandCollapseBtn('#quarantinetable');
-      },
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/quarantine/all",
-        dataSrc: function(data){
-          $.each(data, function (i, item) {
-            if (item.subject === null) {
-              item.subject = '';
-            } else {
-              item.subject = escapeHtml(item.subject);
-            }
-            if (item.score === null) {
-              item.score = '-';
-            }
-            if (item.virus_flag > 0) {
-              item.virus = '<span class="badge fs-6 bg-danger">' + lang.high_danger + '</span>';
-            } else {
-              item.virus = '<span class="badge fs-6 bg-secondary">' + lang.neutral_danger + '</span>';
-            }
-            if (item.action === "reject") {
-              item.rspamdaction = '<span class="badge fs-6 bg-danger">' + lang.rejected + '</span>';
-            } else if (item.action === "add header") {
-              item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.junk_folder + '</span>';
-            } else if (item.action === "rewrite subject") {
-              item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.rewrite_subject + '</span>';
-            }
-            if(item.notified > 0) {
-              item.notified = '&#10004;';
-            } else {
-              item.notified = '&#10006;';
-            }
-            if (acl_data.login_as === 1) {
-            item.action = '<div class="btn-group">' +
-              '<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-info show_qid_info"><i class="bi bi-box-arrow-up-right"></i> ' + lang.show_item + '</a>' +
-              '<a href="#" data-action="delete_selected" data-id="del-single-qitem" data-api-url="delete/qitem" data-item="' + encodeURI(item.id) + '" class="btn btn-xs  btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
-              '</div>';
-            }
-            else {
-            item.action = '<div class="btn-group">' +
-              '<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-info show_qid_info"><i class="bi bi-file-earmark-text"></i> ' + lang.show_item + '</a>' +
-              '</div>';
-            }
-            item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />';
-          });
-
-          return data;
-        }
-      },
-      columns: [
-          {
-            // placeholder, so checkbox will not block child row toggle
-            title: '',
-            data: null,
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: '',
-            data: 'chkbox',
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: 'ID',
-            data: 'id',
-            defaultContent: ''
-          },
-          {
-            title: lang.qid,
-            data: 'qid',
-            defaultContent: ''
-          },
-          {
-            title: lang.sender,
-            data: 'sender',
-            defaultContent: ''
-          },
-          {
-            title: lang.subj,
-            data: 'subject',
-            defaultContent: ''
-          },
-          {
-            title: lang.rspamd_result,
-            data: 'rspamdaction',
-            defaultContent: ''
-          },
-          {
-            title: lang.rcpt,
-            data: 'rcpt',
-            defaultContent: ''
-          },
-          {
-            title: lang.danger,
-            data: 'virus',
-            defaultContent: ''
-          },
-          {
-            title: lang.spam_score,
-            data: 'score',
-            defaultContent: ''
-          },
-          {
-            title: lang.notified,
-            data: 'notified',
-            defaultContent: ''
-          },
-          {
-            title: lang.received,
-            data: 'created',
-            defaultContent: '',
-            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);
-            }
-          },
-          {
-            title: lang.action,
-            data: 'action',
-            className: 'text-md-end dt-sm-head-hidden dt-body-right',
-            defaultContent: ''
-          },
-      ]
-    });
-
-    table.on('responsive-resize', function (e, datatable, columns){
-      hideTableExpandCollapseBtn('#quarantinetable');
-    });
-  }
-
-  $('body').on('click', '.show_qid_info', function (e) {
-    e.preventDefault();
-    var qitem = $(this).attr('data-item');
-    var qError = $("#qid_error");
-
-    $('#qidDetailModal').modal('show');
-    qError.hide();
-
-    $.ajax({
-      url: '/inc/ajax/qitem_details.php',
-      data: { id: qitem },
-      dataType: 'json',
-      success: function(data){
-
-        $('[data-id="qitems_single"]').each(function(index) {
-          $(this).attr("data-item", qitem);
-        });
-
-        $("#quick_download_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&eml', '_blank')");
-        $("#quick_release_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_release', '_blank')");
-        $("#quick_delete_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_delete', '_blank')");
-
-        $('#qid_detail_subj').text(data.subject);
-        $('#qid_detail_hfrom').text(data.header_from);
-        $('#qid_detail_efrom').text(data.env_from);
-        $('#qid_detail_score').html('');
-        $('#qid_detail_recipients').html('');
-        $('#qid_detail_symbols').html('');
-        $('#qid_detail_fuzzy').html('');
-        if (typeof data.symbols !== 'undefined') {
-          data.symbols.sort(function (a, b) {
-            if (a.score === 0) return 1
-            if (b.score === 0) return -1
-            if (b.score < 0 && a.score < 0) {
-              return a.score - b.score
-            }
-            if (b.score > 0 && a.score > 0) {
-              return b.score - a.score
-            }
-            return b.score - a.score
-          })
-          $.each(data.symbols, function (index, value) {
-            var highlightClass = ''
-            if (value.score > 0) highlightClass = 'negative'
-            else if (value.score < 0) highlightClass = 'positive'
-            else highlightClass = 'neutral'
-            $('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
-          });
-          $('[data-bs-toggle="tooltip"]').tooltip()
-        }
-        if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
-          $.each(data.fuzzy_hashes, function (index, value) {
-            $('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
-          });
-        } else {
-          $('#qid_detail_fuzzy').append('-');
-        }
-        if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
-          if (data.action == "add header") {
-            $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
-          } else if (data.action == "reject") {
-            $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
-          } else if (data.action == "rewrite subject") {
-            $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
-          }
-        }
-        if (typeof data.recipients !== 'undefined') {
-          $.each(data.recipients, function(index, value) {
-            var elem = $('<span class="mail-address-item"></span>');
-            elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
-            $('#qid_detail_recipients').append(elem);
-          });
-        }
-        $('#qid_detail_text').text(data.text_plain);
-        $('#qid_detail_text_from_html').text(data.text_html);
-        var qAtts = $("#qid_detail_atts");
-        if (typeof data.attachments !== 'undefined') {
-          qAtts.text('');
-          $.each(data.attachments, function(index, value) {
-            qAtts.append(
-              '<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' +
-              ' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>'
-            );
-          });
-        }
-        else {
-          qAtts.text('-');
-        }
-      },
-      error: function(data){
-        if (typeof data.error !== 'undefined') {
-          $('#qid_detail_subj').text('-');
-          $('#qid_detail_hfrom').text('-');
-          $('#qid_detail_efrom').text('-');
-          $('#qid_detail_score').html('-');
-          $('#qid_detail_recipients').html('-');
-          $('#qid_detail_symbols').html('-');
-          $('#qid_detail_fuzzy').html('-');
-          $('#qid_detail_text').text('-');
-          $('#qid_detail_text_from_html').text('-');
-          qError.text("Error loading quarantine item");
-          qError.show();
-        }
-      }
-    });
-  });
-
-  $('body').on('click', 'span.footable-toggle', function () {
-    event.stopPropagation();
-  })
-
-  // Initial table drawings
-  draw_quarantine_table();
-
-  
-  function hideTableExpandCollapseBtn(table){
-    if ($(table).hasClass('collapsed'))
-      $(".table_collapse_option").show(); 
-    else
-      $(".table_collapse_option").hide(); 
-  }
-});
+// Base64 functions
+var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
+
+jQuery(function($){
+  acl_data = JSON.parse(acl);
+  // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
+  var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
+  function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
+  function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
+  $(".refresh_table").on('click', function(e) {
+    e.preventDefault();
+    var table_name = $(this).data('table');
+    $('#' + table_name).DataTable().ajax.reload();
+  });
+  function draw_quarantine_table() {
+    var table = $('#quarantinetable').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      order: [[2, 'desc']],
+      lengthMenu: [
+        [10, 25, 50, 100, -1],
+        [10, 25, 50, 100, 'all']
+      ],
+      pagingType: 'first_last_numbers',
+      aColumns: [
+        { sWidth: '8.25%' },
+        { sClass: 'classDataTable' }
+      ],
+      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,
+      initComplete: function(){
+        hideTableExpandCollapseBtn('#quarantinetable');
+      },
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/quarantine/all",
+        dataSrc: function(data){
+          $.each(data, function (i, item) {
+            if (item.subject === null) {
+              item.subject = '';
+            } else {
+              item.subject = escapeHtml(item.subject);
+            }
+            if (item.score === null) {
+              item.score = '-';
+            }
+            if (item.virus_flag > 0) {
+              item.virus = '<span class="badge fs-6 bg-danger">' + lang.high_danger + '</span>';
+            } else {
+              item.virus = '<span class="badge fs-6 bg-secondary">' + lang.neutral_danger + '</span>';
+            }
+            if (item.action === "reject") {
+              item.rspamdaction = '<span class="badge fs-6 bg-danger">' + lang.rejected + '</span>';
+            } else if (item.action === "add header") {
+              item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.junk_folder + '</span>';
+            } else if (item.action === "rewrite subject") {
+              item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.rewrite_subject + '</span>';
+            }
+            if(item.notified > 0) {
+              item.notified = '&#10004;';
+            } else {
+              item.notified = '&#10006;';
+            }
+            if (acl_data.login_as === 1) {
+            item.action = '<div class="btn-group">' +
+              '<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-info show_qid_info"><i class="bi bi-box-arrow-up-right"></i> ' + lang.show_item + '</a>' +
+              '<a href="#" data-action="delete_selected" data-id="del-single-qitem" data-api-url="delete/qitem" data-item="' + encodeURI(item.id) + '" class="btn btn-xs  btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+              '</div>';
+            }
+            else {
+            item.action = '<div class="btn-group">' +
+              '<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-info show_qid_info"><i class="bi bi-file-earmark-text"></i> ' + lang.show_item + '</a>' +
+              '</div>';
+            }
+            item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />';
+          });
+
+          return data;
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: 'ID',
+          data: 'id',
+          defaultContent: ''
+        },
+        {
+          title: lang.qid,
+          data: 'qid',
+          defaultContent: ''
+        },
+        {
+          title: lang.sender,
+          data: 'sender',
+          className: 'senders-mw220',
+          defaultContent: ''
+        },
+        {
+          title: lang.subj,
+          data: 'subject',
+          defaultContent: ''
+        },
+        {
+          title: lang.rspamd_result,
+          data: 'rspamdaction',
+          defaultContent: ''
+        },
+        {
+          title: lang.rcpt,
+          data: 'rcpt',
+          defaultContent: ''
+        },
+        {
+          title: lang.danger,
+          data: 'virus',
+          defaultContent: ''
+        },
+        {
+          title: lang.spam_score,
+          data: 'score',
+          defaultContent: ''
+        },
+        {
+          title: lang.notified,
+          data: 'notified',
+          defaultContent: ''
+        },
+        {
+          title: lang.received,
+          data: 'created',
+          defaultContent: '',
+          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);
+          }
+        },
+        {
+          title: lang.action,
+          data: 'action',
+          className: 'text-md-end dt-sm-head-hidden dt-body-right',
+          defaultContent: ''
+        },
+      ]
+    });
+
+    table.on('responsive-resize', function (e, datatable, columns){
+      hideTableExpandCollapseBtn('#quarantinetable');
+    });
+  }
+
+  $('body').on('click', '.show_qid_info', function (e) {
+    e.preventDefault();
+    var qitem = $(this).attr('data-item');
+    var qError = $("#qid_error");
+
+    $('#qidDetailModal').modal('show');
+    qError.hide();
+
+    $.ajax({
+      url: '/inc/ajax/qitem_details.php',
+      data: { id: qitem },
+      dataType: 'json',
+      success: function(data){
+
+        $('[data-id="qitems_single"]').each(function(index) {
+          $(this).attr("data-item", qitem);
+        });
+
+        $("#quick_download_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&eml', '_blank')");
+        $("#quick_release_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_release', '_blank')");
+        $("#quick_delete_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_delete', '_blank')");
+
+        $('#qid_detail_subj').text(data.subject);
+        $('#qid_detail_hfrom').text(data.header_from);
+        $('#qid_detail_efrom').text(data.env_from);
+        $('#qid_detail_score').html('');
+        $('#qid_detail_recipients').html('');
+        $('#qid_detail_symbols').html('');
+        $('#qid_detail_fuzzy').html('');
+        if (typeof data.symbols !== 'undefined') {
+          data.symbols.sort(function (a, b) {
+            if (a.score === 0) return 1;
+            if (b.score === 0) return -1;
+            if (b.score < 0 && a.score < 0) {
+              return a.score - b.score;
+            }
+            if (b.score > 0 && a.score > 0) {
+              return b.score - a.score;
+            }
+            return b.score - a.score;
+          })
+          $.each(data.symbols, function (index, value) {
+            var highlightClass = '';
+            if (value.score > 0) highlightClass = 'negative';
+            else if (value.score < 0) highlightClass = 'positive';
+            else highlightClass = 'neutral';
+            $('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
+          });
+          $('[data-bs-toggle="tooltip"]').tooltip();
+        }
+        if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
+          $.each(data.fuzzy_hashes, function (index, value) {
+            $('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
+          });
+        } else {
+          $('#qid_detail_fuzzy').append('-');
+        }
+        if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
+          if (data.action == "add header") {
+            $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
+          } else if (data.action == "reject") {
+            $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
+          } else if (data.action == "rewrite subject") {
+            $('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
+          }
+        }
+        if (typeof data.recipients !== 'undefined') {
+          $.each(data.recipients, function(index, value) {
+            var elem = $('<span class="mail-address-item"></span>');
+            elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
+            $('#qid_detail_recipients').append(elem);
+          });
+        }
+        $('#qid_detail_text').text(data.text_plain);
+        $('#qid_detail_text_from_html').text(data.text_html);
+        var qAtts = $("#qid_detail_atts");
+        if (typeof data.attachments !== 'undefined') {
+          qAtts.text('');
+          $.each(data.attachments, function(index, value) {
+            qAtts.append(
+              '<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' +
+              ' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>'
+            );
+          });
+        }
+        else {
+          qAtts.text('-');
+        }
+      },
+      error: function(data){
+        if (typeof data.error !== 'undefined') {
+          $('#qid_detail_subj').text('-');
+          $('#qid_detail_hfrom').text('-');
+          $('#qid_detail_efrom').text('-');
+          $('#qid_detail_score').html('-');
+          $('#qid_detail_recipients').html('-');
+          $('#qid_detail_symbols').html('-');
+          $('#qid_detail_fuzzy').html('-');
+          $('#qid_detail_text').text('-');
+          $('#qid_detail_text_from_html').text('-');
+          qError.text("Error loading quarantine item");
+          qError.show();
+        }
+      }
+    });
+  });
+
+  $('body').on('click', 'span.footable-toggle', function () {
+    event.stopPropagation();
+  })
+
+  // Initial table drawings
+  draw_quarantine_table();
+
+  function hideTableExpandCollapseBtn(table){
+    if ($(table).hasClass('collapsed'))
+      $(".table_collapse_option").show();
+    else
+      $(".table_collapse_option").hide();
+  }
+});

+ 108 - 107
data/web/js/site/queue.js

@@ -1,127 +1,128 @@
 jQuery(function($){
 
-    $(".refresh_table").on('click', function(e) {
-      e.preventDefault();
-      var table_name = $(this).data('table');
-      $('#' + table_name).DataTable().ajax.reload();
-    });
+  $(".refresh_table").on('click', function(e) {
+    e.preventDefault();
+    var table_name = $(this).data('table');
+    $('#' + table_name).DataTable().ajax.reload();
+  });
 
 
-    function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
+  function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
 
-    // Queue item
-    $('#showQueuedMsg').on('show.bs.modal', function (e) {
-      $('#queue_msg_content').text(lang.loading);
-      button = $(e.relatedTarget)
-      if (button != null) {
-        $('#queue_id').text(button.data('queue-id'));
+  // Queue item
+  $('#showQueuedMsg').on('show.bs.modal', function (e) {
+    $('#queue_msg_content').text(lang.loading);
+    button = $(e.relatedTarget)
+    if (button != null) {
+      $('#queue_id').text(button.data('queue-id'));
+    }
+    $.ajax({
+      type: 'GET',
+      url: '/api/v1/get/postcat/' + button.data('queue-id'),
+      dataType: 'text',
+      complete: function (data) {
+        $('#queue_msg_content').text(data.responseText);
       }
-      $.ajax({
-          type: 'GET',
-          url: '/api/v1/get/postcat/' + button.data('queue-id'),
-          dataType: 'text',
-          complete: function (data) {
-            $('#queue_msg_content').text(data.responseText);
-          }
-      });
-    })
+    });
+  })
 
-    function draw_queue() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#queuetable') ) {
-      $('#queuetable').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
+  function draw_queue() {
+  // just recalc width if instance already exists
+  if ($.fn.DataTable.isDataTable('#queuetable') ) {
+    $('#queuetable').DataTable().columns.adjust().responsive.recalc();
+    return;
+  }
 
-    $('#queuetable').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/mailq/all",
-        dataSrc: function(data){
-          $.each(data, function (i, item) {
-            item.chkbox = '<input type="checkbox" data-id="mailqitems" name="multi_select" value="' + item.queue_id + '" />';
-            rcpts = $.map(item.recipients, function(i) {
-              return escapeHtml(i);
-            });
-            item.recipients = rcpts.join('<hr style="margin:1px!important">');
-            item.action = '<div class="btn-group">' +
-              '<a href="#" data-bs-toggle="modal" data-bs-target="#showQueuedMsg" data-queue-id="' + encodeURI(item.queue_id) + '" class="btn btn-xs btn-secondary">' + lang.show_message + '</a>' +
+  $('#queuetable').DataTable({
+    responsive: true,
+    processing: true,
+    serverSide: false,
+    stateSave: true,
+    pageLength: pagination_size,
+    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,
+    ajax: {
+      type: "GET",
+      url: "/api/v1/get/mailq/all",
+      dataSrc: function(data){
+        $.each(data, function (i, item) {
+          item.chkbox = '<input type="checkbox" data-id="mailqitems" name="multi_select" value="' + item.queue_id + '" />';
+          rcpts = $.map(item.recipients, function(i) {
+            return escapeHtml(i);
+          });
+          item.recipients = rcpts.join('<hr style="margin:1px!important">');
+          item.action = '<div class="btn-group">' +
+            '<a href="#" data-bs-toggle="modal" data-bs-target="#showQueuedMsg" data-queue-id="' + encodeURI(item.queue_id) + '" class="btn btn-xs btn-secondary">' + lang.show_message + '</a>' +
             '</div>';
           });
           return data;
         }
       },
       columns: [
-          {
-            // placeholder, so checkbox will not block child row toggle
-            title: '',
-            data: null,
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: '',
-            data: 'chkbox',
-            searchable: false,
-            orderable: false,
-            defaultContent: ''
-          },
-          {
-            title: 'QID',
-            data: 'queue_id',
-            defaultContent: ''
-          },
-          {
-            title: 'Queue',
-            data: 'queue_name',
-            defaultContent: ''
-          },
-          {
-            title: lang_admin.arrival_time,
-            data: 'arrival_time',
-            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"});
-            }
-          },
-          {
-            title: lang_admin.message_size,
-            data: 'message_size',
-            defaultContent: '',
-            render: function (data, type){
-              return humanFileSize(data);
-            }
-          },
-          {
-            title: lang_admin.sender,
-            data: 'sender',
-            defaultContent: ''
-          },
-          {
-            title: lang_admin.recipients,
-            data: 'recipients',
-            defaultContent: ''
-          },
-          {
-            title: lang_admin.action,
-            data: 'action',
-            className: 'text-md-end dt-sm-head-hidden dt-body-right',
-            defaultContent: ''
-          },
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: 'QID',
+          data: 'queue_id',
+          defaultContent: ''
+        },
+        {
+          title: 'Queue',
+          data: 'queue_name',
+          defaultContent: ''
+        },
+        {
+          title: lang_admin.arrival_time,
+          data: 'arrival_time',
+          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"});
+          }
+        },
+        {
+          title: lang_admin.message_size,
+          data: 'message_size',
+          defaultContent: '',
+          render: function (data, type){
+            return humanFileSize(data);
+          }
+        },
+        {
+          title: lang_admin.sender,
+          data: 'sender',
+          defaultContent: ''
+        },
+        {
+          title: lang_admin.recipients,
+          data: 'recipients',
+          defaultContent: ''
+        },
+        {
+          title: lang_admin.action,
+          data: 'action',
+          className: 'text-md-end dt-sm-head-hidden dt-body-right',
+          defaultContent: ''
+        },
       ]
     });
   }
 
   draw_queue();
 
-})
+})

+ 667 - 662
data/web/js/site/user.js

@@ -1,662 +1,667 @@
-// Base64 functions
-var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
-$(document).ready(function() {
-  // Spam score slider
-  var spam_slider = $('#spam_score')[0];
-  if (typeof spam_slider !== 'undefined') {
-    noUiSlider.create(spam_slider, {
-      start: user_spam_score,
-      connect: [true, true, true],
-      range: {
-        'min': [0], //stepsize is 50.000
-        '50%': [10],
-        '70%': [20, 5],
-        '80%': [50, 10],
-        '90%': [100, 100],
-        '95%': [1000, 1000],
-        'max': [5000]
-      },
-    });
-    var connect = spam_slider.querySelectorAll('.noUi-connect');
-    var classes = ['c-1-color', 'c-2-color', 'c-3-color'];
-    for (var i = 0; i < connect.length; i++) {
-      connect[i].classList.add(classes[i]);
-    }
-    spam_slider.noUiSlider.on('update', function (values, handle) {
-      $('.spam-ham-score').text('< ' + Math.round(values[0] * 10) / 10);
-      $('.spam-spam-score').text(Math.round(values[0] * 10) / 10 + ' - ' + Math.round(values[1] * 10) / 10);
-      $('.spam-reject-score').text('> ' + Math.round(values[1] * 10) / 10);
-      $('#spam_score_value').val((Math.round(values[0] * 10) / 10) + ',' + (Math.round(values[1] * 10) / 10));
-    });
-  }
-  // syncjobLogModal
-  $('#syncjobLogModal').on('show.bs.modal', function(e) {
-    var syncjob_id = $(e.relatedTarget).data('syncjob-id');
-    $.ajax({
-      url: '/inc/ajax/syncjob_logs.php',
-      data: { id: syncjob_id },
-      dataType: 'text',
-      success: function(data){
-        $(e.currentTarget).find('#logText').text(data);
-      },
-      error: function(xhr, status, error) {
-        $(e.currentTarget).find('#logText').text(xhr.responseText);
-      }
-    });
-  });
-  $(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
-  $("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
-
-});
-jQuery(function($){
-  // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
-  var entityMap = {
-  '&': '&amp;',
-  '<': '&lt;',
-  '>': '&gt;',
-  '"': '&quot;',
-  "'": '&#39;',
-  '/': '&#x2F;',
-  '`': '&#x60;',
-  '=': '&#x3D;'
-  };
-  function escapeHtml(string) {
-    return String(string).replace(/[&<>"'`=\/]/g, function (s) {
-      return entityMap[s];
-    });
-  }
-  // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
-  function validateEmail(email) {
-    var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-    return re.test(email);
-  }
-  function unix_time_format(tm) {
-    var date = new Date(tm ? tm * 1000 : 0);
-    return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-  }
-  acl_data = JSON.parse(acl);
-
-  $('.clear-last-logins').on('click', function () {if (confirm(lang.delete_ays)) {last_logins('reset');}})
-  $(".login-history").on('click', function(e) {e.preventDefault(); last_logins('get', $(this).data('days'));$(this).addClass('active').siblings().removeClass('active');});
-
-  function last_logins(action, days = 7) {
-    if (action == 'get') {
-      $('.last-login').html('<i class="bi bi-hourglass"></i>' +  lang.waiting);
-      $.ajax({
-        dataType: 'json',
-        url: '/api/v1/get/last-login/' + encodeURIComponent(mailcow_cc_username) + '/' + days,
-        jsonp: false,
-        error: function () {
-          console.log('error reading last logins');
-        },
-        success: function (data) {
-          $('.last-login').html();
-          if (data.ui.time) {
-            $('.last-login').html('<i class="bi bi-person-fill"></i> ' + lang.last_ui_login + ': ' + unix_time_format(data.ui.time));
-          } else {
-            $('.last-login').text(lang.no_last_login);
-          }
-          if (data.sasl) {
-            $('.last-login').append('<ul class="list-group">');
-            $.each(data.sasl, function (i, item) {
-              var datetime = new Date(item.datetime.replace(/-/g, "/"));
-              var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-              var service = '<div class="badge fs-6 bg-secondary">' + item.service.toUpperCase() + '</div>';
-              var app_password = item.app_password ? ' <a href="/edit/app-passwd/' + item.app_password + '"><i class="bi bi-app-indicator"></i> ' + escapeHtml(item.app_password_name || "App") + '</a>' : '';
-              var real_rip = item.real_rip.startsWith("Web") ? item.real_rip : '<a href="https://bgp.he.net/ip/' + item.real_rip + '" target="_blank">' + item.real_rip + "</a>";
-              var ip_location = item.location ? ' <span class="flag-icon flag-icon-' + item.location.toLowerCase() + '"></span>' : '';
-              var ip_data = real_rip + ip_location + app_password;
-              $(".last-login").append('<li class="list-group-item">' + local_datetime + " " + service + " " + lang.from + " " + ip_data + "</li>");
-            })
-            $('.last-login').append('</ul>');
-          }
-        }
-      })
-    } else if (action == 'reset') {
-      $.ajax({
-        dataType: 'json',
-        url: '/api/v1/get/reset-last-login/' + encodeURIComponent(mailcow_cc_username),
-        jsonp: false,
-        error: function () {
-          console.log('cannot reset last logins');
-        },
-        success: function (data) {
-          last_logins('get');
-        }
-      })
-    }
-  }
-
-  function draw_tla_table() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#tla_table') ) {
-      $('#tla_table').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#tla_table').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: "/api/v1/get/time_limited_aliases",
-        dataSrc: function(data){
-          console.log(data);
-          $.each(data, function (i, item) {
-            if (acl_data.spam_alias === 1) {
-              item.action = '<div class="btn-group">' +
-                '<a href="#" data-action="delete_selected" data-id="single-tla" data-api-url="delete/time_limited_alias" data-item="' + encodeURIComponent(item.address) + '" class="btn btn-xs btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
-                '</div>';
-              item.chkbox = '<input type="checkbox" data-id="tla" name="multi_select" value="' + encodeURIComponent(item.address) + '" />';
-              item.address = escapeHtml(item.address);
-            }
-            else {
-              item.chkbox = '<input type="checkbox" disabled />';
-              item.action = '<span>-</span>';
-            }
-          });
-
-          return data;
-        }
-      },
-      columns: [          
-        {
-          // placeholder, so checkbox will not block child row toggle
-          title: '',
-          data: null,
-          searchable: false,
-          orderable: false,
-          defaultContent: ''
-        },
-        {
-          title: '',
-          data: 'chkbox',
-          searchable: false,
-          orderable: false,
-          defaultContent: ''
-        },
-        {
-          title: lang.alias,
-          data: 'address',
-          defaultContent: ''
-        },
-        {
-          title: lang.alias_valid_until,
-          data: 'validity',
-          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"});
-          }
-        },
-        {
-          title: lang.created_on,
-          data: 'created',
-          defaultContent: '',
-          render: function (data, type) {
-            var date = new Date(data.replace(/-/g, "/"));
-            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
-          }
-        },
-        {
-          title: lang.action,
-          data: 'action',
-          className: 'text-md-end dt-sm-head-hidden dt-body-right',
-          defaultContent: ''
-        }
-      ]
-    });
-  }
-  function draw_sync_job_table() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#sync_job_table') ) {
-      $('#sync_job_table').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#sync_job_table').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: '/api/v1/get/syncjobs/' + encodeURIComponent(mailcow_cc_username) + '/no_log',
-        dataSrc: function(data){
-          console.log(data);
-          $.each(data, function (i, item) {
-            item.user1 = escapeHtml(item.user1);
-            item.log = '<a href="#syncjobLogModal" data-bs-toggle="modal" data-syncjob-id="' + item.id + '">' + lang.open_logs + '</a>'
-            if (!item.exclude > 0) {
-              item.exclude = '-';
-            } else {
-              item.exclude  = '<code>' + escapeHtml(item.exclude) + '</code>';
-            }
-            item.server_w_port = escapeHtml(item.user1 + '@' + item.host1 + ':' + item.port1);
-            if (acl_data.syncjobs === 1) {
-              item.action = '<div class="btn-group">' +
-                '<a href="/edit/syncjob/' + item.id + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
-                '<a href="#" data-action="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
-                '</div>';
-              item.chkbox = '<input type="checkbox" data-id="syncjob" name="multi_select" value="' + item.id + '" />';
-            }
-            else {
-              item.action = '<span>-</span>';
-              item.chkbox = '<input type="checkbox" disabled />';
-            }
-            if (item.is_running == 1) {
-              item.is_running = '<span id="active-script" class="badge fs-6 bg-success">' + lang.running + '</span>';
-            } else {
-              item.is_running = '<span id="inactive-script" class="badge fs-6 bg-warning">' + lang.waiting + '</span>';
-            }
-            if (!item.last_run > 0) {
-              item.last_run = lang.waiting;
-            }
-            if (item.success == null) {
-              item.success = '-';
-              item.exit_status = '';
-            } else {
-              item.success = '<i class="text-' + (item.success == 1 ? 'success' : 'danger') + ' bi bi-' + (item.success == 1 ? 'check-lg' : 'x-lg') + '"></i>';
-            }
-            if (lang['syncjob_'+item.exit_status]) {
-	            item.exit_status = lang['syncjob_'+item.exit_status];
-            } else if (item.success != '-') {
-	            item.exit_status = lang.syncjob_check_log;
-            }
-            item.exit_status = item.success + ' ' + item.exit_status;
-          });
-
-          return data;
-        }
-      },
-      columns: [          
-        {
-          // placeholder, so checkbox will not block child row toggle
-          title: '',
-          data: null,
-          searchable: false,
-          orderable: false,
-          defaultContent: '',
-          responsivePriority: 1
-        },
-        {
-          title: '',
-          data: 'chkbox',
-          searchable: false,
-          orderable: false,
-          defaultContent: '',
-          responsivePriority: 2
-        },
-        {
-          title: 'ID',
-          data: 'id',
-          defaultContent: '',
-          responsivePriority: 3
-        },
-        {
-          title: 'Server',
-          data: 'server_w_port',
-          defaultContent: ''
-        },
-        {
-          title: lang.username,
-          data: 'user1',
-          defaultContent: '',
-          responsivePriority: 3
-        },
-        {
-          title: lang.last_run,
-          data: 'last_run',
-          defaultContent: ''
-        },
-        {
-          title: lang.syncjob_last_run_result,
-          data: 'exit_status',
-          defaultContent: ''
-        },
-        {
-          title: 'Log',
-          data: 'log',
-          defaultContent: ''
-        },
-        {
-          title: lang.active,
-          data: 'active',
-          defaultContent: '',
-          render: function (data, type) {
-            return 1==data?'<i class="bi bi-check-lg"></i>':0==data&&'<i class="bi bi-x-lg"></i>'
-          }
-        },
-        {
-          title: lang.status,
-          data: 'is_running',
-          defaultContent: '',
-          responsivePriority: 5
-        },
-        {
-          title: lang.encryption,
-          data: 'enc1',
-          defaultContent: ''
-        },
-        {
-          title: lang.excludes,
-          data: 'exclude',
-          defaultContent: ''
-        },
-        {
-          title: lang.interval + " (min)",
-          data: 'mins_interval',
-          defaultContent: ''
-        },
-        {
-          title: lang.action,
-          data: 'action',
-          className: 'text-md-end dt-sm-head-hidden dt-body-right',
-          defaultContent: '',
-          responsivePriority: 5
-        }
-      ]
-    });
-  }
-  function draw_app_passwd_table() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#app_passwd_table') ) {
-      $('#app_passwd_table').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#app_passwd_table').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: '/api/v1/get/app-passwd/all',
-        dataSrc: function(data){
-          console.log(data);
-          $.each(data, function (i, item) {
-            item.name = escapeHtml(item.name)
-            item.protocols = []
-            if (item.imap_access == 1) { item.protocols.push("<code>IMAP</code>"); }
-            if (item.smtp_access == 1) { item.protocols.push("<code>SMTP</code>"); }
-            if (item.eas_access == 1) { item.protocols.push("<code>EAS/ActiveSync</code>"); }
-            if (item.dav_access == 1) { item.protocols.push("<code>DAV</code>"); }
-            if (item.pop3_access == 1) { item.protocols.push("<code>POP3</code>"); }
-            if (item.sieve_access == 1) { item.protocols.push("<code>Sieve</code>"); }
-            item.protocols = item.protocols.join(" ")
-            if (acl_data.app_passwds === 1) {
-              item.action = '<div class="btn-group">' +
-                '<a href="/edit/app-passwd/' + item.id + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
-                '<a href="#" data-action="delete_selected" data-id="single-apppasswd" data-api-url="delete/app-passwd" data-item="' + item.id + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
-                '</div>';
-              item.chkbox = '<input type="checkbox" data-id="apppasswd" name="multi_select" value="' + item.id + '" />';
-            }
-            else {
-              item.action = '<span>-</span>';
-              item.chkbox = '<input type="checkbox" disabled />';
-            }
-          });
-
-          return data;
-        }
-      },
-      columns: [          
-        {
-          // placeholder, so checkbox will not block child row toggle
-          title: '',
-          data: null,
-          searchable: false,
-          orderable: false,
-          defaultContent: ''
-        },
-        {
-          title: '',
-          data: 'chkbox',
-          searchable: false,
-          orderable: false,
-          defaultContent: ''
-        },
-        {
-          title: 'ID',
-          data: 'id',
-          defaultContent: ''
-        },
-        {
-          title: lang.app_name,
-          data: 'name',
-          defaultContent: ''
-        },
-        {
-          title: lang.allowed_protocols,
-          data: 'protocols',
-          defaultContent: ''
-        },
-        {
-          title: lang.active,
-          data: 'active',
-          defaultContent: '',
-          render: function (data, type) {
-            return 1==data?'<i class="bi bi-check-lg"></i>':0==data&&'<i class="bi bi-x-lg"></i>'
-          }
-        },
-        {
-          title: lang.action,
-          data: 'action',
-          className: 'text-md-end dt-sm-head-hidden dt-body-right',
-          defaultContent: ''
-        }
-      ]
-    });
-  }
-  function draw_wl_policy_mailbox_table() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#wl_policy_mailbox_table') ) {
-      $('#wl_policy_mailbox_table').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#wl_policy_mailbox_table').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: '/api/v1/get/policy_wl_mailbox',
-        dataSrc: function(data){
-          console.log(data);
-          $.each(data, function (i, item) {
-            if (validateEmail(item.object)) {
-              item.chkbox = '<input type="checkbox" data-id="policy_wl_mailbox" name="multi_select" value="' + item.prefid + '" />';
-            }
-            else {
-              item.chkbox = '<input type="checkbox" disabled title="' + lang.spamfilter_table_domain_policy + '" />';
-            }
-            if (acl_data.spam_policy === 0) {
-              item.chkbox = '<input type="checkbox" disabled />';
-            }
-          });
-
-          return data;
-        }
-      },
-      columns: [          
-        {
-          // placeholder, so checkbox will not block child row toggle
-          title: '',
-          data: null,
-          searchable: false,
-          orderable: false,
-          defaultContent: ''
-        },
-        {
-          title: '',
-          data: 'chkbox',
-          searchable: false,
-          orderable: false,
-          defaultContent: ''
-        },
-        {
-          title: 'ID',
-          data: 'prefid',
-          defaultContent: ''
-        },
-        {
-          title: lang.spamfilter_table_rule,
-          data: 'value',
-          defaultContent: ''
-        },
-        {
-          title:'Scope',
-          data: 'object',
-          defaultContent: ''
-        }
-      ]
-    });
-  }
-  function draw_bl_policy_mailbox_table() {
-    // just recalc width if instance already exists
-    if ($.fn.DataTable.isDataTable('#bl_policy_mailbox_table') ) {
-      $('#bl_policy_mailbox_table').DataTable().columns.adjust().responsive.recalc();
-      return;
-    }
-
-    $('#bl_policy_mailbox_table').DataTable({
-			responsive: true,
-      processing: true,
-      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,
-      ajax: {
-        type: "GET",
-        url: '/api/v1/get/policy_bl_mailbox',
-        dataSrc: function(data){
-          console.log(data);
-          $.each(data, function (i, item) {
-            if (validateEmail(item.object)) {
-              item.chkbox = '<input type="checkbox" data-id="policy_bl_mailbox" name="multi_select" value="' + item.prefid + '" />';
-            }
-            else {
-              item.chkbox = '<input type="checkbox" disabled tooltip="' + lang.spamfilter_table_domain_policy + '" />';
-            }
-            if (acl_data.spam_policy === 0) {
-              item.chkbox = '<input type="checkbox" disabled />';
-            }
-          });
-
-          return data;
-        }
-      },
-      columns: [          
-        {
-          // placeholder, so checkbox will not block child row toggle
-          title: '',
-          data: null,
-          searchable: false,
-          orderable: false,
-          defaultContent: ''
-        },
-        {
-          title: '',
-          data: 'chkbox',
-          searchable: false,
-          orderable: false,
-          defaultContent: ''
-        },
-        {
-          title: 'ID',
-          data: 'prefid',
-          defaultContent: ''
-        },
-        {
-          title: lang.spamfilter_table_rule,
-          data: 'value',
-          defaultContent: ''
-        },
-        {
-          title:'Scope',
-          data: 'object',
-          defaultContent: ''
-        }
-      ]
-    });
-  }
-
-  // FIDO2 friendly name modal
-  $('#fido2ChangeFn').on('show.bs.modal', function (e) {
-    rename_link = $(e.relatedTarget)
-    if (rename_link != null) {
-      $('#fido2_cid').val(rename_link.data('cid'));
-      $('#fido2_subject_desc').text(Base64.decode(rename_link.data('subject')));
-    }
-  })
-
-  // Sieve data modal
-  $('#userFilterModal').on('show.bs.modal', function(e) {
-    $('#user_sieve_filter').text(lang.loading);
-    $.ajax({
-      dataType: 'json',
-      url: '/api/v1/get/active-user-sieve/' + encodeURIComponent(mailcow_cc_username),
-      jsonp: false,
-      error: function () {
-        console.log('Cannot get active sieve script');
-      },
-      complete: function (data) {
-        if (data.responseText == '{}') {
-          $('#user_sieve_filter').text(lang.no_active_filter);
-        } else {
-          $('#user_sieve_filter').text(JSON.parse(data.responseText));
-        }
-      }
-    })
-  });
-  $('#userFilterModal').on('hidden.bs.modal', function () {
-    $('#user_sieve_filter').text(lang.loading);
-  });
-
-  // detect element visibility changes
-  function onVisible(element, callback) {
-    $(document).ready(function() {
-      element_object = document.querySelector(element);
-      if (element_object === null) return;
-
-      new IntersectionObserver((entries, observer) => {
-        entries.forEach(entry => {
-          if(entry.intersectionRatio > 0) {
-            callback(element_object);
-          }
-        });
-      }).observe(element_object);
-    });
-  }
-
-  // Load only if the tab is visible
-  onVisible("[id^=tla_table]", () => draw_tla_table());
-  onVisible("[id^=bl_policy_mailbox_table]", () => draw_bl_policy_mailbox_table());
-  onVisible("[id^=wl_policy_mailbox_table]", () => draw_wl_policy_mailbox_table());
-  onVisible("[id^=sync_job_table]", () => draw_sync_job_table());
-  onVisible("[id^=app_passwd_table]", () => draw_app_passwd_table());
-  last_logins('get');
-});
+// Base64 functions
+var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
+$(document).ready(function() {
+  // Spam score slider
+  var spam_slider = $('#spam_score')[0];
+  if (typeof spam_slider !== 'undefined') {
+    noUiSlider.create(spam_slider, {
+      start: user_spam_score,
+      connect: [true, true, true],
+      range: {
+        'min': [0], //stepsize is 50.000
+        '50%': [10],
+        '70%': [20, 5],
+        '80%': [50, 10],
+        '90%': [100, 100],
+        '95%': [1000, 1000],
+        'max': [5000]
+      },
+    });
+    var connect = spam_slider.querySelectorAll('.noUi-connect');
+    var classes = ['c-1-color', 'c-2-color', 'c-3-color'];
+    for (var i = 0; i < connect.length; i++) {
+      connect[i].classList.add(classes[i]);
+    }
+    spam_slider.noUiSlider.on('update', function (values, handle) {
+      $('.spam-ham-score').text('< ' + Math.round(values[0] * 10) / 10);
+      $('.spam-spam-score').text(Math.round(values[0] * 10) / 10 + ' - ' + Math.round(values[1] * 10) / 10);
+      $('.spam-reject-score').text('> ' + Math.round(values[1] * 10) / 10);
+      $('#spam_score_value').val((Math.round(values[0] * 10) / 10) + ',' + (Math.round(values[1] * 10) / 10));
+    });
+  }
+  // syncjobLogModal
+  $('#syncjobLogModal').on('show.bs.modal', function(e) {
+    var syncjob_id = $(e.relatedTarget).data('syncjob-id');
+    $.ajax({
+      url: '/inc/ajax/syncjob_logs.php',
+      data: { id: syncjob_id },
+      dataType: 'text',
+      success: function(data){
+        $(e.currentTarget).find('#logText').text(data);
+      },
+      error: function(xhr, status, error) {
+        $(e.currentTarget).find('#logText').text(xhr.responseText);
+      }
+    });
+  });
+  $(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
+  $("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
+
+});
+jQuery(function($){
+  // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
+  var entityMap = {
+  '&': '&amp;',
+  '<': '&lt;',
+  '>': '&gt;',
+  '"': '&quot;',
+  "'": '&#39;',
+  '/': '&#x2F;',
+  '`': '&#x60;',
+  '=': '&#x3D;'
+  };
+  function escapeHtml(string) {
+    return String(string).replace(/[&<>"'`=\/]/g, function (s) {
+      return entityMap[s];
+    });
+  }
+  // http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
+  function validateEmail(email) {
+    var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+    return re.test(email);
+  }
+  function unix_time_format(tm) {
+    var date = new Date(tm ? tm * 1000 : 0);
+    return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
+  }
+  acl_data = JSON.parse(acl);
+
+  $('.clear-last-logins').on('click', function () {if (confirm(lang.delete_ays)) {last_logins('reset');}})
+  $(".login-history").on('click', function(e) {e.preventDefault(); last_logins('get', $(this).data('days'));$(this).addClass('active').siblings().removeClass('active');});
+
+  function last_logins(action, days = 7) {
+    if (action == 'get') {
+      $('.last-login').html('<i class="bi bi-hourglass"></i>' +  lang.waiting);
+      $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/last-login/' + encodeURIComponent(mailcow_cc_username) + '/' + days,
+        jsonp: false,
+        error: function () {
+          console.log('error reading last logins');
+        },
+        success: function (data) {
+          $('.last-login').html();
+          if (data.ui.time) {
+            $('.last-login').html('<i class="bi bi-person-fill"></i> ' + lang.last_ui_login + ': ' + unix_time_format(data.ui.time));
+          } else {
+            $('.last-login').text(lang.no_last_login);
+          }
+          if (data.sasl) {
+            $('.last-login').append('<ul class="list-group">');
+            $.each(data.sasl, function (i, item) {
+              var datetime = new Date(item.datetime.replace(/-/g, "/"));
+              var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
+              var service = '<div class="badge fs-6 bg-secondary">' + item.service.toUpperCase() + '</div>';
+              var app_password = item.app_password ? ' <a href="/edit/app-passwd/' + item.app_password + '"><i class="bi bi-app-indicator"></i> ' + escapeHtml(item.app_password_name || "App") + '</a>' : '';
+              var real_rip = item.real_rip.startsWith("Web") ? item.real_rip : '<a href="https://bgp.he.net/ip/' + item.real_rip + '" target="_blank">' + item.real_rip + "</a>";
+              var ip_location = item.location ? ' <span class="flag-icon flag-icon-' + item.location.toLowerCase() + '"></span>' : '';
+              var ip_data = real_rip + ip_location + app_password;
+              $(".last-login").append('<li class="list-group-item">' + local_datetime + " " + service + " " + lang.from + " " + ip_data + "</li>");
+            })
+            $('.last-login').append('</ul>');
+          }
+        }
+      })
+    } else if (action == 'reset') {
+      $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/reset-last-login/' + encodeURIComponent(mailcow_cc_username),
+        jsonp: false,
+        error: function () {
+          console.log('cannot reset last logins');
+        },
+        success: function (data) {
+          last_logins('get');
+        }
+      })
+    }
+  }
+
+  function draw_tla_table() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#tla_table') ) {
+      $('#tla_table').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#tla_table').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: "/api/v1/get/time_limited_aliases",
+        dataSrc: function(data){
+          console.log(data);
+          $.each(data, function (i, item) {
+            if (acl_data.spam_alias === 1) {
+              item.action = '<div class="btn-group">' +
+                '<a href="#" data-action="delete_selected" data-id="single-tla" data-api-url="delete/time_limited_alias" data-item="' + encodeURIComponent(item.address) + '" class="btn btn-xs btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+                '</div>';
+              item.chkbox = '<input type="checkbox" data-id="tla" name="multi_select" value="' + encodeURIComponent(item.address) + '" />';
+              item.address = escapeHtml(item.address);
+            }
+            else {
+              item.chkbox = '<input type="checkbox" disabled />';
+              item.action = '<span>-</span>';
+            }
+          });
+
+          return data;
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: lang.alias,
+          data: 'address',
+          defaultContent: ''
+        },
+        {
+          title: lang.alias_valid_until,
+          data: 'validity',
+          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"});
+          }
+        },
+        {
+          title: lang.created_on,
+          data: 'created',
+          defaultContent: '',
+          render: function (data, type) {
+            var date = new Date(data.replace(/-/g, "/"));
+            return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
+          }
+        },
+        {
+          title: lang.action,
+          data: 'action',
+          className: 'text-md-end dt-sm-head-hidden dt-body-right',
+          defaultContent: ''
+        }
+      ]
+    });
+  }
+  function draw_sync_job_table() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#sync_job_table') ) {
+      $('#sync_job_table').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#sync_job_table').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: '/api/v1/get/syncjobs/' + encodeURIComponent(mailcow_cc_username) + '/no_log',
+        dataSrc: function(data){
+          console.log(data);
+          $.each(data, function (i, item) {
+            item.user1 = escapeHtml(item.user1);
+            item.log = '<a href="#syncjobLogModal" data-bs-toggle="modal" data-syncjob-id="' + item.id + '">' + lang.open_logs + '</a>'
+            if (!item.exclude > 0) {
+              item.exclude = '-';
+            } else {
+              item.exclude  = '<code>' + escapeHtml(item.exclude) + '</code>';
+            }
+            item.server_w_port = escapeHtml(item.user1 + '@' + item.host1 + ':' + item.port1);
+            if (acl_data.syncjobs === 1) {
+              item.action = '<div class="btn-group">' +
+                '<a href="/edit/syncjob/' + item.id + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
+                '<a href="#" data-action="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+                '</div>';
+              item.chkbox = '<input type="checkbox" data-id="syncjob" name="multi_select" value="' + item.id + '" />';
+            }
+            else {
+              item.action = '<span>-</span>';
+              item.chkbox = '<input type="checkbox" disabled />';
+            }
+            if (item.is_running == 1) {
+              item.is_running = '<span id="active-script" class="badge fs-6 bg-success">' + lang.running + '</span>';
+            } else {
+              item.is_running = '<span id="inactive-script" class="badge fs-6 bg-warning">' + lang.waiting + '</span>';
+            }
+            if (!item.last_run > 0) {
+              item.last_run = lang.waiting;
+            }
+            if (item.success == null) {
+              item.success = '-';
+              item.exit_status = '';
+            } else {
+              item.success = '<i class="text-' + (item.success == 1 ? 'success' : 'danger') + ' bi bi-' + (item.success == 1 ? 'check-lg' : 'x-lg') + '"></i>';
+            }
+            if (lang['syncjob_'+item.exit_status]) {
+              item.exit_status = lang['syncjob_'+item.exit_status];
+            } else if (item.success != '-') {
+              item.exit_status = lang.syncjob_check_log;
+            }
+            item.exit_status = item.success + ' ' + item.exit_status;
+          });
+
+          return data;
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: '',
+          responsivePriority: 1
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: '',
+          responsivePriority: 2
+        },
+        {
+          title: 'ID',
+          data: 'id',
+          defaultContent: '',
+          responsivePriority: 3
+        },
+        {
+          title: 'Server',
+          data: 'server_w_port',
+          defaultContent: ''
+        },
+        {
+          title: lang.username,
+          data: 'user1',
+          defaultContent: '',
+          responsivePriority: 3
+        },
+        {
+          title: lang.last_run,
+          data: 'last_run',
+          defaultContent: ''
+        },
+        {
+          title: lang.syncjob_last_run_result,
+          data: 'exit_status',
+          defaultContent: ''
+        },
+        {
+          title: 'Log',
+          data: 'log',
+          defaultContent: ''
+        },
+        {
+          title: lang.active,
+          data: 'active',
+          defaultContent: '',
+          render: function (data, type) {
+            return 1==data?'<i class="bi bi-check-lg"></i>':0==data&&'<i class="bi bi-x-lg"></i>'
+          }
+        },
+        {
+          title: lang.status,
+          data: 'is_running',
+          defaultContent: '',
+          responsivePriority: 5
+        },
+        {
+          title: lang.encryption,
+          data: 'enc1',
+          defaultContent: ''
+        },
+        {
+          title: lang.excludes,
+          data: 'exclude',
+          defaultContent: ''
+        },
+        {
+          title: lang.interval + " (min)",
+          data: 'mins_interval',
+          defaultContent: ''
+        },
+        {
+          title: lang.action,
+          data: 'action',
+          className: 'text-md-end dt-sm-head-hidden dt-body-right',
+          defaultContent: '',
+          responsivePriority: 5
+        }
+      ]
+    });
+  }
+  function draw_app_passwd_table() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#app_passwd_table') ) {
+      $('#app_passwd_table').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#app_passwd_table').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: '/api/v1/get/app-passwd/all',
+        dataSrc: function(data){
+          console.log(data);
+          $.each(data, function (i, item) {
+            item.name = escapeHtml(item.name)
+            item.protocols = []
+            if (item.imap_access == 1) { item.protocols.push("<code>IMAP</code>"); }
+            if (item.smtp_access == 1) { item.protocols.push("<code>SMTP</code>"); }
+            if (item.eas_access == 1) { item.protocols.push("<code>EAS/ActiveSync</code>"); }
+            if (item.dav_access == 1) { item.protocols.push("<code>DAV</code>"); }
+            if (item.pop3_access == 1) { item.protocols.push("<code>POP3</code>"); }
+            if (item.sieve_access == 1) { item.protocols.push("<code>Sieve</code>"); }
+            item.protocols = item.protocols.join(" ")
+            if (acl_data.app_passwds === 1) {
+              item.action = '<div class="btn-group">' +
+                '<a href="/edit/app-passwd/' + item.id + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
+                '<a href="#" data-action="delete_selected" data-id="single-apppasswd" data-api-url="delete/app-passwd" data-item="' + item.id + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
+                '</div>';
+              item.chkbox = '<input type="checkbox" data-id="apppasswd" name="multi_select" value="' + item.id + '" />';
+            }
+            else {
+              item.action = '<span>-</span>';
+              item.chkbox = '<input type="checkbox" disabled />';
+            }
+          });
+
+          return data;
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: 'ID',
+          data: 'id',
+          defaultContent: ''
+        },
+        {
+          title: lang.app_name,
+          data: 'name',
+          defaultContent: ''
+        },
+        {
+          title: lang.allowed_protocols,
+          data: 'protocols',
+          defaultContent: ''
+        },
+        {
+          title: lang.active,
+          data: 'active',
+          defaultContent: '',
+          render: function (data, type) {
+            return 1==data?'<i class="bi bi-check-lg"></i>':0==data&&'<i class="bi bi-x-lg"></i>'
+          }
+        },
+        {
+          title: lang.action,
+          data: 'action',
+          className: 'text-md-end dt-sm-head-hidden dt-body-right',
+          defaultContent: ''
+        }
+      ]
+    });
+  }
+  function draw_wl_policy_mailbox_table() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#wl_policy_mailbox_table') ) {
+      $('#wl_policy_mailbox_table').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#wl_policy_mailbox_table').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: '/api/v1/get/policy_wl_mailbox',
+        dataSrc: function(data){
+          console.log(data);
+          $.each(data, function (i, item) {
+            if (validateEmail(item.object)) {
+              item.chkbox = '<input type="checkbox" data-id="policy_wl_mailbox" name="multi_select" value="' + item.prefid + '" />';
+            }
+            else {
+              item.chkbox = '<input type="checkbox" disabled title="' + lang.spamfilter_table_domain_policy + '" />';
+            }
+            if (acl_data.spam_policy === 0) {
+              item.chkbox = '<input type="checkbox" disabled />';
+            }
+          });
+
+          return data;
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: 'ID',
+          data: 'prefid',
+          defaultContent: ''
+        },
+        {
+          title: lang.spamfilter_table_rule,
+          data: 'value',
+          defaultContent: ''
+        },
+        {
+          title:'Scope',
+          data: 'object',
+          defaultContent: ''
+        }
+      ]
+    });
+  }
+  function draw_bl_policy_mailbox_table() {
+    // just recalc width if instance already exists
+    if ($.fn.DataTable.isDataTable('#bl_policy_mailbox_table') ) {
+      $('#bl_policy_mailbox_table').DataTable().columns.adjust().responsive.recalc();
+      return;
+    }
+
+    $('#bl_policy_mailbox_table').DataTable({
+      responsive: true,
+      processing: true,
+      serverSide: false,
+      stateSave: true,
+      pageLength: pagination_size,
+      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,
+      ajax: {
+        type: "GET",
+        url: '/api/v1/get/policy_bl_mailbox',
+        dataSrc: function(data){
+          console.log(data);
+          $.each(data, function (i, item) {
+            if (validateEmail(item.object)) {
+              item.chkbox = '<input type="checkbox" data-id="policy_bl_mailbox" name="multi_select" value="' + item.prefid + '" />';
+            }
+            else {
+              item.chkbox = '<input type="checkbox" disabled tooltip="' + lang.spamfilter_table_domain_policy + '" />';
+            }
+            if (acl_data.spam_policy === 0) {
+              item.chkbox = '<input type="checkbox" disabled />';
+            }
+          });
+
+          return data;
+        }
+      },
+      columns: [
+        {
+          // placeholder, so checkbox will not block child row toggle
+          title: '',
+          data: null,
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: '',
+          data: 'chkbox',
+          searchable: false,
+          orderable: false,
+          defaultContent: ''
+        },
+        {
+          title: 'ID',
+          data: 'prefid',
+          defaultContent: ''
+        },
+        {
+          title: lang.spamfilter_table_rule,
+          data: 'value',
+          defaultContent: ''
+        },
+        {
+          title:'Scope',
+          data: 'object',
+          defaultContent: ''
+        }
+      ]
+    });
+  }
+
+  // FIDO2 friendly name modal
+  $('#fido2ChangeFn').on('show.bs.modal', function (e) {
+    rename_link = $(e.relatedTarget)
+    if (rename_link != null) {
+      $('#fido2_cid').val(rename_link.data('cid'));
+      $('#fido2_subject_desc').text(Base64.decode(rename_link.data('subject')));
+    }
+  })
+
+  // Sieve data modal
+  $('#userFilterModal').on('show.bs.modal', function(e) {
+    $('#user_sieve_filter').text(lang.loading);
+    $.ajax({
+      dataType: 'json',
+      url: '/api/v1/get/active-user-sieve/' + encodeURIComponent(mailcow_cc_username),
+      jsonp: false,
+      error: function () {
+        console.log('Cannot get active sieve script');
+      },
+      complete: function (data) {
+        if (data.responseText == '{}') {
+          $('#user_sieve_filter').text(lang.no_active_filter);
+        } else {
+          $('#user_sieve_filter').text(JSON.parse(data.responseText));
+        }
+      }
+    })
+  });
+  $('#userFilterModal').on('hidden.bs.modal', function () {
+    $('#user_sieve_filter').text(lang.loading);
+  });
+
+  // detect element visibility changes
+  function onVisible(element, callback) {
+    $(document).ready(function() {
+      element_object = document.querySelector(element);
+      if (element_object === null) return;
+
+      new IntersectionObserver((entries, observer) => {
+        entries.forEach(entry => {
+          if(entry.intersectionRatio > 0) {
+            callback(element_object);
+          }
+        });
+      }).observe(element_object);
+    });
+  }
+
+  // Load only if the tab is visible
+  onVisible("[id^=tla_table]", () => draw_tla_table());
+  onVisible("[id^=bl_policy_mailbox_table]", () => draw_bl_policy_mailbox_table());
+  onVisible("[id^=wl_policy_mailbox_table]", () => draw_wl_policy_mailbox_table());
+  onVisible("[id^=sync_job_table]", () => draw_sync_job_table());
+  onVisible("[id^=app_passwd_table]", () => draw_app_passwd_table());
+  last_logins('get');
+});

+ 3 - 3
data/web/templates/admin.twig

@@ -57,7 +57,7 @@
       </div>
     </div> <!-- /col-md-12 -->
   </div> <!-- /row -->
-</div> 
+</div>
 
 {% include 'modals/admin.twig' %}
 
@@ -66,7 +66,7 @@ var lang = {{ lang_admin|raw }};
 var lang_datatables = {{ lang_datatables|raw }};
 var admin_username = '{{ mailcow_cc_username }}';
 var csrf_token = '{{ csrf_token }}';
-var pagination_size = '{{ pagination_size }}';
-var log_pagination_size = '{{ log_pagination_size }}';
+var pagination_size = Math.trunc('{{ pagination_size }}');
+var log_pagination_size = Math.trunc('{{ log_pagination_size }}');
 </script>
 {% endblock %}

File diff suppressed because it is too large
+ 5 - 5
data/web/templates/debug.twig


+ 1 - 1
data/web/templates/domainadmin.twig

@@ -46,7 +46,7 @@
       <div class="col-sm-3 col-5 text-end">{{ lang.fido2.known_ids }}:</div>
       <div class="col-sm-9 col-7">
         <div class="table-responsive">
-          <table class="table table-striped table-hover table-condensed" id="fido2_keys">
+          <table class="table table-striped table-hover table-condensed w-100" id="fido2_keys">
             <tr>
               <th>ID</th>
               <th style="min-width:240px;text-align: right">{{ lang.admin.action }}</th>

+ 1 - 1
data/web/templates/edit.twig

@@ -26,7 +26,7 @@
   var lang_user = {{ lang_user|raw }};
   var lang_datatables = {{ lang_datatables|raw }};
   var csrf_token = '{{ csrf_token }}';
-  var pagination_size = '{{ pagination_size }}';
+  var pagination_size = Math.trunc('{{ pagination_size }}');
   var table_for_domain = '{{ domain }}';
 </script>
 {% endblock %}

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

@@ -58,7 +58,7 @@
   var lang_rl = {{ lang_rl|raw }};
   var lang_datatables = {{ lang_datatables|raw }};
   var csrf_token = '{{ csrf_token }}';
-  var pagination_size = '{{ pagination_size }}';
+  var pagination_size = Math.trunc('{{ pagination_size }}');
   var role = '{{ role }}';
   var is_dual = {{ is_dual }};
   var ALLOW_ADMIN_EMAIL_LOGIN = {{ allow_admin_email_login }};

+ 2 - 2
data/web/templates/quarantine.twig

@@ -37,7 +37,7 @@
           </p>
           {% endif %}
         </p>
-        <table id="quarantinetable" class="table table-striped"></table>
+        <table id="quarantinetable" class="table table-striped w-100"></table>
         <div class="mass-actions-quarantine mt-4">
           <div class="btn-group" data-acl="{{ acl.quarantine }}">
             <a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary" id="toggle_multi_select_all" data-id="qitems" href="#"><i class="bi bi-check-all"></i> {{ lang.quarantine.toggle_all }}</a>
@@ -66,7 +66,7 @@ var acl = '{{ acl_json|raw }}';
 var lang = {{ lang_quarantine|raw }};
 var lang_datatables = {{ lang_datatables|raw }};
 var csrf_token = '{{ csrf_token }}';
-var pagination_size = '{{ pagination_size }}';
+var pagination_size = Math.trunc('{{ pagination_size }}');
 var role = '{{ role }}';
 </script>
 {% endblock %}

+ 1 - 1
data/web/templates/queue.twig

@@ -55,7 +55,7 @@
   var lang = {{ lang_queue|raw }};
   var lang_datatables = {{ lang_datatables|raw }};
   var csrf_token = '{{ csrf_token }}';
-  var pagination_size = '{{ pagination_size }}';
+  var pagination_size = Math.trunc('{{ pagination_size }}');
   var table_for_domain = '{{ domain }}';
 </script>
 {% endblock %}

+ 1 - 1
data/web/templates/user_domainadmin_common.twig

@@ -4,7 +4,7 @@
   var acl = '{{ acl_json|raw }}';
   var lang = {{ lang_user|raw }};
   var csrf_token = '{{ csrf_token }}';
-  var pagination_size = '{{ pagination_size }}';
+  var pagination_size = Math.trunc('{{ pagination_size }}');
   var mailcow_cc_username = '{{ mailcow_cc_username }}';
   var user_spam_score = [{{ user_spam_score }}];
   var lang_datatables = {{ lang_datatables|raw }};

Some files were not shown because too many files changed in this diff