From 14d2b3d7632881d1cb208dce7641a5a153bd2fcf Mon Sep 17 00:00:00 2001
From: Michael Kuron <m.kuron@gmx.de>
Date: Sun, 9 Jul 2017 10:01:27 +0200
Subject: [PATCH 01/23] DNS diagnostics page

---
 data/web/inc/header.inc.php |   5 +
 data/web/lang/lang.de.php   |   7 +
 data/web/lang/lang.en.php   |   7 +
 diagnostics.php             | 272 ++++++++++++++++++++++++++++++++++++
 4 files changed, 291 insertions(+)
 create mode 100644 diagnostics.php

diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php
index 533711de..8e5b6d07 100644
--- a/data/web/inc/header.inc.php
+++ b/data/web/inc/header.inc.php
@@ -77,6 +77,11 @@
                 <li<?= (preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/mailbox.php"><?= $lang['header']['mailboxes']; ?></a></li>
               <?php
               }
+              if ($_SESSION['mailcow_cc_role'] == 'admin') {
+              ?>
+                <li<?= (preg_match("/diagnostics/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/diagnostics.php"><?= $lang['header']['diagnostics']; ?></a></li>
+              <?php
+              }
               if ($_SESSION['mailcow_cc_role'] != 'admin') {
               ?>
                 <li<?= (preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/user.php"><?= $lang['header']['user_settings']; ?></a></li>
diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php
index eef13b3f..732171cc 100644
--- a/data/web/lang/lang.de.php
+++ b/data/web/lang/lang.de.php
@@ -214,6 +214,7 @@ $lang['header']['mailcow_settings'] = 'Konfiguration';
 $lang['header']['administration'] = 'Administration';
 $lang['header']['mailboxes'] = 'Mailboxen';
 $lang['header']['user_settings'] = 'Benutzereinstellungen';
+$lang['header']['diagnostics'] = 'Diagnose';
 $lang['header']['login'] = 'Anmeldung';
 $lang['header']['logged_in_as_logout'] = 'Eingeloggt als <b>%s</b> (abmelden)';
 $lang['header']['logged_in_as_logout_dual'] = 'Eingeloggt als <b>%s <span class="text-info">[%s]</span></b>';
@@ -495,3 +496,9 @@ $lang['admin']['add_forwarding_host'] = 'Weiterleitungs-Host hinzufügen';
 $lang['delete']['remove_forwardinghost_warning'] = '<b>Warnung:</b> Sie entfernen den Weiterleitungs-Host <b>%s</b>!';
 $lang['success']['forwarding_host_removed'] = "Weiterleitungs-Host %s wurde entfernt";
 $lang['success']['forwarding_host_added'] = "Weiterleitungs-Host %s wurde hinzugefügt";
+$lang['diagnostics']['dns_records'] = 'DNS-Einträge';
+$lang['diagnostics']['dns_records_24hours'] = 'Bitte beachten Sie, dass es bis zu 24 Stunden dauern kann, bis Änderungen an Ihren DNS-Einträgen als aktueller Status auf dieser Seite dargestellt werden. Diese Seite ist nur als Hilfsmittel gedacht, um die korrekten Werte für DNS-Einträge zu anzuzeigen und zu überprüfen, ob die Daten im DNS hinterlegt sind.';
+$lang['diagnostics']['dns_records_name'] = 'Name';
+$lang['diagnostics']['dns_records_type'] = 'Typ';
+$lang['diagnostics']['dns_records_data'] = 'Korrekte Daten';
+$lang['diagnostics']['dns_records_status'] = 'Aktueller Status';
diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php
index 3be72fec..391c7d01 100644
--- a/data/web/lang/lang.en.php
+++ b/data/web/lang/lang.en.php
@@ -216,6 +216,7 @@ $lang['header']['mailcow_settings'] = 'Configuration';
 $lang['header']['administration'] = 'Administration';
 $lang['header']['mailboxes'] = 'Mailboxes';
 $lang['header']['user_settings'] = 'User settings';
+$lang['header']['diagnostics'] = 'Diagnostics';
 $lang['header']['login'] = 'Login';
 $lang['header']['logged_in_as_logout'] = 'Logged in as <b>%s</b> (logout)';
 $lang['header']['logged_in_as_logout_dual'] = 'Logged in as <b>%s <span class="text-info">[%s]</span></b>';
@@ -508,3 +509,9 @@ $lang['admin']['add_forwarding_host'] = 'Add Forwarding Host';
 $lang['delete']['remove_forwardinghost_warning'] = '<b>Warning:</b> You are about to remove the forwarding host <b>%s</b>!';
 $lang['success']['forwarding_host_removed'] = "Forwarding host %s has been removed";
 $lang['success']['forwarding_host_added'] = "Forwarding host %s has been added";
+$lang['diagnostics']['dns_records'] = 'DNS Records';
+$lang['diagnostics']['dns_records_24hours'] = 'Please note that changes made to DNS may take up to 24 hours to correctly have their current state reflected on this page. It is intended as a way for you to easily see how to configure your DNS records and to check whether all your records are correctly stored in DNS.';
+$lang['diagnostics']['dns_records_name'] = 'Name';
+$lang['diagnostics']['dns_records_type'] = 'Type';
+$lang['diagnostics']['dns_records_data'] = 'Correct Data';
+$lang['diagnostics']['dns_records_status'] = 'Current State';
diff --git a/diagnostics.php b/diagnostics.php
new file mode 100644
index 00000000..4de0082e
--- /dev/null
+++ b/diagnostics.php
@@ -0,0 +1,272 @@
+<?php
+require_once 'inc/prerequisites.inc.php';
+require_once 'inc/spf.inc.php';
+
+function in_net($addr, $net) {
+  $net = explode('/', $net);
+  if (count($net) > 1) {
+    $mask = $net[1];
+  }
+  $net = inet_pton($net[0]);
+  $addr = inet_pton($addr);
+  $length = strlen($net); // 4 for IPv4, 16 for IPv6
+  if (strlen($net) != strlen($addr)) {
+    return false;
+  }
+  if (!isset($mask)) {
+    $mask = $length * 8;
+  }
+  $addr_bin = '';
+  $net_bin = '';
+  for ($i = 0; $i < $length; ++$i) {
+    $addr_bin .= str_pad(decbin(ord(substr($addr, $i, $i+1))), 8, '0', STR_PAD_LEFT);
+    $net_bin .= str_pad(decbin(ord(substr($net, $i, $i+1))), 8, '0', STR_PAD_LEFT);
+  }
+  return substr($addr_bin, 0, $mask) == substr($net_bin, 0, $mask);
+}
+
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
+require_once("inc/header.inc.php");
+
+$ch = curl_init('http://ipv4.mailcow.email');
+curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
+curl_setopt($ch, CURLOPT_VERBOSE, false);
+curl_setopt($ch, CURLOPT_HEADER, false);
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
+$ip = curl_exec($ch);
+curl_close($ch);
+
+$ch = curl_init('http://ipv6.mailcow.email');
+curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
+curl_setopt($ch, CURLOPT_VERBOSE, false);
+curl_setopt($ch, CURLOPT_HEADER, false);
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
+$ip6 = curl_exec($ch);
+curl_close($ch);
+
+$ptr = implode('.', array_reverse(explode('.', $ip))) . '.in-addr.arpa';
+if (!empty($ip6)) {
+  $ip6_full = str_replace('::', str_repeat(':', 9-substr_count($ip6, ':')), $ip6);
+  $ip6_full = str_replace('::', ':0:', $ip6_full);
+  $ip6_full = str_replace('::', ':0:', $ip6_full);
+  $ptr6 = '';
+  foreach (explode(':', $ip6_full) as $part) {
+    $ptr6 .= str_pad($part, 4, '0', STR_PAD_LEFT);
+  }
+  $ptr6 = implode('.', array_reverse(str_split($ptr6, 1))) . '.ip6.arpa';
+}
+
+$https_port = strpos($_SERVER['HTTP_HOST'], ':');
+if ($https_port === FALSE) {
+  $https_port = 443;
+} else {
+  $https_port = substr($_SERVER['HTTP_HOST'], $https_port+1);
+}
+
+$records = array();
+$records[] = array($mailcow_hostname, 'A', $ip);
+$records[] = array($ptr, 'PTR', $mailcow_hostname);
+if (!empty($ip6)) {
+  $records[] = array($mailcow_hostname, 'AAAA', $ip6);
+  $records[] = array($ptr6, 'PTR', $mailcow_hostname);
+}
+$domains = mailbox('get', 'domains');
+foreach(mailbox('get', 'domains') as $domain) {
+  $domains = array_merge($domains, mailbox('get', 'alias_domains', $domain));
+}
+
+if (!isset($autodiscover_config['sieve'])) {
+  $autodiscover_config['sieve'] = array('server' => $mailcow_hostname, 'port' => array_pop(explode(':', getenv('SIEVE_PORT'))));
+}
+
+$records[] = array('_25._tcp.'                                              . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], 25, 1));
+$records[] = array('_' . $https_port    . '._tcp.' . $mailcow_hostname, 'TLSA', generate_tlsa_digest($mailcow_hostname, $https_port));
+$records[] = array('_' . $autodiscover_config['pop3']['tlsport'] . '._tcp.' . $autodiscover_config['pop3']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['tlsport'], 1));
+$records[] = array('_' . $autodiscover_config['imap']['tlsport'] . '._tcp.' . $autodiscover_config['imap']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['tlsport'], 1));
+$records[] = array('_' . $autodiscover_config['smtp']['port']    . '._tcp.' . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['port']));
+$records[] = array('_' . $autodiscover_config['smtp']['tlsport'] . '._tcp.' . $autodiscover_config['smtp']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['smtp']['server'], $autodiscover_config['smtp']['tlsport'], 1));
+$records[] = array('_' . $autodiscover_config['imap']['port']    . '._tcp.' . $autodiscover_config['imap']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['imap']['server'], $autodiscover_config['imap']['port']));
+$records[] = array('_' . $autodiscover_config['pop3']['port']    . '._tcp.' . $autodiscover_config['pop3']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['pop3']['server'], $autodiscover_config['pop3']['port']));
+$records[] = array('_' . $autodiscover_config['sieve']['port']   . '._tcp.' . $autodiscover_config['sieve']['server'], 'TLSA', generate_tlsa_digest($autodiscover_config['sieve']['server'], $autodiscover_config['sieve']['port'], 1));
+
+foreach ($domains as $domain) {
+  $records[] = array($domain, 'MX', $mailcow_hostname);
+  $records[] = array('autodiscover.' . $domain, 'CNAME', $mailcow_hostname);
+  $records[] = array('_autodiscover._tcp.' . $domain, 'SRV', $mailcow_hostname . ' ' . $https_port);
+  $records[] = array('autoconfig.' . $domain, 'CNAME', $mailcow_hostname);
+  $records[] = array($domain, 'TXT', 'v=spf1 mx -all');
+  $records[] = array('_dmarc.' . $domain, 'TXT', 'v=DMARC1; p=reject', 'v=DMARC1; p=');
+  
+  if (!empty($dkim = dkim('details', $domain))) {
+    $records[] = array($dkim['dkim_selector'] . '._domainkey.' . $domain, 'TXT', $dkim['dkim_txt']);
+  }
+
+  $current_records = dns_get_record('_pop3._tcp.' . $domain, DNS_SRV);
+  if (count($current_records) == 0 || $current_records[0]['target'] != '') {
+    if ($autodiscover_config['pop3']['tlsport'] != '110')  {
+      $records[] = array('_pop3._tcp.' . $domain, 'SRV', $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['tlsport']);
+    }
+  } else {
+      $records[] = array('_pop3._tcp.' . $domain, 'SRV', '. 0');
+  }
+  $current_records = dns_get_record('_pop3s._tcp.' . $domain, DNS_SRV);
+  if (count($current_records) == 0 || $current_records[0]['target'] != '') {
+    if ($autodiscover_config['pop3']['port'] != '995')  {
+      $records[] = array('_pop3s._tcp.' . $domain, 'SRV', $autodiscover_config['pop3']['server'] . ' ' . $autodiscover_config['pop3']['port']);
+    }
+  } else {
+      $records[] = array('_pop3s._tcp.' . $domain, 'SRV', '. 0');
+  }
+  if ($autodiscover_config['imap']['tlsport'] != '143')  {
+    $records[] = array('_imap._tcp.' . $domain, 'SRV', $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['tlsport']);
+  }
+  if ($autodiscover_config['imap']['port'] != '993')  {
+    $records[] = array('_imaps._tcp.' . $domain, 'SRV', $autodiscover_config['imap']['server'] . ' ' . $autodiscover_config['imap']['port']);
+  }
+  if ($autodiscover_config['smtp']['tlsport'] != '587')  {
+    $records[] = array('_submission._tcp.' . $domain, 'SRV', $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['tlsport']);
+  }
+  if ($autodiscover_config['smtp']['port'] != '465')  {
+    $records[] = array('_smtps._tcp.' . $domain, 'SRV', $autodiscover_config['smtp']['server'] . ' ' . $autodiscover_config['smtp']['port']);
+  }
+  if ($autodiscover_config['sieve']['port'] != '4190')  {
+    $records[] = array('_sieve._tcp.' . $domain, 'SRV', $autodiscover_config['sieve']['server'] . ' ' . $autodiscover_config['sieve']['port']);
+  }
+}
+
+define('state_good',  "&#10003;");
+define('state_missing',   "&#x2717;");
+define('state_nomatch', "?");
+
+$record_types = array(
+  'A' => DNS_A,
+  'AAAA' => DNS_AAAA,
+  'CNAME' => DNS_CNAME,
+  'MX' => DNS_MX,
+  'PTR' => DNS_PTR,
+  'SRV' => DNS_SRV,
+  'TXT' => DNS_TXT,
+);
+$data_field = array(
+  'A' => 'ip',
+  'AAAA' => 'ipv6',
+  'CNAME' => 'target',
+  'MX' => 'target',
+  'PTR' => 'target',
+  'SRV' => 'data',
+  'TLSA' => 'data',
+  'TXT' => 'txt',
+);
+?>
+<div class="container">
+  <h3><?=$lang['diagnostics']['dns_records'];?></h3>
+  <p><?=$lang['diagnostics']['dns_records_24hours'];?></p>
+  <div class="table-responsive" id="dnstable">
+    <table class="table table-striped">
+      <tr> <th><?=$lang['diagnostics']['dns_records_name'];?></th> <th><?=$lang['diagnostics']['dns_records_type'];?></th> <th><?=$lang['diagnostics']['dns_records_data'];?></th ><th><?=$lang['diagnostics']['dns_records_status'];?></th> </tr>
+<?php
+foreach ($records as $record)
+{
+  $record[1] = strtoupper($record[1]);
+  $state = state_missing;
+  if ($record[1] == 'TLSA') {
+    $currents = dns_get_record($record[0], 52, $_, $_, TRUE);
+    foreach ($currents as &$current) {
+      $current['type'] = 'TLSA';
+      $current['cert_usage'] = hexdec(bin2hex($current['data']{0}));
+      $current['selector'] = hexdec(bin2hex($current['data']{1}));
+      $current['match_type'] = hexdec(bin2hex($current['data']{2}));
+      $current['cert_data'] = bin2hex(substr($current['data'], 3));
+      $current['data'] = $current['cert_usage'] . ' ' . $current['selector'] . ' ' . $current['match_type'] . ' ' . $current['cert_data'];
+    }
+    unset($current);
+  }
+  else {
+    $currents = dns_get_record($record[0], $record_types[$record[1]]);
+    if ($record[1] == 'SRV') {
+      foreach ($currents as &$current) {
+        if ($current['target'] == '') {
+          $current['target'] = '.';
+          $current['port'] = '0';
+        }
+        $current['data'] = $current['target'] . ' ' . $current['port'];
+      }
+      unset($current);
+    }
+  }
+  
+  if ($record[1] == 'CNAME' && count($currents) == 0) {
+    // A and AAAA are also valid instead of CNAME
+    $a = dns_get_record($record[0], DNS_A);
+    $cname = dns_get_record($record[2], DNS_A);
+    if (count($a) > 0 && count($cname) > 0) {
+      if ($a[0]['ip'] == $cname[0]['ip']) {
+        $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $record[2]));
+      
+        $aaaa = dns_get_record($record[0], DNS_AAAA);
+        $cname = dns_get_record($record[2], DNS_AAAA);
+        if (count($aaaa) == 0 || count($cname) == 0 || $aaaa[0]['ipv6'] != $cname[0]['ipv6']) {
+          $currents[0]['target'] = $aaaa[0]['ipv6'];
+        }
+      } else {
+        $currents = array(array('host' => $record[0], 'class' => 'IN', 'type' => 'CNAME', 'target' => $a[0]['ip']));
+      }
+    }
+  }
+  
+  foreach ($currents as $current) {
+    $current['type'] == strtoupper($current['type']);
+    if ($current['type'] != $record[1])
+    {
+      continue;
+    }
+    
+    elseif ($current['type'] == 'TXT' && strpos($record[0], '_dmarc.') === 0) {
+      $state = state_nomatch;
+      if (strpos($current[$data_field[$current['type']]], $record[3]) === 0)
+        $state = state_good . ' (' . current[$data_field[$current['type']]] . ')';
+    }
+    else if ($current['type'] == 'TXT' && strpos($current['txt'], 'v=spf1') === 0) {
+      $allowed = get_spf_allowed_hosts($record[0]);
+      $spf_ok = FALSE;
+      $spf_ok6 = FALSE;
+      foreach ($allowed as $net)
+      {
+        if (in_net($ip, $net))
+          $spf_ok = TRUE;
+        if (in_net($ip6, $net))
+          $spf_ok6 = TRUE;
+      }
+      if ($spf_ok && (empty($ip6) || $spf_ok6))
+        $state = state_good . ' (' . $current[$data_field[$current['type']]] . ')';
+    }
+    else if ($current['type'] != 'TXT' && isset($data_field[$current['type']]) && $state != state_good) {
+      $state = state_nomatch;
+      if ($current[$data_field[$current['type']]] == $record[2])
+        $state = state_good;
+    }
+  }
+  
+  if ($state == state_nomatch) {
+    $state = array();
+    foreach ($currents as $current) {
+      $state[] = $current[$data_field[$current['type']]];
+    }
+    $state = implode('<br />', $state);
+  }
+  
+  echo sprintf('<tr><td>%s</td><td>%s</td><td style="max-width: 300px; word-break: break-all">%s</td><td style="max-width: 150px; word-break: break-all">%s</td></tr>', $record[0], $record[1], $record[2], $state);
+}
+?>
+    </table>
+	</div>
+</div>
+<?php
+require_once("inc/footer.inc.php");
+} else {
+  header('Location: index.php');
+  exit();
+}
+?>

From 98be90c4943bf05f488d7cefc8299283336369df Mon Sep 17 00:00:00 2001
From: Michael Kuron <m.kuron@gmx.de>
Date: Mon, 10 Jul 2017 21:41:45 +0200
Subject: [PATCH 02/23] Remove SPF and DMARC checks

---
 diagnostics.php => data/web/diagnostics.php | 57 +++++----------------
 1 file changed, 13 insertions(+), 44 deletions(-)
 rename diagnostics.php => data/web/diagnostics.php (88%)

diff --git a/diagnostics.php b/data/web/diagnostics.php
similarity index 88%
rename from diagnostics.php
rename to data/web/diagnostics.php
index 4de0082e..5a199007 100644
--- a/diagnostics.php
+++ b/data/web/diagnostics.php
@@ -2,28 +2,10 @@
 require_once 'inc/prerequisites.inc.php';
 require_once 'inc/spf.inc.php';
 
-function in_net($addr, $net) {
-  $net = explode('/', $net);
-  if (count($net) > 1) {
-    $mask = $net[1];
-  }
-  $net = inet_pton($net[0]);
-  $addr = inet_pton($addr);
-  $length = strlen($net); // 4 for IPv4, 16 for IPv6
-  if (strlen($net) != strlen($addr)) {
-    return false;
-  }
-  if (!isset($mask)) {
-    $mask = $length * 8;
-  }
-  $addr_bin = '';
-  $net_bin = '';
-  for ($i = 0; $i < $length; ++$i) {
-    $addr_bin .= str_pad(decbin(ord(substr($addr, $i, $i+1))), 8, '0', STR_PAD_LEFT);
-    $net_bin .= str_pad(decbin(ord(substr($net, $i, $i+1))), 8, '0', STR_PAD_LEFT);
-  }
-  return substr($addr_bin, 0, $mask) == substr($net_bin, 0, $mask);
-}
+define('state_good',  "&#10003;");
+define('state_missing',   "&#x2717;");
+define('state_nomatch', "?");
+define('state_optional', "(optional)");
 
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
 require_once("inc/header.inc.php");
@@ -96,8 +78,8 @@ foreach ($domains as $domain) {
   $records[] = array('autodiscover.' . $domain, 'CNAME', $mailcow_hostname);
   $records[] = array('_autodiscover._tcp.' . $domain, 'SRV', $mailcow_hostname . ' ' . $https_port);
   $records[] = array('autoconfig.' . $domain, 'CNAME', $mailcow_hostname);
-  $records[] = array($domain, 'TXT', 'v=spf1 mx -all');
-  $records[] = array('_dmarc.' . $domain, 'TXT', 'v=DMARC1; p=reject', 'v=DMARC1; p=');
+  $records[] = array($domain, 'TXT', '<a href="http://www.openspf.org/SPF_Record_Syntax" target="_blank">SPF Record Syntax</a>', state_optional);
+  $records[] = array('_dmarc.' . $domain, 'TXT', '<a href="http://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>', state_optional);
   
   if (!empty($dkim = dkim('details', $domain))) {
     $records[] = array($dkim['dkim_selector'] . '._domainkey.' . $domain, 'TXT', $dkim['dkim_txt']);
@@ -136,10 +118,6 @@ foreach ($domains as $domain) {
   }
 }
 
-define('state_good',  "&#10003;");
-define('state_missing',   "&#x2717;");
-define('state_nomatch', "?");
-
 $record_types = array(
   'A' => DNS_A,
   'AAAA' => DNS_AAAA,
@@ -224,23 +202,10 @@ foreach ($records as $record)
     }
     
     elseif ($current['type'] == 'TXT' && strpos($record[0], '_dmarc.') === 0) {
-      $state = state_nomatch;
-      if (strpos($current[$data_field[$current['type']]], $record[3]) === 0)
-        $state = state_good . ' (' . current[$data_field[$current['type']]] . ')';
+      $state = state_optional . '<br />' . $current[$data_field[$current['type']]];
     }
     else if ($current['type'] == 'TXT' && strpos($current['txt'], 'v=spf1') === 0) {
-      $allowed = get_spf_allowed_hosts($record[0]);
-      $spf_ok = FALSE;
-      $spf_ok6 = FALSE;
-      foreach ($allowed as $net)
-      {
-        if (in_net($ip, $net))
-          $spf_ok = TRUE;
-        if (in_net($ip6, $net))
-          $spf_ok6 = TRUE;
-      }
-      if ($spf_ok && (empty($ip6) || $spf_ok6))
-        $state = state_good . ' (' . $current[$data_field[$current['type']]] . ')';
+      $state = state_optional . '<br />' . $current[$data_field[$current['type']]];
     }
     else if ($current['type'] != 'TXT' && isset($data_field[$current['type']]) && $state != state_good) {
       $state = state_nomatch;
@@ -249,6 +214,10 @@ foreach ($records as $record)
     }
   }
   
+  if (isset($record[3]) && $record[3] == state_optional && ($state == state_missing || $state == state_nomatch)) {
+    $state = state_optional;
+  }
+  
   if ($state == state_nomatch) {
     $state = array();
     foreach ($currents as $current) {
@@ -256,7 +225,7 @@ foreach ($records as $record)
     }
     $state = implode('<br />', $state);
   }
-  
+
   echo sprintf('<tr><td>%s</td><td>%s</td><td style="max-width: 300px; word-break: break-all">%s</td><td style="max-width: 150px; word-break: break-all">%s</td></tr>', $record[0], $record[1], $record[2], $state);
 }
 ?>

From 651c1cac23d77988521d96283043f476d3361ed8 Mon Sep 17 00:00:00 2001
From: Phoenix Eve Aspacio <aspaciop@gmail.com>
Date: Thu, 21 Sep 2017 07:22:33 +0800
Subject: [PATCH 03/23] Fixed broken link

---
 data/web/diagnostics.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/data/web/diagnostics.php b/data/web/diagnostics.php
index 5a199007..d35c4d47 100644
--- a/data/web/diagnostics.php
+++ b/data/web/diagnostics.php
@@ -10,7 +10,7 @@ define('state_optional', "(optional)");
 if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
 require_once("inc/header.inc.php");
 
-$ch = curl_init('http://ipv4.mailcow.email');
+$ch = curl_init('http://ip4.mailcow.email');
 curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
 curl_setopt($ch, CURLOPT_VERBOSE, false);
 curl_setopt($ch, CURLOPT_HEADER, false);
@@ -19,7 +19,7 @@ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
 $ip = curl_exec($ch);
 curl_close($ch);
 
-$ch = curl_init('http://ipv6.mailcow.email');
+$ch = curl_init('http://ip6.mailcow.email');
 curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
 curl_setopt($ch, CURLOPT_VERBOSE, false);
 curl_setopt($ch, CURLOPT_HEADER, false);

From f067a45bcb2a2c0b75fda17693297829bee64674 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Thu, 2 Nov 2017 09:51:58 +0100
Subject: [PATCH 04/23] [SOGo] Should fix some Android sync issues

---
 data/conf/sogo/sogo.conf | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/data/conf/sogo/sogo.conf b/data/conf/sogo/sogo.conf
index 86278abc..404fd451 100644
--- a/data/conf/sogo/sogo.conf
+++ b/data/conf/sogo/sogo.conf
@@ -43,7 +43,9 @@
     SOGoInternalSyncInterval = 30;
     SOGoMaximumSyncInterval = 354;
 
-    SOGoMaximumSyncWindowSize = 100;
+    // 100 seems to break some Android clients
+    //SOGoMaximumSyncWindowSize = 100;
+    // This should do the trick for Outlook 2016
     SOGoMaximumSyncResponseSize = 5172;
 
     WOWatchDogRequestTimeout = 10;

From b32e5adcc5d6e22fed6eefc94f769073978525e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Fri, 3 Nov 2017 20:25:38 +0100
Subject: [PATCH 05/23] [Dovecot] sieve_before/after maps in sql, changed dict
 names

---
 data/Dockerfiles/dovecot/Dockerfile           | 12 ++---
 data/Dockerfiles/dovecot/docker-entrypoint.sh | 50 ++++++++++++++++++-
 2 files changed, 54 insertions(+), 8 deletions(-)

diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile
index d7f9cf38..d2cdd0e3 100644
--- a/data/Dockerfiles/dovecot/Dockerfile
+++ b/data/Dockerfiles/dovecot/Dockerfile
@@ -3,8 +3,8 @@ LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
 ENV LC_ALL C
-ENV DOVECOT_VERSION 2.2.32
-ENV PIGEONHOLE_VERSION 0.4.20
+ENV DOVECOT_VERSION 2.2.33.2
+ENV PIGEONHOLE_VERSION 0.4.21
 
 RUN apt-get update && apt-get -y install \
 	automake \
@@ -40,10 +40,11 @@ RUN apt-get update && apt-get -y install \
 	libtest-pod-perl \
 	libtest-simple-perl \
 	libunicode-string-perl \
-    libproc-processtable-perl \
+  libproc-processtable-perl \
 	liburi-perl \
 	lzma-dev \
 	make \
+  procps \
 	supervisor \
 	syslog-ng \
 	syslog-ng-core \
@@ -64,7 +65,8 @@ RUN curl https://www.dovecot.org/releases/2.2/dovecot-$DOVECOT_VERSION.tar.gz |
 	&& make -j3 \
 	&& make install \
 	&& make clean \
-	&& cd .. && rm -rf dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION
+	&& cd .. \
+  && rm -rf dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION
 
 RUN cpanm Data::Uniqid Mail::IMAPClient String::Util
 RUN echo '* * * * *   root   /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync
@@ -98,8 +100,6 @@ RUN touch /etc/default/locale
 RUN apt-get purge -y build-essential automake autotools-dev \
 	&& apt-get autoremove --purge -y
 
-EXPOSE 24 10001
-
 ENTRYPOINT ["/docker-entrypoint.sh"]
 CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
 
diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh
index 4e9fe14b..9f8f5313 100755
--- a/data/Dockerfiles/dovecot/docker-entrypoint.sh
+++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh
@@ -15,7 +15,7 @@ sed -i "/^\$DBNAME/c\\\$DBNAME='${DBNAME}';" /usr/local/bin/imapsync_cron.pl
 DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g')
 
 # Create quota dict for Dovecot
-cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql.conf
+cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-quota.conf
 connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
 map {
   pattern = priv/quota/storage
@@ -31,8 +31,54 @@ map {
 }
 EOF
 
+# Create dict used for sieve pre and postfilters
+cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
+connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
+map {
+  pattern = priv/sieve/name/\$script_name
+  table = sieve_before
+  username_field = username
+  value_field = id
+  fields {
+    script_name = \$script_name
+  }
+}
+map {
+  pattern = priv/sieve/data/\$id
+  table = sieve_before
+  username_field = username
+  value_field = script_data
+  fields {
+    id = \$id
+  }
+}
+EOF
+
+cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
+connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
+map {
+  pattern = priv/sieve/name/\$script_name
+  table = sieve_after
+  username_field = username
+  value_field = id
+  fields {
+    script_name = \$script_name
+  }
+}
+map {
+  pattern = priv/sieve/data/\$id
+  table = sieve_after
+  username_field = username
+  value_field = script_data
+  fields {
+    id = \$id
+  }
+}
+EOF
+
+
 # Create user and pass dict for Dovecot
-cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-mysql.conf
+cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql-passdb.conf
 driver = mysql
 connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
 default_pass_scheme = SSHA256

From 21e20f378686a031197f1a785409beaf003cc421 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Fri, 3 Nov 2017 20:25:43 +0100
Subject: [PATCH 06/23] [Dovecot] sieve_before/after maps in sql, changed dict
 names

---
 data/conf/dovecot/dovecot.conf | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf
index 59362958..a9e6f0af 100644
--- a/data/conf/dovecot/dovecot.conf
+++ b/data/conf/dovecot/dovecot.conf
@@ -32,7 +32,7 @@ passdb {
   pass = yes
 }
 passdb {
-  args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf
+  args = /usr/local/etc/dovecot/sql/dovecot-dict-sql-passdb.conf
   driver = sql
 }
 # Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
@@ -211,7 +211,7 @@ listen = *,[::]
 ssl_cert = </etc/ssl/mail/cert.pem
 ssl_key = </etc/ssl/mail/key.pem
 userdb {
-  args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf
+  args = /usr/local/etc/dovecot/sql/dovecot-dict-sql-passdb.conf
   driver = sql
 }
 protocol imap {
@@ -245,17 +245,21 @@ plugin {
   # END
   sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve
   sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute
-  sieve_after = /var/vmail/sieve/global.sieve
   sieve_max_script_size = 1M
   sieve_quota_max_scripts = 0
   sieve_quota_max_storage = 0
   listescape_char = "\\"
+  sieve_before = dict:proxy::sieve_before;name=active
+  sieve_after = dict:proxy::sieve_after;name=active
+  sieve_after2 = /var/vmail/sieve/global.sieve
   #mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
   #mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
   #mail_crypt_save_version = 2
 }
 dict {
-  sqlquota = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql.conf
+  sqlquota = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql-quota.conf
+  sieve_after = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
+  sieve_before = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
 }
 remote 127.0.0.1 {
   disable_plaintext_auth = no

From a9f64a34726f6d0e36422a2e1dd755e0183650a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Fri, 3 Nov 2017 20:26:09 +0100
Subject: [PATCH 07/23] [Dockerapi] Return answers in json

---
 data/Dockerfiles/dockerapi/server.py | 66 ++++++++++++++++++++--------
 1 file changed, 47 insertions(+), 19 deletions(-)

diff --git a/data/Dockerfiles/dockerapi/server.py b/data/Dockerfiles/dockerapi/server.py
index aabcaf31..b406b5ee 100644
--- a/data/Dockerfiles/dockerapi/server.py
+++ b/data/Dockerfiles/dockerapi/server.py
@@ -1,6 +1,7 @@
 from flask import Flask
 from flask_restful import Resource, Api
 from flask import jsonify
+from flask import request
 from threading import Thread
 import docker
 import signal
@@ -13,17 +14,23 @@ api = Api(app)
 class containers_get(Resource):
   def get(self):
     containers = {}
-    for container in docker_client.containers.list(all=True):
-      containers.update({container.attrs['Id']: container.attrs})
-    return containers
+    try:
+      for container in docker_client.containers.list(all=True):
+        containers.update({container.attrs['Id']: container.attrs})
+      return containers
+    except Exception as e:
+      return jsonify(type='danger', msg=e)
 
 class container_get(Resource):
   def get(self, container_id):
     if container_id and container_id.isalnum():
-      for container in docker_client.containers.list(all=True, filters={"id": container_id}):
-        return container.attrs            
+      try:
+        for container in docker_client.containers.list(all=True, filters={"id": container_id}):
+          return container.attrs
+      except Exception as e:
+          return jsonify(type='danger', msg=e)
     else:
-      return jsonify(message='No or invalid id defined')
+      return jsonify(type='danger', msg='no or invalid id defined')
 
 class container_post(Resource):
   def post(self, container_id, post_action):
@@ -32,30 +39,51 @@ class container_post(Resource):
         try:
           for container in docker_client.containers.list(all=True, filters={"id": container_id}):
             container.stop()
-        except:
-          return 'Error'
-        else:
-          return 'OK'
+          return jsonify(type='success', msg='command completed successfully')
+        except Exception as e:
+          return jsonify(type='danger', msg=e)
+
       elif post_action == 'start':
         try:
           for container in docker_client.containers.list(all=True, filters={"id": container_id}):
             container.start()
-        except:
-          return 'Error'
-        else:
-          return 'OK'
+          return jsonify(type='success', msg='command completed successfully')
+        except Exception as e:
+          return jsonify(type='danger', msg=e)
+
       elif post_action == 'restart':
         try:
           for container in docker_client.containers.list(all=True, filters={"id": container_id}):
             container.restart()
-        except:
-          return 'Error'
+          return jsonify(type='success', msg='command completed successfully')
+        except Exception as e:
+          return jsonify(type='danger', msg=e)
+
+      elif post_action == 'exec':
+
+        if not request.json or not 'cmd' in request.json:
+          return jsonify(type='danger', msg='cmd is missing')
+
+        if request.json['cmd'] == 'sieve_list' and request.json['username']:
+          try:
+            for container in docker_client.containers.list(filters={"id": container_id}):
+              return container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve list -u '" + request.json['username'].replace("'", "'\\''") + "'"], user='vmail')
+          except Exception as e:
+            return jsonify(type='danger', msg=e)
+        elif request.json['cmd'] == 'sieve_print' and request.json['script_name'] and request.json['username']:
+          try:
+            for container in docker_client.containers.list(filters={"id": container_id}):
+              return container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm sieve get -u '" + request.json['username'].replace("'", "'\\''") + "' '" + request.json['script_name'].replace("'", "'\\''") + "'"], user='vmail')
+          except Exception as e:
+            return jsonify(type='danger', msg=e)
         else:
-          return 'OK'
+          return jsonify(type='danger', msg='Unknown command')
+
       else:
-        return jsonify(message='Invalid action')
+        return jsonify(type='danger', msg='invalid action')
+
     else:
-      return jsonify(message='Invalid container id or missing action')
+      return jsonify(type='danger', msg='invalid container id or missing action')
 
 class GracefulKiller:
   kill_now = False

From b16684ce204b99d24583da8b6622ad180c870fb9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Fri, 3 Nov 2017 20:26:36 +0100
Subject: [PATCH 08/23] [Rspamd] Slightly reduce map watch interval

---
 data/conf/rspamd/local.d/options.inc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/data/conf/rspamd/local.d/options.inc b/data/conf/rspamd/local.d/options.inc
index 09117984..f915a151 100644
--- a/data/conf/rspamd/local.d/options.inc
+++ b/data/conf/rspamd/local.d/options.inc
@@ -1,7 +1,7 @@
 dns {
   enable_dnssec = true;
 }
-map_watch_interval = 60s;
+map_watch_interval = 30s;
 dns {
   timeout = 4s;
   retransmits = 5;

From 1ef10f135889985577e1eb34239a214953f64afd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Fri, 3 Nov 2017 20:27:43 +0100
Subject: [PATCH 09/23] [PHP-FPM] Include net_sieve, test removal of
 usr/src/php for size

---
 data/Dockerfiles/phpfpm/Dockerfile | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile
index fc6552fd..6a1e90a3 100644
--- a/data/Dockerfiles/phpfpm/Dockerfile
+++ b/data/Dockerfiles/phpfpm/Dockerfile
@@ -36,6 +36,7 @@ RUN apk add -U --no-cache libxml2-dev \
   && pear install channel://pear.php.net/Net_IDNA2-0.2.0 \
     channel://pear.php.net/Auth_SASL-1.1.0 \
     Net_IMAP \
+    Net_Sieve \
     NET_SMTP \
     Mail_mime \
 	&& pecl install redis-${REDIS_PECL} memcached-${MEMCACHED_PECL} APCu-${APCU_PECL} imagick-${IMAGICK_PECL} \
@@ -54,7 +55,8 @@ RUN apk add -U --no-cache libxml2-dev \
   echo 'opcache.memory_consumption=128'; \
   echo 'opcache.save_comments=1'; \
   echo 'opcache.revalidate_freq=1'; \
-} > /usr/local/etc/php/conf.d/opcache-recommended.ini
+} > /usr/local/etc/php/conf.d/opcache-recommended.ini \
+  && rm -rf /usr/src/php*
 
 
 COPY ./docker-entrypoint.sh /

From 85d1ee2f49ed350529fb3fd829268c4cdf8dfbd3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Fri, 3 Nov 2017 20:37:24 +0100
Subject: [PATCH 10/23] [Web] Autodiscover returns given password decoded and
 trimed; Add sieve pre and post filters to UI; Move ajax called files; Rework
 log system: 100 entries per default, add more per click; Syncjobs: Do not
 read log to data attribute

---
 data/web/admin.php                            |  56 +-
 data/web/api.php                              |  32 -
 data/web/autodiscover.php                     |   3 +-
 data/web/css/footable.bootstrap.min.css       |   5 +-
 data/web/css/mailbox.css                      |   2 +-
 data/web/css/numberedtextarea.min.css         |   1 +
 data/web/css/user.css                         |   7 +
 data/web/edit.php                             |  85 +-
 data/web/inc/ajax/sieve_validation.php        |  23 +
 data/web/inc/ajax/sogo_ctrl.php               |  39 +
 data/web/inc/ajax/syncjob_logs.php            |  15 +
 data/web/inc/footer.inc.php                   |  16 +-
 ...sogo_ctrl.php => functions.docker.inc.php} |  39 +-
 data/web/inc/functions.inc.php                |  63 +-
 data/web/inc/functions.mailbox.inc.php        | 376 ++++++++-
 data/web/inc/header.inc.php                   |   1 +
 data/web/inc/init_db.inc.php                  |  46 +-
 data/web/inc/lib/sieve/SieveDumpable.php      |   7 +
 data/web/inc/lib/sieve/SieveException.php     |  47 ++
 .../inc/lib/sieve/SieveKeywordRegistry.php    | 233 +++++
 data/web/inc/lib/sieve/SieveParser.php        | 255 ++++++
 data/web/inc/lib/sieve/SieveScanner.php       | 145 ++++
 data/web/inc/lib/sieve/SieveScript.php        |   6 +
 data/web/inc/lib/sieve/SieveSemantics.php     | 611 ++++++++++++++
 data/web/inc/lib/sieve/SieveToken.php         |  88 ++
 data/web/inc/lib/sieve/SieveTree.php          | 117 +++
 data/web/inc/lib/sieve/extensions/body.xml    |  14 +
 .../extensions/comparator-ascii-numeric.xml   |   7 +
 data/web/inc/lib/sieve/extensions/copy.xml    |   9 +
 data/web/inc/lib/sieve/extensions/date.xml    |  28 +
 .../inc/lib/sieve/extensions/editheader.xml   |  22 +
 .../web/inc/lib/sieve/extensions/envelope.xml |  13 +
 .../inc/lib/sieve/extensions/environment.xml  |  13 +
 data/web/inc/lib/sieve/extensions/ereject.xml |  11 +
 .../web/inc/lib/sieve/extensions/fileinto.xml |   9 +
 .../inc/lib/sieve/extensions/imap4flags.xml   |  29 +
 .../inc/lib/sieve/extensions/imapflags.xml    |  21 +
 data/web/inc/lib/sieve/extensions/index.xml   |  17 +
 data/web/inc/lib/sieve/extensions/notify.xml  |  29 +
 data/web/inc/lib/sieve/extensions/regex.xml   |  11 +
 data/web/inc/lib/sieve/extensions/reject.xml  |  11 +
 .../inc/lib/sieve/extensions/relational.xml   |  14 +
 .../web/inc/lib/sieve/extensions/spamtest.xml |  11 +
 .../inc/lib/sieve/extensions/spamtestplus.xml |  12 +
 .../inc/lib/sieve/extensions/subaddress.xml   |   8 +
 .../web/inc/lib/sieve/extensions/vacation.xml |  31 +
 .../inc/lib/sieve/extensions/variables.xml    |  21 +
 .../inc/lib/sieve/extensions/virustest.xml    |  11 +
 data/web/inc/lib/sieve/keywords.xml           |  91 ++
 data/web/inc/prerequisites.inc.php            |   4 +
 data/web/inc/sessions.inc.php                 |   2 +
 data/web/inc/vars.inc.php                     |   3 +
 data/web/js/admin.js                          | 794 +++++++-----------
 data/web/js/api.js                            |  13 +-
 data/web/js/edit.js                           |   1 +
 data/web/js/mailbox.js                        | 135 ++-
 data/web/js/numberedtextarea.min.js           |   1 +
 data/web/js/user.js                           |  47 +-
 data/web/json_api.php                         | 219 ++++-
 data/web/lang/lang.de.php                     |  24 +-
 data/web/lang/lang.en.php                     |  20 +
 data/web/mailbox.php                          |  36 +-
 data/web/modals/mailbox.php                   |  79 +-
 data/web/modals/user.php                      |  23 +-
 data/web/user.php                             |   7 +
 65 files changed, 3460 insertions(+), 709 deletions(-)
 delete mode 100644 data/web/api.php
 create mode 100644 data/web/css/numberedtextarea.min.css
 create mode 100644 data/web/inc/ajax/sieve_validation.php
 create mode 100644 data/web/inc/ajax/sogo_ctrl.php
 create mode 100644 data/web/inc/ajax/syncjob_logs.php
 rename data/web/inc/{call_sogo_ctrl.php => functions.docker.inc.php} (63%)
 create mode 100644 data/web/inc/lib/sieve/SieveDumpable.php
 create mode 100644 data/web/inc/lib/sieve/SieveException.php
 create mode 100644 data/web/inc/lib/sieve/SieveKeywordRegistry.php
 create mode 100644 data/web/inc/lib/sieve/SieveParser.php
 create mode 100644 data/web/inc/lib/sieve/SieveScanner.php
 create mode 100644 data/web/inc/lib/sieve/SieveScript.php
 create mode 100644 data/web/inc/lib/sieve/SieveSemantics.php
 create mode 100644 data/web/inc/lib/sieve/SieveToken.php
 create mode 100644 data/web/inc/lib/sieve/SieveTree.php
 create mode 100644 data/web/inc/lib/sieve/extensions/body.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/copy.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/date.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/editheader.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/envelope.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/environment.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/ereject.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/fileinto.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/imap4flags.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/imapflags.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/index.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/notify.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/regex.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/reject.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/relational.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/spamtest.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/spamtestplus.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/subaddress.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/vacation.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/variables.xml
 create mode 100644 data/web/inc/lib/sieve/extensions/virustest.xml
 create mode 100644 data/web/inc/lib/sieve/keywords.xml
 create mode 100644 data/web/js/numberedtextarea.min.js

diff --git a/data/web/admin.php b/data/web/admin.php
index a2ec3d94..06af3db9 100644
--- a/data/web/admin.php
+++ b/data/web/admin.php
@@ -457,12 +457,11 @@ $tfa_data = get_tfa();
 
   <div role="tabpanel" class="tab-pane" id="tab-postfix-logs">
     <div class="panel panel-default">
-      <div class="panel-heading">Postfix
+      <div class="panel-heading">Postfix <span class="badge badge-info log-lines"></span>
         <div class="btn-group pull-right">
-          <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
-          <ul class="dropdown-menu">
-            <li><a href="#" id="refresh_postfix_log"><?=$lang['admin']['refresh'];?></a></li>
-          </ul>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="postfix_log" data-log-url="postfix" data-nrows="100">+ 100</button>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="postfix_log" data-log-url="postfix" data-nrows="1000">+ 1000</button>
+          <button class="btn btn-xs btn-default" id="refresh_postfix_log"><?=$lang['admin']['refresh'];?></button>
         </div>
       </div>
       <div class="panel-body">
@@ -475,12 +474,11 @@ $tfa_data = get_tfa();
 
   <div role="tabpanel" class="tab-pane" id="tab-dovecot-logs">
     <div class="panel panel-default">
-      <div class="panel-heading">Dovecot
+      <div class="panel-heading">Dovecot <span class="badge badge-info log-lines"></span>
         <div class="btn-group pull-right">
-          <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
-          <ul class="dropdown-menu">
-            <li><a href="#" id="refresh_dovecot_log"><?=$lang['admin']['refresh'];?></a></li>
-          </ul>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="dovecot_log" data-log-url="dovecot" data-nrows="100">+ 100</button>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="dovecot_log" data-log-url="dovecot" data-nrows="1000">+ 1000</button>
+          <button class="btn btn-xs btn-default" id="refresh_dovecot_log"><?=$lang['admin']['refresh'];?></button>
         </div>
       </div>
       <div class="panel-body">
@@ -493,12 +491,11 @@ $tfa_data = get_tfa();
 
   <div role="tabpanel" class="tab-pane" id="tab-sogo-logs">
     <div class="panel panel-default">
-      <div class="panel-heading">SOGo
+      <div class="panel-heading">SOGo <span class="badge badge-info log-lines"></span>
         <div class="btn-group pull-right">
-          <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
-          <ul class="dropdown-menu">
-            <li><a href="#" id="refresh_sogo_log"><?=$lang['admin']['refresh'];?></a></li>
-          </ul>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="sogo_log" data-log-url="sogo" data-nrows="100">+ 100</button>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="sogo_log" data-log-url="sogo" data-nrows="1000">+ 1000</button>
+          <button class="btn btn-xs btn-default" id="refresh_sogo_log"><?=$lang['admin']['refresh'];?></button>
         </div>
       </div>
       <div class="panel-body">
@@ -511,12 +508,11 @@ $tfa_data = get_tfa();
 
   <div role="tabpanel" class="tab-pane" id="tab-fail2ban-logs">
     <div class="panel panel-default">
-      <div class="panel-heading">Fail2ban
+      <div class="panel-heading">Fail2ban <span class="badge badge-info log-lines"></span>
         <div class="btn-group pull-right">
-          <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
-          <ul class="dropdown-menu">
-            <li><a href="#" id="refresh_fail2ban_log"><?=$lang['admin']['refresh'];?></a></li>
-          </ul>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="fail2ban_log" data-log-url="fail2ban" data-nrows="100">+ 100</button>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="general_syslog" data-table="fail2ban_log" data-log-url="fail2ban" data-nrows="1000">+ 1000</button>
+          <button class="btn btn-xs btn-default" id="refresh_fail2ban_log"><?=$lang['admin']['refresh'];?></button>
         </div>
       </div>
       <div class="panel-body">
@@ -529,17 +525,16 @@ $tfa_data = get_tfa();
 
   <div role="tabpanel" class="tab-pane" id="tab-rspamd-history">
     <div class="panel panel-default">
-      <div class="panel-heading">Rspamd history
+      <div class="panel-heading">Rspamd history <span class="badge badge-info log-lines"></span>
         <div class="btn-group pull-right">
-          <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
-          <ul class="dropdown-menu">
-            <li><a href="#" id="refresh_rspamd_history"><?=$lang['admin']['refresh'];?></a></li>
-          </ul>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd-history" data-nrows="100">+ 100</button>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="rspamd_history" data-table="rspamd_history" data-log-url="rspamd-history" data-nrows="1000">+ 1000</button>
+          <button class="btn btn-xs btn-default" id="refresh_rspamd_history"><?=$lang['admin']['refresh'];?></button>
         </div>
       </div>
       <div class="panel-body">
         <div class="table-responsive">
-          <table class="table table-striped table-condensed" id="rspamd_history"></table>
+          <table class="table table-striped table-condensed log-table" id="rspamd_history"></table>
         </div>
       </div>
     </div>
@@ -547,12 +542,11 @@ $tfa_data = get_tfa();
 
   <div role="tabpanel" class="tab-pane" id="tab-autodiscover-logs">
     <div class="panel panel-default">
-      <div class="panel-heading">Autodiscover
+      <div class="panel-heading">Autodiscover <span class="badge badge-info log-lines"></span>
         <div class="btn-group pull-right">
-          <a class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['admin']['action'];?> <span class="caret"></span></a>
-          <ul class="dropdown-menu">
-            <li><a href="#" id="refresh_autodiscover_log"><?=$lang['admin']['refresh'];?></a></li>
-          </ul>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="autodiscover_log" data-table="autodiscover_log" data-log-url="autodiscover" data-nrows="100">+ 100</button>
+          <button class="btn btn-xs btn-default add_log_lines" data-post-process="autodiscover_log" data-table="autodiscover_log" data-log-url="autodiscover" data-nrows="1000">+ 1000</button>
+          <button class="btn btn-xs btn-default" id="refresh_autodiscover_log"><?=$lang['admin']['refresh'];?></button>
         </div>
       </div>
       <div class="panel-body">
diff --git a/data/web/api.php b/data/web/api.php
deleted file mode 100644
index d250929d..00000000
--- a/data/web/api.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-set_time_limit (0);
-
-$address = '0.0.0.0';
-
-$port = 7777;
-$con = 1;
-$word = "";
-
-$sock = socket_create(AF_INET, SOCK_STREAM, 0);
-$bind = socket_bind($sock, $address, $port);
-
-socket_listen($sock);
-
-while ($con == 1)
-{
-    $client = socket_accept($sock);
-    $input = socket_read($client, 2024);
-
-    if ($input == 'exit') 
-    {
-        $close = socket_close($sock);
-        $con = 0;
-    }
-
-    if($con == 1)
-    {
-        $word .= $input;
-    }
-}
-
-echo $word;
diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php
index 5e0ccf30..2f5fb2c9 100644
--- a/data/web/autodiscover.php
+++ b/data/web/autodiscover.php
@@ -35,7 +35,8 @@ $opt = [
 ];
 $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
 $login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
-$login_role = check_login($login_user, $_SERVER['PHP_AUTH_PW']);
+$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
+$login_role = check_login($login_user, $login_pass);
 
 if (!isset($_SERVER['PHP_AUTH_USER']) OR $login_role !== "user") {
   try {
diff --git a/data/web/css/footable.bootstrap.min.css b/data/web/css/footable.bootstrap.min.css
index a030a7ff..00e96c91 100644
--- a/data/web/css/footable.bootstrap.min.css
+++ b/data/web/css/footable.bootstrap.min.css
@@ -1 +1,4 @@
-table.footable-details,table.footable>thead>tr.footable-filtering>th div.form-group{margin-bottom:0}table.footable,table.footable-details{position:relative;width:100%;border-spacing:0;border-collapse:collapse}table.footable-hide-fouc{display:none}table>tbody>tr>td>span.footable-toggle{margin-right:8px;opacity:.3}table>tbody>tr>td>span.footable-toggle.last-column{margin-left:8px;float:right}table.table-condensed>tbody>tr>td>span.footable-toggle{margin-right:5px}table.footable-details>tbody>tr>th:nth-child(1){min-width:40px;width:120px}table.footable-details>tbody>tr>td:nth-child(2){word-break:break-all}table.footable-details>tbody>tr:first-child>td,table.footable-details>tbody>tr:first-child>th,table.footable-details>tfoot>tr:first-child>td,table.footable-details>tfoot>tr:first-child>th,table.footable-details>thead>tr:first-child>td,table.footable-details>thead>tr:first-child>th{border-top-width:0}table.footable-details.table-bordered>tbody>tr:first-child>td,table.footable-details.table-bordered>tbody>tr:first-child>th,table.footable-details.table-bordered>tfoot>tr:first-child>td,table.footable-details.table-bordered>tfoot>tr:first-child>th,table.footable-details.table-bordered>thead>tr:first-child>td,table.footable-details.table-bordered>thead>tr:first-child>th{border-top-width:1px}div.footable-loader{vertical-align:middle;text-align:center;height:300px;position:relative}div.footable-loader>span.fooicon{display:inline-block;opacity:.3;font-size:30px;line-height:32px;width:32px;height:32px;margin-top:-16px;margin-left:-16px;position:absolute;top:50%;left:50%;-webkit-animation:fooicon-spin-r 2s infinite linear;animation:fooicon-spin-r 2s infinite linear}table.footable>tbody>tr.footable-empty>td{vertical-align:middle;text-align:center;font-size:30px}table.footable>tbody>tr>td,table.footable>tbody>tr>th{display:none}table.footable>tbody>tr.footable-detail-row>td,table.footable>tbody>tr.footable-detail-row>th,table.footable>tbody>tr.footable-empty>td,table.footable>tbody>tr.footable-empty>th{display:table-cell}@-webkit-keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fooicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings'!important;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fooicon:after,.fooicon:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.fooicon-loader:before{content:"\e030"}.fooicon-plus:before{content:"\2b"}.fooicon-minus:before{content:"\2212"}.fooicon-search:before{content:"\e003"}.fooicon-remove:before{content:"\e014"}.fooicon-sort:before{content:"\e150"}.fooicon-sort-asc:before{content:"\e155"}.fooicon-sort-desc:before{content:"\e156"}.fooicon-pencil:before{content:"\270f"}.fooicon-trash:before{content:"\e020"}.fooicon-eye-close:before{content:"\e106"}.fooicon-flash:before{content:"\e162"}.fooicon-cog:before{content:"\e019"}.fooicon-stats:before{content:"\e185"}table.footable>thead>tr.footable-filtering>th{border-bottom-width:1px;font-weight:400}table.footable.footable-filtering-right>thead>tr.footable-filtering>th,table.footable>thead>tr.footable-filtering>th{text-align:right}table.footable.footable-filtering-left>thead>tr.footable-filtering>th{text-align:left}table.footable-paging-center>tfoot>tr.footable-paging>td,table.footable.footable-filtering-center>thead>tr.footable-filtering>th,table.footable>tfoot>tr.footable-paging>td{text-align:center}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:5px}table.footable>thead>tr.footable-filtering>th div.input-group{width:100%}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox{margin:0;display:block;position:relative}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox>label{display:block;padding-left:20px}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox input[type=checkbox]{position:absolute;margin-left:-20px}@media (min-width:768px){table.footable>thead>tr.footable-filtering>th div.input-group{width:auto}table.footable>thead>tr.footable-filtering>th div.form-group{margin-left:2px;margin-right:2px}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:0}}table.footable>tbody>tr>td.footable-sortable,table.footable>tbody>tr>th.footable-sortable,table.footable>tfoot>tr>td.footable-sortable,table.footable>tfoot>tr>th.footable-sortable,table.footable>thead>tr>td.footable-sortable,table.footable>thead>tr>th.footable-sortable{position:relative;padding-right:30px;cursor:pointer}td.footable-sortable>span.fooicon,th.footable-sortable>span.fooicon{position:absolute;right:6px;top:50%;margin-top:-7px;opacity:0;transition:opacity .3s ease-in}td.footable-sortable.footable-asc>span.fooicon,td.footable-sortable.footable-desc>span.fooicon,td.footable-sortable:hover>span.fooicon,th.footable-sortable.footable-asc>span.fooicon,th.footable-sortable.footable-desc>span.fooicon,th.footable-sortable:hover>span.fooicon{opacity:1}table.footable-sorting-disabled td.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled td.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled td.footable-sortable:hover>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled th.footable-sortable:hover>span.fooicon{opacity:0;visibility:hidden}table.footable>tfoot>tr.footable-paging>td>ul.pagination{margin:10px 0 0}table.footable>tfoot>tr.footable-paging>td>span.label{display:inline-block;margin:0 0 10px;padding:4px 10px}table.footable-paging-left>tfoot>tr.footable-paging>td{text-align:left}table.footable-editing-right td.footable-editing,table.footable-editing-right tr.footable-editing,table.footable-paging-right>tfoot>tr.footable-paging>td{text-align:right}ul.pagination>li.footable-page{display:none}ul.pagination>li.footable-page.visible{display:inline}td.footable-editing{width:90px;max-width:90px}table.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit td.footable-editing,table.footable-editing-no-view td.footable-editing{width:70px;max-width:70px}table.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit.footable-editing-no-view td.footable-editing{width:50px;max-width:50px}table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view th.footable-editing{width:0;max-width:0;display:none!important}table.footable-editing-left td.footable-editing,table.footable-editing-left tr.footable-editing{text-align:left}table.footable-editing button.footable-add,table.footable-editing button.footable-hide,table.footable-editing-show button.footable-show,table.footable-editing.footable-editing-always-show button.footable-hide,table.footable-editing.footable-editing-always-show button.footable-show,table.footable-editing.footable-editing-always-show.footable-editing-no-add tr.footable-editing{display:none}table.footable-editing.footable-editing-always-show button.footable-add,table.footable-editing.footable-editing-show button.footable-add,table.footable-editing.footable-editing-show button.footable-hide{display:inline-block}
\ No newline at end of file
+table.footable-details,table.footable>thead>tr.footable-filtering>th div.form-group{margin-bottom:0}table.footable,table.footable-details{position:relative;width:100%;border-spacing:0;border-collapse:collapse}table.footable-hide-fouc{display:none}table>tbody>tr>td>span.footable-toggle{margin-right:8px;opacity:.3}table>tbody>tr>td>span.footable-toggle.last-column{margin-left:8px;float:right}table.table-condensed>tbody>tr>td>span.footable-toggle{margin-right:5px}table.footable-details>tbody>tr>th:nth-child(1){min-width:40px;width:120px}table.footable-details>tbody>tr>td:nth-child(2){word-break:break-all}table.footable-details>tbody>tr:first-child>td,table.footable-details>tbody>tr:first-child>th,table.footable-details>tfoot>tr:first-child>td,table.footable-details>tfoot>tr:first-child>th,table.footable-details>thead>tr:first-child>td,table.footable-details>thead>tr:first-child>th{border-top-width:0}table.footable-details.table-bordered>tbody>tr:first-child>td,table.footable-details.table-bordered>tbody>tr:first-child>th,table.footable-details.table-bordered>tfoot>tr:first-child>td,table.footable-details.table-bordered>tfoot>tr:first-child>th,table.footable-details.table-bordered>thead>tr:first-child>td,table.footable-details.table-bordered>thead>tr:first-child>th{border-top-width:1px}div.footable-loader{vertical-align:middle;text-align:center;height:300px;position:relative}div.footable-loader>span.fooicon{display:inline-block;opacity:.3;font-size:30px;line-height:32px;width:32px;height:32px;margin-top:-16px;margin-left:-16px;position:absolute;top:50%;left:50%;-webkit-animation:fooicon-spin-r 2s infinite linear;animation:fooicon-spin-r 2s infinite linear}table.footable>tbody>tr.footable-empty>td{vertical-align:middle;text-align:center;font-size:30px}table.footable>tbody>tr>td,table.footable>tbody>tr>th{display:none}table.footable>tbody>tr.footable-detail-row>td,table.footable>tbody>tr.footable-detail-row>th,table.footable>tbody>tr.footable-empty>td,table.footable>tbody>tr.footable-empty>th{display:table-cell}@-webkit-keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fooicon-spin-r{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fooicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings'!important;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fooicon:after,.fooicon:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.fooicon-loader:before{content:"\e030"}.fooicon-plus:before{content:"\2b"}.fooicon-minus:before{content:"\2212"}.fooicon-search:before{content:"\e003"}.fooicon-remove:before{content:"\e014"}.fooicon-sort:before{content:"\e150"}.fooicon-sort-asc:before{content:"\e155"}.fooicon-sort-desc:before{content:"\e156"}.fooicon-pencil:before{content:"\270f"}.fooicon-trash:before{content:"\e020"}.fooicon-eye-close:before{content:"\e106"}.fooicon-flash:before{content:"\e162"}.fooicon-cog:before{content:"\e019"}.fooicon-stats:before{content:"\e185"}table.footable>thead>tr.footable-filtering>th{border-bottom-width:1px;font-weight:400}table.footable.footable-filtering-right>thead>tr.footable-filtering>th,table.footable>thead>tr.footable-filtering>th{text-align:right}table.footable.footable-filtering-left>thead>tr.footable-filtering>th{text-align:left}table.footable-paging-center>tfoot>tr.footable-paging>td,table.footable.footable-filtering-center>thead>tr.footable-filtering>th,table.footable>tfoot>tr.footable-paging>td{text-align:center}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:5px}table.footable>thead>tr.footable-filtering>th div.input-group{width:100%}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox{margin:0;display:block;position:relative}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox>label{display:block;padding-left:20px}table.footable>thead>tr.footable-filtering>th ul.dropdown-menu>li>a.checkbox input[type=checkbox]{position:absolute;margin-left:-20px}@media (min-width:768px){table.footable>thead>tr.footable-filtering>th div.input-group{width:auto}table.footable>thead>tr.footable-filtering>th div.form-group{margin-left:2px;margin-right:2px}table.footable>thead>tr.footable-filtering>th div.form-group+div.form-group{margin-top:0}}table.footable>tbody>tr>td.footable-sortable,table.footable>tbody>tr>th.footable-sortable,table.footable>tfoot>tr>td.footable-sortable,table.footable>tfoot>tr>th.footable-sortable,table.footable>thead>tr>td.footable-sortable,table.footable>thead>tr>th.footable-sortable{position:relative;padding-right:30px;cursor:pointer}td.footable-sortable>span.fooicon,th.footable-sortable>span.fooicon{position:absolute;right:6px;top:50%;margin-top:-7px;opacity:0;transition:opacity .3s ease-in}td.footable-sortable.footable-asc>span.fooicon,td.footable-sortable.footable-desc>span.fooicon,td.footable-sortable:hover>span.fooicon,th.footable-sortable.footable-asc>span.fooicon,th.footable-sortable.footable-desc>span.fooicon,th.footable-sortable:hover>span.fooicon{opacity:1}table.footable-sorting-disabled td.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled td.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled td.footable-sortable:hover>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-asc>span.fooicon,table.footable-sorting-disabled th.footable-sortable.footable-desc>span.fooicon,table.footable-sorting-disabled th.footable-sortable:hover>span.fooicon{opacity:0;visibility:hidden}table.footable>tfoot>tr.footable-paging>td>ul.pagination{margin:10px 0 0}table.footable>tfoot>tr.footable-paging>td>span.label{display:inline-block;margin:0 0 10px;padding:4px 10px}table.footable-paging-left>tfoot>tr.footable-paging>td{text-align:left}table.footable-editing-right td.footable-editing,table.footable-editing-right tr.footable-editing,table.footable-paging-right>tfoot>tr.footable-paging>td{text-align:right}ul.pagination>li.footable-page{display:none}ul.pagination>li.footable-page.visible{display:inline}td.footable-editing{width:90px;max-width:90px}table.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit td.footable-editing,table.footable-editing-no-view td.footable-editing{width:70px;max-width:70px}table.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete td.footable-editing,table.footable-editing-no-edit.footable-editing-no-view td.footable-editing{width:50px;max-width:50px}table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view td.footable-editing,table.footable-editing-no-edit.footable-editing-no-delete.footable-editing-no-view th.footable-editing{width:0;max-width:0;display:none!important}table.footable-editing-left td.footable-editing,table.footable-editing-left tr.footable-editing{text-align:left}table.footable-editing button.footable-add,table.footable-editing button.footable-hide,table.footable-editing-show button.footable-show,table.footable-editing.footable-editing-always-show button.footable-hide,table.footable-editing.footable-editing-always-show button.footable-show,table.footable-editing.footable-editing-always-show.footable-editing-no-add tr.footable-editing{display:none}table.footable-editing.footable-editing-always-show button.footable-add,table.footable-editing.footable-editing-show button.footable-add,table.footable-editing.footable-editing-show button.footable-hide{display:inline-block}
+table > tbody > tr > td > span.footable-toggle {
+  opacity: 0.7;
+}
\ No newline at end of file
diff --git a/data/web/css/mailbox.css b/data/web/css/mailbox.css
index 9f07debe..5004cda1 100644
--- a/data/web/css/mailbox.css
+++ b/data/web/css/mailbox.css
@@ -34,4 +34,4 @@ table.footable>tbody>tr.footable-empty>td {
 }
 .inputMissingAttr {
   border-color: #FF4136;
-}
+}
\ No newline at end of file
diff --git a/data/web/css/numberedtextarea.min.css b/data/web/css/numberedtextarea.min.css
new file mode 100644
index 00000000..c147b16b
--- /dev/null
+++ b/data/web/css/numberedtextarea.min.css
@@ -0,0 +1 @@
+div.numberedtextarea-wrapper{position:relative}div.numberedtextarea-wrapper textarea{display:block;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}div.numberedtextarea-line-numbers{position:absolute;top:0;left:0;right:0;bottom:0;width:50px;border-right:none;color:rgba(0,0,0,.4);overflow:hidden}div.numberedtextarea-number{padding-right:6px;text-align:right}textarea#script_data{font-family:Monospace}
\ No newline at end of file
diff --git a/data/web/css/user.css b/data/web/css/user.css
index 07d4e745..e24bebe4 100644
--- a/data/web/css/user.css
+++ b/data/web/css/user.css
@@ -30,3 +30,10 @@ table.footable>tbody>tr.footable-empty>td {
 .inputMissingAttr {
   border-color: #FF4136;
 }
+#logText {
+  white-space: pre-wrap;
+  white-space: -moz-pre-wrap;
+  white-space: -pre-wrap;
+  white-space: -o-pre-wrap;
+  word-wrap: break-word;
+}
\ No newline at end of file
diff --git a/data/web/edit.php b/data/web/edit.php
index fd6682ff..e06a0229 100644
--- a/data/web/edit.php
+++ b/data/web/edit.php
@@ -484,19 +484,25 @@ if (isset($_SESSION['mailcow_cc_role'])) {
         </form>
         <hr>
         <form data-id="mboxratelimit" class="form-inline well" method="post">
-          <div class="form-group">
-            <label class="control-label">Ratelimit</label>
-            <input name="rl_value" id="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
-          </div>
-          <div class="form-group">
-            <select name="rl_frame" id="rl_frame" class="form-control">
-              <option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
-              <option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
-              <option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
-            </select>
-          </div>
-          <div class="form-group">
-            <button class="btn btn-default" id="edit_selected" data-id="mboxratelimit" data-item="<?=$mailbox;?>" data-api-url='edit/ratelimit' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+          <div class="row">
+            <div class="col-sm-1">
+              <p class="help-block">Ratelimit</p>
+            </div>
+            <div class="col-sm-10">
+              <div class="form-group">
+                <input name="rl_value" id="rl_value" type="number" value="<?=(!empty($rl['value'])) ? $rl['value'] : null;?>" class="form-control" placeholder="disabled">
+              </div>
+              <div class="form-group">
+                <select name="rl_frame" id="rl_frame" class="form-control">
+                  <option value="s" <?=(isset($rl['frame']) && $rl['frame'] == 's') ? 'selected' : null;?>>msgs / second</option>
+                  <option value="m" <?=(isset($rl['frame']) && $rl['frame'] == 'm') ? 'selected' : null;?>>msgs / minute</option>
+                  <option value="h" <?=(isset($rl['frame']) && $rl['frame'] == 'h') ? 'selected' : null;?>>msgs / hour</option>
+                </select>
+              </div>
+              <div class="form-group">
+                <button class="btn btn-default" id="edit_selected" data-id="mboxratelimit" data-item="<?=$mailbox;?>" data-api-url='edit/ratelimit' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+              </div>
+            </div>
           </div>
         </form>
       <?php
@@ -717,6 +723,59 @@ if (isset($_SESSION['mailcow_cc_role'])) {
         }
       }
   }
+  if ($_SESSION['mailcow_cc_role'] == "admin"  || $_SESSION['mailcow_cc_role'] == "domainadmin" || $_SESSION['mailcow_cc_role'] == "user") {
+    if (isset($_GET['filter']) &&
+      is_numeric($_GET['filter'])) {
+        $id = $_GET["filter"];
+        $result = mailbox('get', 'filter_details', $id);
+        if (!empty($result)) {
+        ?>
+          <h4>Filter</h4>
+          <form class="form-horizontal" data-id="editfilter" role="form" method="post">
+            <input type="hidden" value="0" name="active">
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="script_desc"><?=$lang['edit']['sieve_desc'];?>:</label>
+              <div class="col-sm-10">
+              <input type="text" class="form-control" name="script_desc" id="script_desc" value="<?=htmlspecialchars($result['script_desc'], ENT_QUOTES, 'UTF-8');?>" required maxlength="255">
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="filter_type"><?=$lang['edit']['sieve_type'];?>:</label>
+              <div class="col-sm-10">
+                <select id="addFilterType" name="filter_type" id="filter_type" required>
+                  <option value="prefilter" <?=($result['filter_type'] == 'prefilter') ? 'selected' : null;?>>Prefilter</option>
+                  <option value="postfilter" <?=($result['filter_type'] == 'postfilter') ? 'selected' : null;?>>Postfilter</option>
+                </select>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="script_data">Script:</label>
+              <div class="col-sm-10">
+                <textarea spellcheck="false" autocorrect="off" autocapitalize="none" class="form-control" rows="20" id="script_data" name="script_data" required><?=$result['script_data'];?></textarea>
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <div class="checkbox">
+                <label><input type="checkbox" value="1" name="active" <?=($result['active_int']=="1") ? "checked" : "";?>> <?=$lang['edit']['active'];?></label>
+                </div>
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <button class="btn btn-success" id="edit_selected" data-id="editfilter" data-item="<?=htmlspecialchars($result['id']);?>" data-api-url='edit/filter' data-api-attr='{}' href="#"><?=$lang['edit']['validate_save'];?></button>
+              </div>
+            </div>
+          </form>
+        <?php
+        }
+        else {
+        ?>
+          <div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+        <?php
+        }
+      }
+  }
 }
 else {
 ?>
diff --git a/data/web/inc/ajax/sieve_validation.php b/data/web/inc/ajax/sieve_validation.php
new file mode 100644
index 00000000..d24a95dc
--- /dev/null
+++ b/data/web/inc/ajax/sieve_validation.php
@@ -0,0 +1,23 @@
+<?php
+session_start();
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+header('Content-Type: application/json');
+if (!isset($_SESSION['mailcow_cc_role'])) {
+  exit();
+}
+if (isset($_GET['script'])) {
+  $sieve = new Sieve\SieveParser();
+  try {
+    if (empty($_GET['script'])) {
+      echo json_encode(array('type' => 'danger', 'msg' => 'Script cannot be empty'));
+      exit();
+    }
+    $sieve->parse($_GET['script']);
+  }
+  catch (Exception $e) {
+    echo json_encode(array('type' => 'danger', 'msg' => $e->getMessage()));
+    exit();
+  }
+  echo json_encode(array('type' => 'success', 'msg' => $lang['add']['validation_success']));
+}
+?>
diff --git a/data/web/inc/ajax/sogo_ctrl.php b/data/web/inc/ajax/sogo_ctrl.php
new file mode 100644
index 00000000..e238d9c0
--- /dev/null
+++ b/data/web/inc/ajax/sogo_ctrl.php
@@ -0,0 +1,39 @@
+<?php
+session_start();
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+header('Content-Type: text/html; charset=utf-8');
+if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
+	exit();
+}
+
+if ($_GET['ACTION'] == "start") {
+  $retry = 0;
+  while (docker('sogo-mailcow', 'info')['State']['Running'] != 1 && $retry <= 3) {
+    $response = docker('sogo-mailcow', 'post', 'start');
+    $response = json_decode($response, true);
+    $last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
+    if ($response['type'] == "success") {
+      break;
+    }
+    usleep(1500000);
+    $retry++;
+  }
+  echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Already running</span></b>' : $last_response;
+}
+
+if ($_GET['ACTION'] == "stop") {
+  $retry = 0;
+  while (docker('sogo-mailcow', 'info')['State']['Running'] == 1 && $retry <= 3) {
+    $response = docker('sogo-mailcow', 'post', 'stop');
+    $response = json_decode($response, true);
+    $last_response = ($response['type'] == "success") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response['msg'] . '</span></b>';
+    if ($response['type'] == "success") {
+      break;
+    }
+    usleep(1500000);
+    $retry++;
+  }
+  echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Not running</span></b>' : $last_response;
+}
+
+?>
diff --git a/data/web/inc/ajax/syncjob_logs.php b/data/web/inc/ajax/syncjob_logs.php
new file mode 100644
index 00000000..a0568167
--- /dev/null
+++ b/data/web/inc/ajax/syncjob_logs.php
@@ -0,0 +1,15 @@
+<?php
+session_start();
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+header('Content-Type: text/plain');
+if (!isset($_SESSION['mailcow_cc_role'])) {
+	exit();
+}
+
+if (isset($_GET['id']) && is_numeric($_GET['id'])) {
+  if ($details = mailbox('get', 'syncjob_details', intval($_GET['id']))) {
+    echo (empty($details['log'])) ? '-' : $details['log'];
+  }
+}
+
+?>
diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php
index 8740612c..9246d230 100644
--- a/data/web/inc/footer.inc.php
+++ b/data/web/inc/footer.inc.php
@@ -8,6 +8,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/footer.php';
 <script src="/js/bootstrap-select.min.js"></script>
 <script src="/js/bootstrap-filestyle.min.js"></script>
 <script src="/js/notifications.min.js"></script>
+<script src="/js/numberedtextarea.min.js"></script>
 <script src="/js/u2f-api.js"></script>
 <script src="/js/api.js"></script>
 <script>
@@ -21,12 +22,17 @@ function setLang(sel) {
 }
 
 $(document).ready(function() {
-  function mailcow_alert_box(message, type) {
+  window.mailcow_alert_box = function(message, type) {
     msg = $('<span/>').html(message).text();
-    $.notify({message: msg},{type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
+    if (type == 'danger') {
+      auto_hide = 0;
+    } else {
+      auto_hide = 5000;
+    }
+    $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
   }
   <?php if (isset($_SESSION['return'])): ?>
-  mailcow_alert_box("<?= $_SESSION['return']['msg']; ?>",  "<?= $_SESSION['return']['type']; ?>");
+  mailcow_alert_box(<?=json_encode($_SESSION['return']['msg']); ?>,  "<?= $_SESSION['return']['type']; ?>");
   <?php endif; unset($_SESSION['return']); ?>
   // Confirm TFA modal
   <?php if (isset($_SESSION['pending_tfa_method'])):?>
@@ -170,7 +176,7 @@ $(document).ready(function() {
     $('#statusTriggerRestartSogo').text('Stopping SOGo workers, this may take a while... ');
     $.ajax({
       method: 'get',
-      url: '/inc/call_sogo_ctrl.php',
+      url: '/inc/ajax/sogo_ctrl.php',
       data: {
         'ajax': true,
         'ACTION': 'stop'
@@ -180,7 +186,7 @@ $(document).ready(function() {
         $('#statusTriggerRestartSogo').append('<br>Starting SOGo...');
         $.ajax({
           method: 'get',
-          url: '/inc/call_sogo_ctrl.php',
+          url: '/inc/ajax/sogo_ctrl.php',
           data: {
             'ajax': true,
             'ACTION': 'start'
diff --git a/data/web/inc/call_sogo_ctrl.php b/data/web/inc/functions.docker.inc.php
similarity index 63%
rename from data/web/inc/call_sogo_ctrl.php
rename to data/web/inc/functions.docker.inc.php
index c54a0a5a..a5f2581c 100644
--- a/data/web/inc/call_sogo_ctrl.php
+++ b/data/web/inc/functions.docker.inc.php
@@ -1,11 +1,4 @@
 <?php
-session_start();
-$AuthUsers = array("admin");
-if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
-	echo "Not allowed." . PHP_EOL;
-	exit();
-}
-
 function docker($service_name, $action, $post_action = null, $post_fields = null) {
   $curl = curl_init();
   curl_setopt($curl, CURLOPT_HTTPHEADER,array( 'Content-Type: application/json' ));
@@ -88,34 +81,4 @@ function docker($service_name, $action, $post_action = null, $post_fields = null
       }
     break;
   }
-}
-
-if ($_GET['ACTION'] == "start") {
-  $retry = 0;
-  while (docker('sogo-mailcow', 'info')['State']['Running'] != 1 && $retry <= 3) {
-    $response = docker('sogo-mailcow', 'post', 'start');
-    $last_response = (trim($response) == "\"OK\"") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response . '</span></b>';
-    if (trim($response) == "\"OK\"") {
-      break;
-    }
-    usleep(1500000);
-    $retry++;
-  }
-  echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Already running</span></b>' : $last_response;
-}
-
-if ($_GET['ACTION'] == "stop") {
-  $retry = 0;
-  while (docker('sogo-mailcow', 'info')['State']['Running'] == 1 && $retry <= 3) {
-    $response = docker('sogo-mailcow', 'post', 'stop');
-    $last_response = (trim($response) == "\"OK\"") ? '<b><span class="pull-right text-success">OK</span></b>' : '<b><span class="pull-right text-danger">Error: ' . $response . '</span></b>';
-    if (trim($response) == "\"OK\"") {
-      break;
-    }
-    usleep(1500000);
-    $retry++;
-  }
-  echo (!isset($last_response)) ? '<b><span class="pull-right text-warning">Not running</span></b>' : $last_response;
-}
-
-?>
+}
\ No newline at end of file
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php
index 79a449eb..1ba9e644 100644
--- a/data/web/inc/functions.inc.php
+++ b/data/web/inc/functions.inc.php
@@ -878,15 +878,24 @@ function get_u2f_registrations($username) {
   $sel->execute(array($username));
   return $sel->fetchAll(PDO::FETCH_OBJ);
 }
-function get_logs($container, $lines = 100) {
+function get_logs($container, $lines = false) {
+  if ($lines === false) {
+    $lines = $GLOBALS['LOG_LINES'] - 1; 
+  }
 	global $lang;
 	global $redis;
 	if ($_SESSION['mailcow_cc_role'] != "admin") {
 		return false;
 	}
-  $lines = intval($lines);
   if ($container == "dovecot-mailcow") {
-    if ($data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines)) {
+    if (!is_numeric($lines)) {
+      list ($from, $to) = explode('-', $lines);
+      $data = $redis->lRange('DOVECOT_MAILLOG', intval($from), intval($to));
+    }
+    else {
+      $data = $redis->lRange('DOVECOT_MAILLOG', 0, intval($lines));
+    }
+    if ($data) {
       foreach ($data as $json_line) {
         $data_array[] = json_decode($json_line, true);
       }
@@ -894,7 +903,14 @@ function get_logs($container, $lines = 100) {
     }
   }
   if ($container == "postfix-mailcow") {
-    if ($data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines)) {
+    if (!is_numeric($lines)) {
+      list ($from, $to) = explode('-', $lines);
+      $data = $redis->lRange('POSTFIX_MAILLOG', intval($from), intval($to));
+    }
+    else {
+      $data = $redis->lRange('POSTFIX_MAILLOG', 0, intval($lines));
+    }
+    if ($data) {
       foreach ($data as $json_line) {
         $data_array[] = json_decode($json_line, true);
       }
@@ -902,7 +918,14 @@ function get_logs($container, $lines = 100) {
     }
   }
   if ($container == "sogo-mailcow") {
-    if ($data = $redis->lRange('SOGO_LOG', 0, $lines)) {
+    if (!is_numeric($lines)) {
+      list ($from, $to) = explode('-', $lines);
+      $data = $redis->lRange('SOGO_LOG', intval($from), intval($to));
+    }
+    else {
+      $data = $redis->lRange('SOGO_LOG', 0, intval($lines));
+    }
+    if ($data) {
       foreach ($data as $json_line) {
         $data_array[] = json_decode($json_line, true);
       }
@@ -910,7 +933,14 @@ function get_logs($container, $lines = 100) {
     }
   }
   if ($container == "fail2ban-mailcow") {
-    if ($data = $redis->lRange('F2B_LOG', 0, $lines)) {
+    if (!is_numeric($lines)) {
+      list ($from, $to) = explode('-', $lines);
+      $data = $redis->lRange('F2B_LOG', intval($from), intval($to));
+    }
+    else {
+      $data = $redis->lRange('F2B_LOG', 0, intval($lines));
+    }
+    if ($data) {
       foreach ($data as $json_line) {
         $data_array[] = json_decode($json_line, true);
       }
@@ -918,7 +948,14 @@ function get_logs($container, $lines = 100) {
     }
   }
   if ($container == "autodiscover-mailcow") {
-    if ($data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines)) {
+    if (!is_numeric($lines)) {
+      list ($from, $to) = explode('-', $lines);
+      $data = $redis->lRange('AUTODISCOVER_LOG', intval($from), intval($to));
+    }
+    else {
+      $data = $redis->lRange('AUTODISCOVER_LOG', 0, intval($lines));
+    }
+    if ($data) {
       foreach ($data as $json_line) {
         $data_array[] = json_decode($json_line, true);
       }
@@ -927,10 +964,16 @@ function get_logs($container, $lines = 100) {
   }
   if ($container == "rspamd-history") {
     $curl = curl_init();
-    curl_setopt($curl, CURLOPT_URL,"http://rspamd-mailcow:11334/history");
+    if (!is_numeric($lines)) {
+      list ($from, $to) = explode('-', $lines);
+      curl_setopt($curl, CURLOPT_URL,"http://rspamd-mailcow:11334/history?from=" . intval($from) . "&to=" . intval($to));
+    }
+    else {
+      curl_setopt($curl, CURLOPT_URL,"http://rspamd-mailcow:11334/history?to=" . intval($lines));
+    }
     curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
     $history = curl_exec($curl);
-    if (!curl_errno($ch)) {
+    if (!curl_errno($curl)) {
       $data_array = json_decode($history, true);
       curl_close($curl);
       return $data_array['rows'];
@@ -940,4 +983,4 @@ function get_logs($container, $lines = 100) {
   }
   return false;
 }
-?>
\ No newline at end of file
+?>
diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php
index 209b4acc..ed3caaa7 100644
--- a/data/web/inc/functions.mailbox.inc.php
+++ b/data/web/inc/functions.mailbox.inc.php
@@ -1,5 +1,5 @@
 <?php
-function mailbox($_action, $_type, $_data = null) {
+function mailbox($_action, $_type, $_data = null, $attr = null) {
 	global $pdo;
 	global $redis;
 	global $lang;
@@ -72,6 +72,116 @@ function mailbox($_action, $_type, $_data = null) {
             'msg' => sprintf($lang['success']['mailbox_modified'], htmlspecialchars($usernames))
           );
         break;
+        case 'filter':
+          $sieve = new Sieve\SieveParser();
+          if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => sprintf($lang['danger']['access_denied'])
+            );
+            return false;
+          }
+          if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) {
+            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => sprintf($lang['danger']['access_denied'])
+              );
+              return false;
+            }
+            else {
+              $username = $_data['username'];
+            }
+          }
+          elseif ($_SESSION['mailcow_cc_role'] == "user") {
+            $username = $_SESSION['mailcow_cc_username'];
+          }
+          else {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'No user defined'
+            );
+            return false;
+          }
+          $active     = intval($_data['active']);
+          $script_data = $_data['script_data'];
+          $script_desc = $_data['script_desc'];
+          $filter_type = $_data['filter_type'];
+          if (empty($script_data)) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'Script cannot be empty'
+            );
+            return false;
+          }
+          try {
+            $sieve->parse($script_data);
+          }
+          catch (Exception $e) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'Sieve parser error: ' . $e->getMessage()
+            );
+            return false;
+          }
+          if (empty($script_data) || empty($script_desc)) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'Please define values for all fields'
+            );
+            return false;
+          }
+          if ($filter_type != 'postfilter' && $filter_type != 'prefilter') {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'Wrong filter type'
+            );
+            return false;
+          }
+          if (!empty($active)) {
+            $script_name = 'active';
+            try {
+              $stmt = $pdo->prepare("UPDATE `sieve_filters` SET `script_name` = 'inactive' WHERE `username` = :username AND `filter_type` = :filter_type");
+              $stmt->execute(array(
+                ':username' => $username,
+                ':filter_type' => $filter_type
+              ));
+            }
+            catch(PDOException $e) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => 'MySQL: '.$e
+              );
+              return false;
+            }
+          }
+          else {
+            $script_name = 'inactive';
+          }
+          try {
+            $stmt = $pdo->prepare("INSERT INTO `sieve_filters` (`username`, `script_data`, `script_desc`, `script_name`, `filter_type`)
+              VALUES (:username, :script_data, :script_desc, :script_name, :filter_type)");
+            $stmt->execute(array(
+              ':username' => $username,
+              ':script_data' => $script_data,
+              ':script_desc' => $script_desc,
+              ':script_name' => $script_name,
+              ':filter_type' => $filter_type
+            ));
+          }
+          catch(PDOException $e) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'MySQL: '.$e
+            );
+            return false;
+          }
+          $_SESSION['return'] = array(
+            'type' => 'success',
+            'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+          );
+          return true;
+        break;
         case 'syncjob':
           if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) {
             $_SESSION['return'] = array(
@@ -1379,6 +1489,100 @@ function mailbox($_action, $_type, $_data = null) {
           );
           return true;
         break;
+        case 'filter':
+          $sieve = new Sieve\SieveParser();
+          if (!is_array($_data['id'])) {
+            $ids = array();
+            $ids[] = $_data['id'];
+          }
+          else {
+            $ids = $_data['id'];
+          }
+          if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => sprintf($lang['danger']['access_denied'])
+            );
+            return false;
+          }
+          foreach ($ids as $id) {
+            $is_now = mailbox('get', 'filter_details', $id);
+            if (!empty($is_now)) {
+              $username = $is_now['username'];
+              $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int'];
+              $script_desc = (!empty($_data['script_desc'])) ? $_data['script_desc'] : $is_now['script_desc'];
+              $script_data = (!empty($_data['script_data'])) ? $_data['script_data'] : $is_now['script_data'];
+              $filter_type = (!empty($_data['filter_type'])) ? $_data['filter_type'] : $is_now['filter_type'];
+            }
+            else {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => sprintf($lang['danger']['access_denied'])
+              );
+              return false;
+            }
+            try {
+              $sieve->parse($script_data);
+            }
+            catch (Exception $e) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => 'Sieve parser error: ' . $e->getMessage()
+              );
+              return false;
+            }
+            if ($filter_type != 'postfilter' && $filter_type != 'prefilter') {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => 'Wrong filter type'
+              );
+              return false;
+            }
+            if ($active == '1') {
+              $script_name = 'active';
+              try {
+                $stmt = $pdo->prepare("UPDATE `sieve_filters` SET `script_name` = 'inactive' WHERE `username` = :username AND `filter_type` = :filter_type");
+                $stmt->execute(array(
+                  ':username' => $username,
+                  ':filter_type' => $filter_type
+                ));
+              }
+              catch(PDOException $e) {
+                $_SESSION['return'] = array(
+                  'type' => 'danger',
+                  'msg' => 'MySQL: '.$e
+                );
+                return false;
+              }
+            }
+            else {
+              $script_name = 'inactive';
+            }
+            try {
+              $stmt = $pdo->prepare("UPDATE `sieve_filters` SET `script_desc` = :script_desc, `script_data` = :script_data, `script_name` = :script_name, `filter_type` = :filter_type
+                WHERE `id` = :id");
+              $stmt->execute(array(
+                ':script_desc' => $script_desc,
+                ':script_data' => $script_data,
+                ':script_name' => $script_name,
+                ':filter_type' => $filter_type,
+                ':id' => $id
+              ));
+            }
+            catch(PDOException $e) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => 'MySQL: '.$e
+              );
+              return false;
+            }
+          }
+          $_SESSION['return'] = array(
+            'type' => 'success',
+            'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+          );
+          return true;
+        break;
         case 'alias':
           if (!is_array($_data['address'])) {
             $addresses = array();
@@ -2140,19 +2344,128 @@ function mailbox($_action, $_type, $_data = null) {
           }
           return $policydata;
         break;
+        case 'filters':
+          $filters = array();
+          if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
+            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+              return false;
+            }
+          }
+          else {
+            $_data = $_SESSION['mailcow_cc_username'];
+          }
+          try {
+            $stmt = $pdo->prepare("SELECT `id` FROM `sieve_filters` WHERE `username` = :username");
+            $stmt->execute(array(':username' => $_data));
+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+            while($row = array_shift($rows)) {
+              $filters[] = $row['id'];
+            }
+          }
+          catch(PDOException $e) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'MySQL: '.$e
+            );
+          }
+          return $filters;
+        break;
+        case 'filter_details':
+          $filter_details = array();
+          if (!is_numeric($_data)) {
+            return false;
+          }
+          try {
+            $stmt = $pdo->prepare("SELECT CASE `script_name` WHEN 'active' THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
+              CASE `script_name` WHEN 'active' THEN 1 ELSE 0 END AS `active_int`,
+              id,
+              username,
+              filter_type,
+              script_data,
+              script_desc
+              FROM `sieve_filters`
+                WHERE `id` = :id");
+            $stmt->execute(array(':id' => $_data));
+            $filter_details = $stmt->fetch(PDO::FETCH_ASSOC);
+          }
+          catch(PDOException $e) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => 'MySQL: '.$e
+            );
+            return false;
+          }
+          if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $filter_details['username'])) {
+            return false;
+          }
+          return $filter_details;
+        break;
+        case 'active_user_sieve':
+          $filter_details = array();
+          if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
+            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
+              return false;
+            }
+          }
+          else {
+            $_data = $_SESSION['mailcow_cc_username'];
+          }
+          $exec_fields = array(
+            'cmd' => 'sieve_list',
+            'username' => $_data
+          );
+          $filters = json_decode(docker('dovecot-mailcow', 'post', 'exec', $exec_fields), true);
+          $filters = array_filter(explode(PHP_EOL, $filters));
+          foreach ($filters as $filter) {
+            if (preg_match('/.+ ACTIVE/i', $filter)) {
+              $exec_fields = array(
+                'cmd' => 'sieve_print',
+                'script_name' => substr($filter, 0, -7),
+                'username' => $_data
+              );
+              $filters = json_decode(docker('dovecot-mailcow', 'post', 'exec', $exec_fields), true);
+              return preg_replace('/^.+\n/', '', $filters);
+            }
+          }
+          return false;
+        break;
         case 'syncjob_details':
           $syncjobdetails = array();
           if (!is_numeric($_data)) {
             return false;
           }
           try {
-            $stmt = $pdo->prepare("SELECT *,
-              CONCAT(LEFT(`password1`, 3), '...') AS `password1_short`,
-              `active` AS `active_int`,
-              CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
-                FROM `imapsync` WHERE id = :id");
+            if (isset($attr) && in_array('no_log', $attr)) {
+              $field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("returned_text", "password1")');
+              $fields = $field_query->fetchAll(PDO::FETCH_ASSOC);
+              while($field = array_shift($fields)) {
+                $shown_fields[] = $field['Field'];
+              }
+              $stmt = $pdo->prepare("SELECT " . implode(',', $shown_fields) . ",
+                `active` AS `active_int`,
+                CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
+                  FROM `imapsync` WHERE id = :id");
+            }
+            else {
+              $field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("password1")');
+              $fields = $field_query->fetchAll(PDO::FETCH_ASSOC);
+              while($field = array_shift($fields)) {
+                $shown_fields[] = $field['Field'];
+              }
+              $stmt = $pdo->prepare("SELECT " . implode(',', $shown_fields) . ",
+                `active` AS `active_int`,
+                CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
+                  FROM `imapsync` WHERE id = :id");
+            }
             $stmt->execute(array(':id' => $_data));
             $syncjobdetails = $stmt->fetch(PDO::FETCH_ASSOC);
+            if (!empty($syncjobdetails['returned_text'])) {
+              $syncjobdetails['log'] = $syncjobdetails['returned_text'];
+            }
+            else {
+              $syncjobdetails['log'] = '';
+            }
+            unset($syncjobdetails['returned_text']);
           }
           catch(PDOException $e) {
             $_SESSION['return'] = array(
@@ -2816,6 +3129,57 @@ function mailbox($_action, $_type, $_data = null) {
           );
           return true;
         break;
+        case 'filter':
+          if (!is_array($_data['id'])) {
+            $ids = array();
+            $ids[] = $_data['id'];
+          }
+          else {
+            $ids = $_data['id'];
+          }
+          if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
+            $_SESSION['return'] = array(
+              'type' => 'danger',
+              'msg' => sprintf($lang['danger']['access_denied'])
+            );
+            return false;
+          }
+          foreach ($ids as $id) {
+            if (!is_numeric($id)) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => $id
+              );
+              return false;
+            }
+            try {
+              $stmt = $pdo->prepare("SELECT `username` FROM `sieve_filters` WHERE id = :id");
+              $stmt->execute(array(':id' => $id));
+              $usr = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
+              if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $usr)) {
+                $_SESSION['return'] = array(
+                  'type' => 'danger',
+                  'msg' => sprintf($lang['danger']['access_denied'])
+                );
+                return false;
+              }
+              $stmt = $pdo->prepare("DELETE FROM `sieve_filters` WHERE `id`= :id");
+              $stmt->execute(array(':id' => $id));
+            }
+            catch (PDOException $e) {
+              $_SESSION['return'] = array(
+                'type' => 'danger',
+                'msg' => 'MySQL: '.$e
+              );
+              return false;
+            }
+          }
+          $_SESSION['return'] = array(
+            'type' => 'success',
+            'msg' => 'Deleted filter id/s ' . implode(', ', $ids)
+          );
+          return true;
+        break;
         case 'time_limited_alias':
           if (!is_array($_data['address'])) {
             $addresses = array();
diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php
index b64bd5a8..cedd07ca 100644
--- a/data/web/inc/header.inc.php
+++ b/data/web/inc/header.inc.php
@@ -22,6 +22,7 @@
 <link rel="stylesheet" href="/inc/languages.min.css">
 <link rel="stylesheet" href="/css/mailcow.css">
 <link rel="stylesheet" href="/css/animate.min.css">
+<link rel="stylesheet" href="/css/numberedtextarea.min.css">
 <?= (preg_match("/mailbox.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/mailbox.css">' : null; ?>
 <?= (preg_match("/admin.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/admin.css">' : null; ?>
 <?= (preg_match("/user.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/user.css">' : null; ?>
diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php
index 16f3fdf2..9b6a63c9 100644
--- a/data/web/inc/init_db.inc.php
+++ b/data/web/inc/init_db.inc.php
@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
     global $pdo;
 
-    $db_version = "25102017_0748";
+    $db_version = "31102017_1049";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); 
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -27,7 +27,13 @@ function init_db_schema() {
       GROUP BY logged_in_as;",
     "grouped_domain_alias_address" => "CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS
       SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox
-      LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;"
+      LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;",
+    "sieve_before" => "CREATE VIEW sieve_before (id, username, script_name, script_data) AS
+      SELECT id, username, script_name, script_data FROM sieve_filters
+      WHERE filter_type = 'prefilter';",
+    "sieve_after" => "CREATE VIEW sieve_after (id, username, script_name, script_data) AS
+      SELECT id, username, script_name, script_data FROM sieve_filters
+      WHERE filter_type = 'postfilter';"
     );
 
     $tables = array(
@@ -155,6 +161,36 @@ function init_db_schema() {
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
       ),
+      "sieve_filters" => array(
+        "cols" => array(
+          "id" => "INT NOT NULL AUTO_INCREMENT",
+          "username" => "VARCHAR(255) NOT NULL",
+          "script_desc" => "VARCHAR(255) NOT NULL",
+          "script_name" => "ENUM('active','inactive')",
+          "script_data" => "TEXT NOT NULL",
+          "filter_type" => "ENUM('postfilter','prefilter')",
+          "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
+          "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("id")
+          ),
+          "key" => array(
+            "username" => array("username"),
+            "script_desc" => array("script_desc")
+          ),
+          "fkey" => array(
+            "fk_username_sieve_global_before" => array(
+              "col" => "username",
+              "ref" => "mailbox.username",
+              "delete" => "CASCADE",
+              "update" => "NO ACTION"
+            )
+          )
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
       "user_acl" => array(
         "cols" => array(
           "username" => "VARCHAR(255) NOT NULL",
@@ -165,6 +201,7 @@ function init_db_schema() {
           "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "filters" => "TINYINT(1) NOT NULL DEFAULT '1'",
         ),
         "keys" => array(
           "fkey" => array(
@@ -248,8 +285,11 @@ function init_db_schema() {
           "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
         ),
         "keys" => array(
+          "primary" => array(
+            "" => array("username")
+          ),
           "key" => array(
-            "username" => array("username")
+            "domain" => array("domain")
           )
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
diff --git a/data/web/inc/lib/sieve/SieveDumpable.php b/data/web/inc/lib/sieve/SieveDumpable.php
new file mode 100644
index 00000000..af82d6af
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveDumpable.php
@@ -0,0 +1,7 @@
+<?php namespace Sieve;
+
+interface SieveDumpable
+{
+	function dump();
+	function text();
+}
diff --git a/data/web/inc/lib/sieve/SieveException.php b/data/web/inc/lib/sieve/SieveException.php
new file mode 100644
index 00000000..da7fe604
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveException.php
@@ -0,0 +1,47 @@
+<?php namespace Sieve;
+
+require_once('SieveToken.php');
+
+use Exception;
+
+class SieveException extends Exception
+{
+    protected $token_;
+
+    public function __construct(SieveToken $token, $arg)
+    {
+        $message = 'undefined sieve exception';
+        $this->token_ = $token;
+
+        if (is_string($arg))
+        {
+            $message = $arg;
+        }
+        else
+        {
+            if (is_array($arg))
+            {
+                $type = SieveToken::typeString(array_shift($arg));
+                foreach($arg as $t)
+                {
+                    $type .= ' or '. SieveToken::typeString($t);
+                }
+            }
+            else
+            {
+                $type = SieveToken::typeString($arg);
+            }
+
+            $tokenType = SieveToken::typeString($token->type);
+            $message = "$tokenType where $type expected near ". $token->text;
+        }
+
+        parent::__construct('line '. $token->line .": $message");
+    }
+
+    public function getLineNo()
+    {
+        return $this->token_->line;
+    }
+
+}
diff --git a/data/web/inc/lib/sieve/SieveKeywordRegistry.php b/data/web/inc/lib/sieve/SieveKeywordRegistry.php
new file mode 100644
index 00000000..b901dbc3
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveKeywordRegistry.php
@@ -0,0 +1,233 @@
+<?php namespace Sieve;
+
+class SieveKeywordRegistry
+{
+    protected $registry_ = array();
+    protected $matchTypes_ = array();
+    protected $comparators_ = array();
+    protected $addressParts_ = array();
+    protected $commands_ = array();
+    protected $tests_ = array();
+    protected $arguments_ = array();
+
+    protected static $refcount = 0;
+    protected static $instance = null;
+
+    protected function __construct()
+    {
+        $keywords = simplexml_load_file(dirname(__FILE__) .'/keywords.xml');
+        foreach ($keywords->children() as $keyword)
+        {
+            switch ($keyword->getName())
+            {
+            case 'matchtype':
+                $type =& $this->matchTypes_;
+                break;
+            case 'comparator':
+                $type =& $this->comparators_;
+                break;
+            case 'addresspart':
+                $type =& $this->addressParts_;
+                break;
+            case 'test':
+                $type =& $this->tests_;
+                break;
+            case 'command':
+                $type =& $this->commands_;
+                break;
+            default:
+                trigger_error('Unsupported keyword type "'. $keyword->getName()
+                    . '" in file "keywords/'. basename($file) .'"');
+                return;
+            }
+
+            $name = (string) $keyword['name'];
+            if (array_key_exists($name, $type))
+                trigger_error("redefinition of $type $name - skipping");
+            else
+                $type[$name] = $keyword->children();
+        }
+
+        foreach (glob(dirname(__FILE__) .'/extensions/*.xml') as $file)
+        {
+            $extension = simplexml_load_file($file);
+            $name = (string) $extension['name'];
+
+            if (array_key_exists($name, $this->registry_))
+            {
+                trigger_error('overwriting extension "'. $name .'"');
+            }
+            $this->registry_[$name] = $extension;
+        }
+    }
+
+    public static function get()
+    {
+        if (self::$instance == null)
+        {
+            self::$instance = new SieveKeywordRegistry();
+        }
+
+        self::$refcount++;
+
+        return self::$instance;
+    }
+
+    public function put()
+    {
+        if (--self::$refcount == 0)
+        {
+            self::$instance = null;
+        }
+    }
+
+    public function activate($extension)
+    {
+        if (!isset($this->registry_[$extension]))
+        {
+            return;
+        }
+
+        $xml = $this->registry_[$extension];
+
+        foreach ($xml->children() as $e)
+        {
+            switch ($e->getName())
+            {
+            case 'matchtype':
+                $type =& $this->matchTypes_;
+                break;
+            case 'comparator':
+                $type =& $this->comparators_;
+                break;
+            case 'addresspart':
+                $type =& $this->addressParts_;
+                break;
+            case 'test':
+                $type =& $this->tests_;
+                break;
+            case 'command':
+                $type =& $this->commands_;
+                break;
+            case 'tagged-argument':
+                $xml = $e->parameter[0];
+                $this->arguments_[(string) $xml['name']] = array(
+                    'extends' => (string) $e['extends'],
+                    'rules'   => $xml
+                );
+                continue;
+            default:
+                trigger_error('Unsupported extension type \''.
+                    $e->getName() ."' in extension '$extension'");
+                return;
+            }
+
+            $name = (string) $e['name'];
+            if (!isset($type[$name]) ||
+                (string) $e['overrides'] == 'true')
+            {
+                $type[$name] = $e->children();
+            }
+        }
+    }
+
+    public function isTest($name)
+    {
+        return (isset($this->tests_[$name]) ? true : false);
+    }
+
+    public function isCommand($name)
+    {
+        return (isset($this->commands_[$name]) ? true : false);
+    }
+
+    public function matchtype($name)
+    {
+        if (isset($this->matchTypes_[$name]))
+        {
+            return $this->matchTypes_[$name];
+        }
+        return null;
+    }
+
+    public function addresspart($name)
+    {
+        if (isset($this->addressParts_[$name]))
+        {
+            return $this->addressParts_[$name];
+        }
+        return null;
+    }
+
+    public function comparator($name)
+    {
+        if (isset($this->comparators_[$name]))
+        {
+            return $this->comparators_[$name];
+        }
+        return null;
+    }
+
+    public function test($name)
+    {
+        if (isset($this->tests_[$name]))
+        {
+            return $this->tests_[$name];
+        }
+        return null;
+    }
+
+    public function command($name)
+    {
+        if (isset($this->commands_[$name]))
+        {
+            return $this->commands_[$name];
+        }
+        return null;
+    }
+
+    public function arguments($command)
+    {
+        $res = array();
+        foreach ($this->arguments_ as $arg)
+        {
+            if (preg_match('/'.$arg['extends'].'/', $command))
+                array_push($res, $arg['rules']);
+        }
+        return $res;
+    }
+
+    public function argument($name)
+    {
+        if (isset($this->arguments_[$name]))
+        {
+            return $this->arguments_[$name]['rules'];
+        }
+        return null;
+    }
+
+    public function requireStrings()
+    {
+        return array_keys($this->registry_);
+    }
+    public function matchTypes()
+    {
+        return array_keys($this->matchTypes_);
+    }
+    public function comparators()
+    {
+        return array_keys($this->comparators_);
+    }
+    public function addressParts()
+    {
+        return array_keys($this->addressParts_);
+    }
+    public function tests()
+    {
+        return array_keys($this->tests_);
+    }
+    public function commands()
+    {
+        return array_keys($this->commands_);
+    }
+}
diff --git a/data/web/inc/lib/sieve/SieveParser.php b/data/web/inc/lib/sieve/SieveParser.php
new file mode 100644
index 00000000..3d81c14b
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveParser.php
@@ -0,0 +1,255 @@
+<?php namespace Sieve;
+
+include_once 'SieveTree.php';
+include_once 'SieveScanner.php';
+include_once 'SieveSemantics.php';
+include_once 'SieveException.php';
+
+class SieveParser
+{
+    protected $scanner_;
+    protected $script_;
+    protected $tree_;
+    protected $status_;
+
+    public function __construct($script = null)
+    {
+        if (isset($script))
+            $this->parse($script);
+    }
+
+    public function GetParseTree()
+    {
+        return $this->tree_;
+    }
+
+    public function dumpParseTree()
+    {
+        return $this->tree_->dump();
+    }
+
+    public function getScriptText()
+    {
+        return $this->tree_->getText();
+    }
+
+    protected function getPrevToken_($parent_id)
+    {
+        $childs = $this->tree_->getChilds($parent_id);
+
+        for ($i = count($childs); $i > 0; --$i)
+        {
+            $prev = $this->tree_->getNode($childs[$i-1]);
+            if ($prev->is(SieveToken::Comment|SieveToken::Whitespace))
+                continue;
+
+            // use command owning a block or list instead of previous
+            if ($prev->is(SieveToken::BlockStart|SieveToken::Comma|SieveToken::LeftParenthesis))
+                $prev = $this->tree_->getNode($parent_id);
+
+            return $prev;
+        }
+
+        return $this->tree_->getNode($parent_id);
+    }
+
+    /*******************************************************************************
+     * methods for recursive descent start below
+     */
+    public function passthroughWhitespaceComment($token)
+    {
+        return 0;
+    }
+
+    public function passthroughFunction($token)
+    {
+        $this->tree_->addChild($token);
+    }
+
+    public function parse($script)
+    {
+        $this->script_ = $script;
+
+        $this->scanner_ = new SieveScanner($this->script_);
+
+        // Define what happens with passthrough tokens like whitespacs and comments
+        $this->scanner_->setPassthroughFunc(
+            array(
+                $this, 'passthroughWhitespaceComment'
+            )
+        );
+
+        $this->tree_ = new SieveTree('tree');
+
+        $this->commands_($this->tree_->getRoot());
+
+        if (!$this->scanner_->nextTokenIs(SieveToken::ScriptEnd)) {
+            $token = $this->scanner_->nextToken();
+            throw new SieveException($token, SieveToken::ScriptEnd);
+        }
+    }
+
+    protected function commands_($parent_id)
+    {
+        while (true)
+        {
+            if (!$this->scanner_->nextTokenIs(SieveToken::Identifier))
+                break;
+
+            // Get and check a command token
+            $token = $this->scanner_->nextToken();
+            $semantics = new SieveSemantics($token, $this->getPrevToken_($parent_id));
+
+            // Process eventual arguments
+            $this_node = $this->tree_->addChildTo($parent_id, $token);
+            $this->arguments_($this_node, $semantics);
+
+            $token = $this->scanner_->nextToken();
+            if (!$token->is(SieveToken::Semicolon))
+            {
+                // TODO: check if/when semcheck is needed here
+                $semantics->validateToken($token);
+
+                if ($token->is(SieveToken::BlockStart))
+                {
+                    $this->tree_->addChildTo($this_node, $token);
+                    $this->block_($this_node, $semantics);
+                    continue;
+                }
+
+                throw new SieveException($token, SieveToken::Semicolon);
+            }
+
+            $semantics->done($token);
+            $this->tree_->addChildTo($this_node, $token);
+        }
+    }
+
+    protected function arguments_($parent_id, &$semantics)
+    {
+        while (true)
+        {
+            if ($this->scanner_->nextTokenIs(SieveToken::Number|SieveToken::Tag))
+            {
+                // Check if semantics allow a number or tag
+                $token = $this->scanner_->nextToken();
+                $semantics->validateToken($token);
+                $this->tree_->addChildTo($parent_id, $token);
+            }
+            else if ($this->scanner_->nextTokenIs(SieveToken::StringList))
+            {
+                $this->stringlist_($parent_id, $semantics);
+            }
+            else
+            {
+                break;
+            }
+        }
+
+        if ($this->scanner_->nextTokenIs(SieveToken::TestList))
+        {
+            $this->testlist_($parent_id, $semantics);
+        }
+    }
+
+    protected function stringlist_($parent_id, &$semantics)
+    {
+        if (!$this->scanner_->nextTokenIs(SieveToken::LeftBracket))
+        {
+            $this->string_($parent_id, $semantics);
+            return;
+        }
+
+        $token = $this->scanner_->nextToken();
+        $semantics->startStringList($token);
+        $this->tree_->addChildTo($parent_id, $token);
+        
+        if($this->scanner_->nextTokenIs(SieveToken::RightBracket)) {
+            //allow empty lists
+            $token = $this->scanner_->nextToken();
+            $this->tree_->addChildTo($parent_id, $token);
+            $semantics->endStringList();
+            return;
+        }
+
+        do
+        {
+            $this->string_($parent_id, $semantics);
+            $token = $this->scanner_->nextToken();
+
+            if (!$token->is(SieveToken::Comma|SieveToken::RightBracket))
+                throw new SieveException($token, array(SieveToken::Comma, SieveToken::RightBracket));
+
+            if ($token->is(SieveToken::Comma))
+                $semantics->continueStringList();
+
+            $this->tree_->addChildTo($parent_id, $token);
+        }
+        while (!$token->is(SieveToken::RightBracket));
+
+        $semantics->endStringList();
+    }
+
+    protected function string_($parent_id, &$semantics)
+    {
+        $token = $this->scanner_->nextToken();
+        $semantics->validateToken($token);
+        $this->tree_->addChildTo($parent_id, $token);
+    }
+
+    protected function testlist_($parent_id, &$semantics)
+    {
+        if (!$this->scanner_->nextTokenIs(SieveToken::LeftParenthesis))
+        {
+            $this->test_($parent_id, $semantics);
+            return;
+        }
+
+        $token = $this->scanner_->nextToken();
+        $semantics->validateToken($token);
+        $this->tree_->addChildTo($parent_id, $token);
+
+        do
+        {
+            $this->test_($parent_id, $semantics);
+
+            $token = $this->scanner_->nextToken();
+            if (!$token->is(SieveToken::Comma|SieveToken::RightParenthesis))
+            {
+                throw new SieveException($token, array(SieveToken::Comma, SieveToken::RightParenthesis));
+            }
+            $this->tree_->addChildTo($parent_id, $token);
+        }
+        while (!$token->is(SieveToken::RightParenthesis));
+    }
+
+    protected function test_($parent_id, &$semantics)
+    {
+        // Check if semantics allow an identifier
+        $token = $this->scanner_->nextToken();
+        $semantics->validateToken($token);
+
+        // Get semantics for this test command
+        $this_semantics = new SieveSemantics($token, $this->getPrevToken_($parent_id));
+        $this_node = $this->tree_->addChildTo($parent_id, $token);
+
+        // Consume eventual argument tokens
+        $this->arguments_($this_node, $this_semantics);
+
+        // Check that all required arguments were there
+        $token = $this->scanner_->peekNextToken();
+        $this_semantics->done($token);
+    }
+
+    protected function block_($parent_id, &$semantics)
+    {
+        $this->commands_($parent_id, $semantics);
+
+        $token = $this->scanner_->nextToken();
+        if (!$token->is(SieveToken::BlockEnd))
+        {
+            throw new SieveException($token, SieveToken::BlockEnd);
+        }
+        $this->tree_->addChildTo($parent_id, $token);
+    }
+}
diff --git a/data/web/inc/lib/sieve/SieveScanner.php b/data/web/inc/lib/sieve/SieveScanner.php
new file mode 100644
index 00000000..a0fa57a2
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveScanner.php
@@ -0,0 +1,145 @@
+<?php namespace Sieve;
+
+include_once('SieveToken.php');
+
+class SieveScanner
+{
+    public function __construct(&$script)
+    {
+        if ($script === null)
+            return;
+
+        $this->tokenize($script);
+    }
+
+    public function setPassthroughFunc($callback)
+    {
+        if ($callback == null || is_callable($callback))
+            $this->ptFn_ = $callback;
+    }
+
+    public function tokenize(&$script)
+    {
+        $pos = 0;
+        $line = 1;
+
+        $scriptLength = mb_strlen($script);
+
+        $unprocessedScript = $script;
+
+
+        //create one regex to find the right match
+        //avoids looping over all possible tokens: increases performance
+        $nameToType = [];
+        $regex = [];
+        // chr(65) == 'A'
+        $i = 65;
+
+        foreach ($this->tokenMatch_ as $type => $subregex) {
+            $nameToType[chr($i)] = $type;
+            $regex[] = "(?P<". chr($i) . ">^$subregex)";
+            $i++;
+        }
+
+        $regex = '/' . join('|', $regex) . '/';
+
+        while ($pos < $scriptLength)
+        {
+            if (preg_match($regex, $unprocessedScript, $match)) {
+
+                // only keep the group that match and we only want matches with group names
+                // we can use the group name to find the token type using nameToType
+                $filterMatch = array_filter(array_filter($match), 'is_string', ARRAY_FILTER_USE_KEY);
+
+                // the first element in filterMatch will contain the matched group and the key will be the name
+                $type = $nameToType[key($filterMatch)];
+                $currentMatch = current($filterMatch);
+
+                //create the token
+                $token = new SieveToken($type, $currentMatch, $line);
+                $this->tokens_[] = $token;
+
+                if ($type == SieveToken::Unknown)
+                    return;
+
+                // just remove the part that we parsed: don't extract the new substring using script length
+                // as mb_strlen is \theta(pos)  (it's linear in the position)
+                $matchLength = mb_strlen($currentMatch);
+                $unprocessedScript = mb_substr($unprocessedScript, $matchLength);
+
+                $pos += $matchLength;
+                $line += mb_substr_count($currentMatch, "\n");
+            } else {
+                $this->tokens_[] = new SieveToken(SieveToken::Unknown, '', $line);
+                return;
+            }
+
+        }
+
+        $this->tokens_[] = new SieveToken(SieveToken::ScriptEnd, '', $line);
+    }
+
+    public function nextTokenIs($type)
+    {
+        return $this->peekNextToken()->is($type);
+    }
+
+    public function peekNextToken()
+    {
+        $offset = 0;
+        do {
+            $next = $this->tokens_[$this->tokenPos_ + $offset++];
+        } while ($next->is(SieveToken::Comment|SieveToken::Whitespace));
+
+        return $next;
+    }
+
+    public function nextToken()
+    {
+        $token = $this->tokens_[$this->tokenPos_++];
+
+        while ($token->is(SieveToken::Comment|SieveToken::Whitespace))
+        {
+            if ($this->ptFn_ != null)
+                call_user_func($this->ptFn_, $token);
+
+            $token = $this->tokens_[$this->tokenPos_++];
+        }
+
+        return $token;
+    }
+
+    protected $ptFn_ = null;
+    protected $tokenPos_ = 0;
+    protected $tokens_ = array();
+    protected $tokenMatch_ = array (
+        SieveToken::LeftBracket       =>  '\[',
+        SieveToken::RightBracket      =>  '\]',
+        SieveToken::BlockStart        =>  '\{',
+        SieveToken::BlockEnd          =>  '\}',
+        SieveToken::LeftParenthesis   =>  '\(',
+        SieveToken::RightParenthesis  =>  '\)',
+        SieveToken::Comma             =>  ',',
+        SieveToken::Semicolon         =>  ';',
+        SieveToken::Whitespace        =>  '[ \r\n\t]+',
+        SieveToken::Tag               =>  ':[[:alpha:]_][[:alnum:]_]*(?=\b)',
+        /*
+        "                           # match a quotation mark
+        (                           # start matching parts that include an escaped quotation mark
+        ([^"]*[^"\\\\])             # match a string without quotation marks and not ending with a backlash
+        ?                           # this also includes the empty string
+        (\\\\\\\\)*                 # match any groups of even number of backslashes
+                                    # (thus the character after these groups are not escaped)
+        \\\\"                       # match an escaped quotation mark
+        )*                          # accept any number of strings that end with an escaped quotation mark
+        [^"]*                       # accept any trailing part that does not contain any quotation marks
+        "                           # end of the quoted string
+        */
+        SieveToken::QuotedString      =>  '"(([^"]*[^"\\\\])?(\\\\\\\\)*\\\\")*[^"]*"',
+        SieveToken::Number            =>  '[[:digit:]]+(?:[KMG])?(?=\b)',
+        SieveToken::Comment           =>  '(?:\/\*(?:[^\*]|\*(?=[^\/]))*\*\/|#[^\r\n]*\r?(\n|$))',
+        SieveToken::MultilineString   =>  'text:[ \t]*(?:#[^\r\n]*)?\r?\n(\.[^\r\n]+\r?\n|[^\.][^\r\n]*\r?\n)*\.\r?(\n|$)',
+        SieveToken::Identifier        =>  '[[:alpha:]_][[:alnum:]_]*(?=\b)',
+        SieveToken::Unknown           =>  '[^ \r\n\t]+'
+    );
+}
diff --git a/data/web/inc/lib/sieve/SieveScript.php b/data/web/inc/lib/sieve/SieveScript.php
new file mode 100644
index 00000000..befd99a1
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveScript.php
@@ -0,0 +1,6 @@
+<?php namespace Sieve;
+
+class SieveScript
+{
+    // TODO: implement
+}
diff --git a/data/web/inc/lib/sieve/SieveSemantics.php b/data/web/inc/lib/sieve/SieveSemantics.php
new file mode 100644
index 00000000..e131d5b4
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveSemantics.php
@@ -0,0 +1,611 @@
+<?php namespace Sieve;
+
+require_once('SieveKeywordRegistry.php');
+require_once('SieveToken.php');
+require_once('SieveException.php');
+
+class SieveSemantics
+{
+    protected static $requiredExtensions_ = array();
+
+    protected $comparator_;
+    protected $matchType_;
+    protected $addressPart_;
+    protected $tags_ = array();
+    protected $arguments_;
+    protected $deps_ = array();
+    protected $followupToken_;
+
+    public function __construct($token, $prevToken)
+    {
+        $this->registry_ = SieveKeywordRegistry::get();
+        $command = strtolower($token->text);
+
+        // Check the registry for $command
+        if ($this->registry_->isCommand($command))
+        {
+            $xml = $this->registry_->command($command);
+            $this->arguments_ = $this->makeArguments_($xml);
+            $this->followupToken_ = SieveToken::Semicolon;
+        }
+        else if ($this->registry_->isTest($command))
+        {
+            $xml = $this->registry_->test($command);
+            $this->arguments_ = $this->makeArguments_($xml);
+            $this->followupToken_ = SieveToken::BlockStart;
+        }
+        else
+        {
+            throw new SieveException($token, 'unknown command '. $command);
+        }
+
+        // Check if command may appear at this position within the script
+        if ($this->registry_->isTest($command))
+        {
+            if (is_null($prevToken))
+                throw new SieveException($token, $command .' may not appear as first command');
+
+            if (!preg_match('/^(if|elsif|anyof|allof|not)$/i', $prevToken->text))
+                throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
+        }
+        else if (isset($prevToken))
+        {
+            switch ($command)
+            {
+            case 'require':
+                $valid_after = 'require';
+                break;
+            case 'elsif':
+            case 'else':
+                $valid_after = '(if|elsif)';
+                break;
+            default:
+                $valid_after = $this->commandsRegex_();
+            }
+
+            if (!preg_match('/^'. $valid_after .'$/i', $prevToken->text))
+                throw new SieveException($token, $command .' may not appear after '. $prevToken->text);
+        }
+
+        // Check for extension arguments to add to the command
+        foreach ($this->registry_->arguments($command) as $arg)
+        {
+            switch ((string) $arg['type'])
+            {
+            case 'tag':
+                array_unshift($this->arguments_, array(
+                    'type'       => SieveToken::Tag,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->regex_($arg),
+                    'call'       => 'tagHook_',
+                    'name'       => $this->name_($arg),
+                    'subArgs'    => $this->makeArguments_($arg->children())
+                ));
+                break;
+            }
+        }
+    }
+
+    public function __destruct()
+    {
+        $this->registry_->put();
+    }
+
+    // TODO: the *Regex functions could possibly also be static properties
+    protected function requireStringsRegex_()
+    {
+        return '('. implode('|', $this->registry_->requireStrings()) .')';
+    }
+
+    protected function matchTypeRegex_()
+    {
+        return '('. implode('|', $this->registry_->matchTypes()) .')';
+    }
+
+    protected function addressPartRegex_()
+    {
+        return '('. implode('|', $this->registry_->addressParts()) .')';
+    }
+
+    protected function commandsRegex_()
+    {
+        return '('. implode('|', $this->registry_->commands()) .')';
+    }
+
+    protected function testsRegex_()
+    {
+        return '('. implode('|', $this->registry_->tests()) .')';
+    }
+
+    protected function comparatorRegex_()
+    {
+        return '('. implode('|', $this->registry_->comparators()) .')';
+    }
+
+    protected function occurrence_($arg)
+    {
+        if (isset($arg['occurrence']))
+        {
+            switch ((string) $arg['occurrence'])
+            {
+            case 'optional':
+                return '?';
+            case 'any':
+                return '*';
+            case 'some':
+                return '+';
+            }
+        }
+        return '1';
+    }
+
+    protected function name_($arg)
+    {
+        if (isset($arg['name']))
+        {
+            return (string) $arg['name'];
+        }
+        return (string) $arg['type'];
+    }
+
+    protected function regex_($arg)
+    {
+        if (isset($arg['regex']))
+        {
+            return (string) $arg['regex'];
+        }
+        return '.*';
+    }
+
+    protected function case_($arg)
+    {
+        if (isset($arg['case']))
+        {
+            return (string) $arg['case'];
+        }
+        return 'adhere';
+    }
+
+    protected function follows_($arg)
+    {
+        if (isset($arg['follows']))
+        {
+            return (string) $arg['follows'];
+        }
+        return '.*';
+    }
+
+    protected function makeValue_($arg)
+    {
+        if (isset($arg->value))
+        {
+            $res = $this->makeArguments_($arg->value);
+            return array_shift($res);
+        }
+        return null;
+    }
+
+    /**
+     * Convert an extension (test) commands parameters from XML to
+     * a PHP array the {@see Semantics} class understands.
+     * @param array(SimpleXMLElement) $parameters
+     * @return array
+     */
+    protected function makeArguments_($parameters)
+    {
+        $arguments = array();
+
+        foreach ($parameters as $arg)
+        {
+            // Ignore anything not a <parameter>
+            if ($arg->getName() != 'parameter')
+                continue;
+
+            switch ((string) $arg['type'])
+            {
+            case 'addresspart':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Tag,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->addressPartRegex_(),
+                    'call'       => 'addressPartHook_',
+                    'name'       => 'address part',
+                    'subArgs'    => $this->makeArguments_($arg)
+                ));
+                break;
+
+            case 'block':
+                array_push($arguments, array(
+                    'type'       => SieveToken::BlockStart,
+                    'occurrence' => '1',
+                    'regex'      => '{',
+                    'name'       => 'block',
+                    'subArgs'    => $this->makeArguments_($arg)
+                ));
+                break;
+
+            case 'comparator':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Tag,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => 'comparator',
+                    'name'       => 'comparator',
+                    'subArgs'    => array( array(
+                        'type'       => SieveToken::String,
+                        'occurrence' => '1',
+                        'call'       => 'comparatorHook_',
+                        'case'       => 'adhere',
+                        'regex'      => $this->comparatorRegex_(),
+                        'name'       => 'comparator string',
+                        'follows'    => 'comparator'
+                    ))
+                ));
+                break;
+
+            case 'matchtype':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Tag,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->matchTypeRegex_(),
+                    'call'       => 'matchTypeHook_',
+                    'name'       => 'match type',
+                    'subArgs'    => $this->makeArguments_($arg)
+                ));
+                break;
+
+            case 'number':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Number,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->regex_($arg),
+                    'name'       => $this->name_($arg),
+                    'follows'    => $this->follows_($arg)
+                ));
+                break;
+
+            case 'requirestrings':
+                array_push($arguments, array(
+                    'type'       => SieveToken::StringList,
+                    'occurrence' => $this->occurrence_($arg),
+                    'call'       => 'setRequire_',
+                    'case'       => 'adhere',
+                    'regex'      => $this->requireStringsRegex_(),
+                    'name'       => $this->name_($arg)
+                ));
+                break;
+
+            case 'string':
+                array_push($arguments, array(
+                    'type'       => SieveToken::String,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->regex_($arg),
+                    'case'       => $this->case_($arg),
+                    'name'       => $this->name_($arg),
+                    'follows'    => $this->follows_($arg)
+                ));
+                break;
+
+            case 'stringlist':
+                array_push($arguments, array(
+                    'type'       => SieveToken::StringList,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->regex_($arg),
+                    'case'       => $this->case_($arg),
+                    'name'       => $this->name_($arg),
+                    'follows'    => $this->follows_($arg)
+                ));
+                break;
+
+            case 'tag':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Tag,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->regex_($arg),
+                    'call'       => 'tagHook_',
+                    'name'       => $this->name_($arg),
+                    'subArgs'    => $this->makeArguments_($arg->children()),
+                    'follows'    => $this->follows_($arg)
+                ));
+                break;
+
+            case 'test':
+                array_push($arguments, array(
+                    'type'       => SieveToken::Identifier,
+                    'occurrence' => $this->occurrence_($arg),
+                    'regex'      => $this->testsRegex_(),
+                    'name'       => $this->name_($arg),
+                    'subArgs'    => $this->makeArguments_($arg->children())
+                ));
+                break;
+
+            case 'testlist':
+                array_push($arguments, array(
+                    'type'       => SieveToken::LeftParenthesis,
+                    'occurrence' => '1',
+                    'regex'      => '\(',
+                    'name'       => $this->name_($arg),
+                    'subArgs'    => null
+                ));
+                array_push($arguments, array(
+                    'type'       => SieveToken::Identifier,
+                    'occurrence' => '+',
+                    'regex'      => $this->testsRegex_(),
+                    'name'       => $this->name_($arg),
+                    'subArgs'    => $this->makeArguments_($arg->children())
+                ));
+                break;
+            }
+        }
+
+        return $arguments;
+    }
+
+    /**
+     * Add argument(s) expected / allowed to appear next.
+     * @param array $value
+     */
+    protected function addArguments_($identifier, $subArgs)
+    {
+        for ($i = count($subArgs); $i > 0; $i--)
+        {
+            $arg = $subArgs[$i-1];
+            if (preg_match('/^'. $arg['follows'] .'$/si', $identifier))
+                array_unshift($this->arguments_, $arg);
+        }
+    }
+
+    /**
+     * Add dependency that is expected to be fullfilled when parsing
+     * of the current command is {@see done}.
+     * @param array $dependency
+     */
+    protected function addDependency_($type, $name, $dependencies)
+    {
+        foreach ($dependencies as $d)
+        {
+            array_push($this->deps_, array(
+                'o_type' => $type,
+                'o_name' => $name,
+                'type'   => $d['type'],
+                'name'   => $d['name'],
+                'regex'  => $d['regex']
+            ));
+        }
+    }
+
+    protected function invoke_($token, $func, $arg = array())
+    {
+        if (!is_array($arg))
+            $arg = array($arg);
+
+        $err = call_user_func_array(array(&$this, $func), $arg);
+
+        if ($err)
+            throw new SieveException($token, $err);
+    }
+
+    protected function setRequire_($extension)
+    {
+        array_push(self::$requiredExtensions_, $extension);
+        $this->registry_->activate($extension);
+    }
+
+    /**
+     * Hook function that is called after a address part match was found
+     * in a command. The kind of address part is remembered in case it's
+     * needed later {@see done}. For address parts from a extension
+     * dependency information and valid values are looked up as well.
+     * @param string $addresspart
+     */
+    protected function addressPartHook_($addresspart)
+    {
+        $this->addressPart_ = $addresspart;
+        $xml = $this->registry_->addresspart($this->addressPart_);
+
+        if (isset($xml))
+        {
+            // Add possible value and dependancy
+            $this->addArguments_($this->addressPart_, $this->makeArguments_($xml));
+            $this->addDependency_('address part', $this->addressPart_, $xml->requires);
+        }
+    }
+
+    /**
+     * Hook function that is called after a match type was found in a
+     * command. The kind of match type is remembered in case it's
+     * needed later {@see done}. For a match type from extensions
+     * dependency information and valid values are looked up as well.
+     * @param string $matchtype
+     */
+    protected function matchTypeHook_($matchtype)
+    {
+        $this->matchType_ = $matchtype;
+        $xml = $this->registry_->matchtype($this->matchType_);
+
+        if (isset($xml))
+        {
+            // Add possible value and dependancy
+            $this->addArguments_($this->matchType_, $this->makeArguments_($xml));
+            $this->addDependency_('match type', $this->matchType_, $xml->requires);
+        }
+    }
+
+    /**
+     * Hook function that is called after a comparator was found in
+     * a command. The comparator is remembered in case it's needed for
+     * comparsion later {@see done}. For a comparator from extensions
+     * dependency information is looked up as well.
+     * @param string $comparator
+     */
+    protected function comparatorHook_($comparator)
+    {
+        $this->comparator_ = $comparator;
+        $xml = $this->registry_->comparator($this->comparator_);
+
+        if (isset($xml))
+        {
+            // Add possible dependancy
+            $this->addDependency_('comparator', $this->comparator_, $xml->requires);
+        }
+    }
+
+    /**
+     * Hook function that is called after a tag was found in
+     * a command. The tag is remembered in case it's needed for
+     * comparsion later {@see done}. For a tags from extensions
+     * dependency information is looked up as well.
+     * @param string $tag
+     */
+    protected function tagHook_($tag)
+    {
+        array_push($this->tags_, $tag);
+        $xml = $this->registry_->argument($tag);
+
+        // Add possible dependancies
+        if (isset($xml))
+            $this->addDependency_('tag', $tag, $xml->requires);
+    }
+
+    protected function validType_($token)
+    {
+        foreach ($this->arguments_ as $arg)
+        {
+            if ($arg['occurrence'] == '0')
+            {
+                array_shift($this->arguments_);
+                continue;
+            }
+
+            if ($token->is($arg['type']))
+                return;
+
+            // Is the argument required
+            if ($arg['occurrence'] != '?' && $arg['occurrence'] != '*')
+                throw new SieveException($token, $arg['type']);
+
+            array_shift($this->arguments_);
+        }
+
+        // Check if command expects any (more) arguments
+        if (empty($this->arguments_))
+            throw new SieveException($token, $this->followupToken_);
+
+        throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
+    }
+
+    public function startStringList($token)
+    {
+        $this->validType_($token);
+        $this->arguments_[0]['type'] = SieveToken::String;
+        $this->arguments_[0]['occurrence'] = '+';
+    }
+
+    public function continueStringList()
+    {
+        $this->arguments_[0]['occurrence'] = '+';
+    }
+
+    public function endStringList()
+    {
+        array_shift($this->arguments_);
+    }
+
+    public function validateToken($token)
+    {
+        // Make sure the argument has a valid type
+        $this->validType_($token);
+
+        foreach ($this->arguments_ as &$arg)
+        {
+            // Build regular expression according to argument type
+            switch ($arg['type'])
+            {
+            case SieveToken::String:
+            case SieveToken::StringList:
+                $regex = '/^(?:text:[^\n]*\n(?P<one>'. $arg['regex'] .')\.\r?\n?|"(?P<two>'. $arg['regex'] .')")$/'
+                       . ($arg['case'] == 'ignore' ? 'si' : 's');
+                break;
+            case SieveToken::Tag:
+                $regex = '/^:(?P<one>'. $arg['regex'] .')$/si';
+                break;
+            default:
+                $regex = '/^(?P<one>'. $arg['regex'] .')$/si';
+            }
+
+            if (preg_match($regex, $token->text, $match))
+            {
+                $text = ($match['one'] ? $match['one'] : $match['two']);
+
+                // Add argument(s) that may now appear after this one
+                if (isset($arg['subArgs']))
+                    $this->addArguments_($text, $arg['subArgs']);
+
+                // Call extra processing function if defined
+                if (isset($arg['call']))
+                    $this->invoke_($token, $arg['call'], $text);
+
+                // Check if a possible value of this argument may occur
+                if ($arg['occurrence'] == '?' || $arg['occurrence'] == '1')
+                {
+                    $arg['occurrence'] = '0';
+                }
+                else if ($arg['occurrence'] == '+')
+                {
+                    $arg['occurrence'] = '*';
+                }
+
+                return;
+            }
+
+            if ($token->is($arg['type']) && $arg['occurrence'] == 1)
+            {
+                throw new SieveException($token,
+                    SieveToken::typeString($token->type) ." $token->text where ". $arg['name'] .' expected');
+            }
+        }
+
+        throw new SieveException($token, 'unexpected '. SieveToken::typeString($token->type) .' '. $token->text);
+    }
+
+    public function done($token)
+    {
+        // Check if there are required arguments left
+        foreach ($this->arguments_ as $arg)
+        {
+            if ($arg['occurrence'] == '+' || $arg['occurrence'] == '1')
+                throw new SieveException($token, $arg['type']);
+        }
+
+        // Check if the command depends on use of a certain tag
+        foreach ($this->deps_ as $d)
+        {
+            switch ($d['type'])
+            {
+            case 'addresspart':
+                $values = array($this->addressPart_);
+                break;
+
+            case 'matchtype':
+                $values = array($this->matchType_);
+                break;
+
+            case 'comparator':
+                $values = array($this->comparator_);
+                break;
+
+            case 'tag':
+                $values = $this->tags_;
+                break;
+            }
+
+            foreach ($values as $value)
+            {
+                if (preg_match('/^'. $d['regex'] .'$/mi', $value))
+                    break 2;
+            }
+
+            throw new SieveException($token,
+                $d['o_type'] .' '. $d['o_name'] .' requires use of '. $d['type'] .' '. $d['name']);
+        }
+    }
+}
diff --git a/data/web/inc/lib/sieve/SieveToken.php b/data/web/inc/lib/sieve/SieveToken.php
new file mode 100644
index 00000000..459f45bc
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveToken.php
@@ -0,0 +1,88 @@
+<?php namespace Sieve;
+
+include_once('SieveDumpable.php');
+
+class SieveToken implements SieveDumpable
+{
+    const Unknown          = 0x0000;
+    const ScriptEnd        = 0x0001;
+    const LeftBracket      = 0x0002;
+    const RightBracket     = 0x0004;
+    const BlockStart       = 0x0008;
+    const BlockEnd         = 0x0010;
+    const LeftParenthesis  = 0x0020;
+    const RightParenthesis = 0x0040;
+    const Comma            = 0x0080;
+    const Semicolon        = 0x0100;
+    const Whitespace       = 0x0200;
+    const Tag              = 0x0400;
+    const QuotedString     = 0x0800;
+    const Number           = 0x1000;
+    const Comment          = 0x2000;
+    const MultilineString  = 0x4000;
+    const Identifier       = 0x8000;
+
+    const String        = 0x4800; // Quoted | Multiline
+    const StringList    = 0x4802; // Quoted | Multiline | LeftBracket
+    const StringListSep = 0x0084; // Comma | RightBracket
+    const Unparsed      = 0x2200; // Comment | Whitespace
+    const TestList      = 0x8020; // Identifier | LeftParenthesis
+
+    public $type;
+    public $text;
+    public $line;
+
+    public function __construct($type, $text, $line)
+    {
+        $this->text = $text;
+        $this->type = $type;
+        $this->line = intval($line);
+    }
+
+    public function dump()
+    {
+        return '<'. SieveToken::escape($this->text) .'> type:'. SieveToken::typeString($this->type) .' line:'. $this->line;
+    }
+
+    public function text()
+    {
+        return $this->text;
+    }
+
+    public function is($type)
+    {
+        return (bool)($this->type & $type);
+    }
+
+    public static function typeString($type)
+    {
+        switch ($type)
+        {
+        case SieveToken::Identifier: return 'identifier';
+        case SieveToken::Whitespace: return 'whitespace';
+        case SieveToken::QuotedString: return 'quoted string';
+        case SieveToken::Tag: return 'tag';
+        case SieveToken::Semicolon: return 'semicolon';
+        case SieveToken::LeftBracket: return 'left bracket';
+        case SieveToken::RightBracket: return 'right bracket';
+        case SieveToken::BlockStart: return 'block start';
+        case SieveToken::BlockEnd: return 'block end';
+        case SieveToken::LeftParenthesis: return 'left parenthesis';
+        case SieveToken::RightParenthesis: return 'right parenthesis';
+        case SieveToken::Comma: return 'comma';
+        case SieveToken::Number: return 'number';
+        case SieveToken::Comment: return 'comment';
+        case SieveToken::MultilineString: return 'multiline string';
+        case SieveToken::ScriptEnd: return 'script end';
+        case SieveToken::String: return 'string';
+        case SieveToken::StringList: return 'string list';
+        default: return 'unknown token';
+        }
+    }
+
+    protected static $tr_ = array("\r" => '\r', "\n" => '\n', "\t" => '\t');
+    public static function escape($val)
+    {
+        return strtr($val, self::$tr_);
+    }
+}
diff --git a/data/web/inc/lib/sieve/SieveTree.php b/data/web/inc/lib/sieve/SieveTree.php
new file mode 100644
index 00000000..49c73496
--- /dev/null
+++ b/data/web/inc/lib/sieve/SieveTree.php
@@ -0,0 +1,117 @@
+<?php namespace Sieve;
+
+class SieveTree
+{
+    protected $childs_;
+    protected $parents_;
+    protected $nodes_;
+    protected $max_id_;
+    protected $dump_;
+
+    public function __construct($name = 'tree')
+    {
+        $this->childs_ = array();
+        $this->parents_ = array();
+        $this->nodes_ = array();
+        $this->max_id_ = 0;
+
+        $this->parents_[0] = null;
+        $this->nodes_[0] = $name;
+    }
+
+    public function addChild(SieveDumpable $child)
+    {
+        return $this->addChildTo($this->max_id_, $child);
+    }
+
+    public function addChildTo($parent_id, SieveDumpable $child)
+    {
+        if (!is_int($parent_id)
+         || !isset($this->nodes_[$parent_id]))
+            return null;
+
+        if (!isset($this->childs_[$parent_id]))
+            $this->childs_[$parent_id] = array();
+
+        $child_id = ++$this->max_id_;
+        $this->nodes_[$child_id] = $child;
+        $this->parents_[$child_id] = $parent_id;
+        array_push($this->childs_[$parent_id], $child_id);
+
+        return $child_id;
+    }
+
+    public function getRoot()
+    {
+        return 0;
+    }
+
+    public function getChilds($node_id)
+    {
+        if (!is_int($node_id)
+        || !isset($this->nodes_[$node_id]))
+            return null;
+
+        if (!isset($this->childs_[$node_id]))
+            return array();
+
+        return $this->childs_[$node_id];
+    }
+
+    public function getNode($node_id)
+    {
+        if ($node_id == 0 || !is_int($node_id)
+         || !isset($this->nodes_[$node_id]))
+            return null;
+
+        return $this->nodes_[$node_id];
+    }
+
+    public function dump()
+    {
+        $this->dump_ = $this->nodes_[$this->getRoot()] ."\n";
+        $this->dumpChilds_($this->getRoot(), ' ');
+        return $this->dump_;
+    }
+
+    protected function dumpChilds_($parent_id, $prefix)
+    {
+        if (!isset($this->childs_[$parent_id]))
+            return;
+
+        $childs = $this->childs_[$parent_id];
+        $last_child = count($childs);
+
+        for ($i=1; $i <= $last_child; ++$i)
+        {
+            $child_node = $this->nodes_[$childs[$i-1]];
+            $infix = ($i == $last_child ? '`--- ' : '|--- ');
+            $this->dump_ .= $prefix . $infix . $child_node->dump() . " (id:" . $childs[$i-1] . ")\n";
+
+            $next_prefix = $prefix . ($i == $last_child ? '   ' : '|  ');
+            $this->dumpChilds_($childs[$i-1], $next_prefix);
+        }
+    }
+
+    public function getText()
+    {
+        $this->dump_ = '';
+        $this->childText_($this->getRoot());
+        return $this->dump_;
+    }
+
+    protected function childText_($parent_id)
+    {
+        if (!isset($this->childs_[$parent_id]))
+            return;
+
+        $childs = $this->childs_[$parent_id];
+
+        for ($i = 0; $i < count($childs); ++$i)
+        {
+            $child_node = $this->nodes_[$childs[$i]];
+            $this->dump_ .= $child_node->text();
+            $this->childText_($childs[$i]);
+        }
+    }
+}
diff --git a/data/web/inc/lib/sieve/extensions/body.xml b/data/web/inc/lib/sieve/extensions/body.xml
new file mode 100644
index 00000000..657b845e
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/body.xml
@@ -0,0 +1,14 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="body">
+
+	<test name="body">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="tag" name="body transform" regex="(raw|content|text)" occurrence="optional">
+			<parameter type="stringlist" name="content types" follows="content" />
+		</parameter>
+		<parameter type="stringlist" name="key list" />
+	</test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml b/data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml
new file mode 100644
index 00000000..6f96f8d1
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/comparator-ascii-numeric.xml
@@ -0,0 +1,7 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="comparator-i;ascii-numeric">
+
+	<comparator name="i;ascii-numeric" />
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/copy.xml b/data/web/inc/lib/sieve/extensions/copy.xml
new file mode 100644
index 00000000..4e3f9020
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/copy.xml
@@ -0,0 +1,9 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="copy">
+
+	<tagged-argument extends="(fileinto|redirect)">
+		<parameter type="tag" name="copy" regex="copy" occurrence="optional" />
+	</tagged-argument>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/date.xml b/data/web/inc/lib/sieve/extensions/date.xml
new file mode 100644
index 00000000..08f65409
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/date.xml
@@ -0,0 +1,28 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="date">
+
+	<test name="date">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="tag" name="zone" regex="(zone|originalzone)" occurrence="optional">
+			<parameter type="string" name="time-zone" follows="zone" />
+		</parameter>
+		<parameter type="string" name="header-name" />
+		<parameter type="string" case="ignore" name="date-part"
+		 regex="(year|month|day|date|julian|hour|minute|second|time|iso8601|std11|zone|weekday)" />
+		<parameter type="stringlist" name="key-list" />
+	</test>
+
+	<test name="currentdate">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="tag" name="zone" regex="zone" occurrence="optional">
+			<parameter type="string" name="time-zone" />
+		</parameter>
+		<parameter type="string" case="ignore" name="date-part"
+		 regex="(year|month|day|date|julian|hour|minute|second|time|iso8601|std11|zone|weekday)" />
+		<parameter type="stringlist" name="key-list" />
+	</test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/editheader.xml b/data/web/inc/lib/sieve/extensions/editheader.xml
new file mode 100644
index 00000000..52244820
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/editheader.xml
@@ -0,0 +1,22 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="editheader">
+
+	<command name="addheader">
+		<parameter type="tag" name="last" regex="last" occurrence="optional" />
+		<parameter type="string" name="field name" />
+		<parameter type="string" name="value" />
+	</command>
+
+	<command name="deleteheader">
+		<parameter type="tag" name="index" regex="index" occurrence="optional">
+			<parameter type="number" name="field number" />
+			<parameter type="tag" name="last" regex="last" occurrence="optional" />
+		</parameter>
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="string" name="field name" />
+		<parameter type="stringlist" name="value patterns" occurrence="optional" />
+	</command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/envelope.xml b/data/web/inc/lib/sieve/extensions/envelope.xml
new file mode 100644
index 00000000..ce88ada9
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/envelope.xml
@@ -0,0 +1,13 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="envelope">
+
+	<test name="envelope">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="addresspart" occurrence="optional" />
+		<parameter type="stringlist" name="envelope-part" />
+		<parameter type="stringlist" name="key" />
+	</test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/environment.xml b/data/web/inc/lib/sieve/extensions/environment.xml
new file mode 100644
index 00000000..edaab8df
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/environment.xml
@@ -0,0 +1,13 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="environment">
+
+	<test name="environment">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="string" name="name"
+		 regex="(domain|host|location|name|phase|remote-host|remote-ip|version|vnd\..+)" />
+		<parameter type="stringlist" name="key-list" />
+	</test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/ereject.xml b/data/web/inc/lib/sieve/extensions/ereject.xml
new file mode 100644
index 00000000..f7230195
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/ereject.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="ereject">
+
+	<command name="ereject">
+
+		<parameter type="string" name="reason" />
+
+	</command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/fileinto.xml b/data/web/inc/lib/sieve/extensions/fileinto.xml
new file mode 100644
index 00000000..3b48a5c0
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/fileinto.xml
@@ -0,0 +1,9 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="fileinto">
+
+	<command name="fileinto">
+		<parameter type="string" name="folder" />
+	</command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/imap4flags.xml b/data/web/inc/lib/sieve/extensions/imap4flags.xml
new file mode 100644
index 00000000..5f6d176e
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/imap4flags.xml
@@ -0,0 +1,29 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="imap4flags">
+
+	<command name="setflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+	<command name="addflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+	<command name="removeflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+	<test name="hasflag">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="stringlist" name="flag list" />
+	</test>
+
+	<tagged-argument extends="(fileinto|keep)">
+		<parameter type="tag" name="flags" regex="flags" occurrence="optional">
+			<parameter type="stringlist" name="flag list" />
+		</parameter>
+	</tagged-argument>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/imapflags.xml b/data/web/inc/lib/sieve/extensions/imapflags.xml
new file mode 100644
index 00000000..4b78cc80
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/imapflags.xml
@@ -0,0 +1,21 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="imapflags">
+
+	<command name="mark" />
+
+	<command name="unmark" />
+
+	<command name="setflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+	<command name="addflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+	<command name="removeflag">
+		<parameter type="stringlist" name="flag list" />
+	</command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/index.xml b/data/web/inc/lib/sieve/extensions/index.xml
new file mode 100644
index 00000000..f81055c5
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/index.xml
@@ -0,0 +1,17 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="index">
+
+	<tagged-argument extends="(header|address|date)">
+		<parameter type="tag" name="index" regex="index" occurrence="optional">
+			<parameter type="number" name="field number" />
+		</parameter>
+	</tagged-argument>
+
+	<tagged-argument extends="(header|address|date)">
+		<parameter type="tag" name="last" regex="last" occurrence="optional">
+			<requires type="tag" name="index" regex="index" />
+		</parameter>
+	</tagged-argument>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/notify.xml b/data/web/inc/lib/sieve/extensions/notify.xml
new file mode 100644
index 00000000..e1702e95
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/notify.xml
@@ -0,0 +1,29 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="notify">
+
+	<command name="notify">
+		<parameter type="tag" name="method" regex="method" occurrence="optional">
+			<parameter type="string" name="method-name" />
+		</parameter>
+
+		<parameter type="tag" name="id" regex="id" occurrence="optional">
+			<parameter type="string" name="message-id" />
+		</parameter>
+
+		<parameter type="tag" name="priority" regex="(low|normal|high)" occurrence="optional" />
+
+		<parameter type="tag" name="message" regex="message" occurrence="optional">
+			<parameter type="string" name="message-text" />
+		</parameter>
+	</command>
+
+	<command name="denotify">
+		<parameter type="matchtype" occurrence="optional">
+			<parameter type="string" name="message-id" />
+		</parameter>
+
+		<parameter type="tag" name="priority" regex="(low|normal|high)" occurrence="optional" />
+	</command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/regex.xml b/data/web/inc/lib/sieve/extensions/regex.xml
new file mode 100644
index 00000000..79d67fc0
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/regex.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="regex">
+
+	<matchtype name="regex" />
+
+	<tagged-argument extends="set">
+		<parameter type="tag" name="modifier" regex="quoteregex" occurrence="optional" />
+	</tagged-argument>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/reject.xml b/data/web/inc/lib/sieve/extensions/reject.xml
new file mode 100644
index 00000000..33d2573f
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/reject.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="reject">
+
+	<command name="reject">
+
+		<parameter type="string" name="reason" />
+
+	</command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/relational.xml b/data/web/inc/lib/sieve/extensions/relational.xml
new file mode 100644
index 00000000..b9e2b39e
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/relational.xml
@@ -0,0 +1,14 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="relational">
+
+	<matchtype name="count">
+		<requires type="comparator" name="i;ascii-numeric" regex="i;ascii-numeric" />
+		<parameter type="string" name="relation string" regex="(lt|le|eq|ge|gt|ne)" />
+	</matchtype>
+
+	<matchtype name="value">
+		<parameter type="string" name="relation string" regex="(lt|le|eq|ge|gt|ne)" />
+	</matchtype>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/spamtest.xml b/data/web/inc/lib/sieve/extensions/spamtest.xml
new file mode 100644
index 00000000..06c8c3bc
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/spamtest.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="spamtest">
+
+	<test name="spamtest">
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="string" name="value" />
+	</test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/spamtestplus.xml b/data/web/inc/lib/sieve/extensions/spamtestplus.xml
new file mode 100644
index 00000000..c7b768d9
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/spamtestplus.xml
@@ -0,0 +1,12 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="spamtestplus">
+
+	<test name="spamtest" overrides="true">
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="tag" name="percent" regex="percent" occurrence="optional" />
+		<parameter type="string" name="value" />
+	</test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/subaddress.xml b/data/web/inc/lib/sieve/extensions/subaddress.xml
new file mode 100644
index 00000000..a668fdf9
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/subaddress.xml
@@ -0,0 +1,8 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="subaddress">
+
+	<addresspart name="user" />
+	<addresspart name="detail" />
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/vacation.xml b/data/web/inc/lib/sieve/extensions/vacation.xml
new file mode 100644
index 00000000..dbd6992a
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/vacation.xml
@@ -0,0 +1,31 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="vacation">
+
+	<command name="vacation">
+		<parameter type="tag" name="days" occurrence="optional" regex="days">
+			<parameter type="number" name="period" />
+		</parameter>
+
+		<parameter type="tag" name="addresses" occurrence="optional" regex="addresses">
+			<parameter type="stringlist" name="address strings" />
+		</parameter>
+
+		<parameter type="tag" name="subject" occurrence="optional" regex="subject">
+			<parameter type="string" name="subject string" />
+		</parameter>
+
+		<parameter type="tag" name="from" occurrence="optional" regex="from">
+			<parameter type="string" name="from string" />
+		</parameter>
+
+		<parameter type="tag" name="handle" occurrence="optional" regex="handle">
+			<parameter type="string" name="handle string" />
+		</parameter>
+
+		<parameter type="tag" name="mime" occurrence="optional" regex="mime" />
+
+		<parameter type="string" name="reason" />
+	</command>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/variables.xml b/data/web/inc/lib/sieve/extensions/variables.xml
new file mode 100644
index 00000000..d9ff0008
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/variables.xml
@@ -0,0 +1,21 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="variables">
+
+	<command name="set">
+		<parameter type="tag" name="modifier" regex="(lower|upper)" occurrence="optional" />
+		<parameter type="tag" name="modifier" regex="(lower|upper)first" occurrence="optional" />
+		<parameter type="tag" name="modifier" regex="quotewildcard" occurrence="optional" />
+		<parameter type="tag" name="modifier" regex="length" occurrence="optional" />
+		<parameter type="string" name="name" regex="[[:alpha:]_][[:alnum:]_]*" />
+		<parameter type="string" name="value" />
+	</command>
+
+	<test name="string">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="stringlist" name="source" />
+		<parameter type="stringlist" name="key list" />
+	</test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/extensions/virustest.xml b/data/web/inc/lib/sieve/extensions/virustest.xml
new file mode 100644
index 00000000..6dac8e87
--- /dev/null
+++ b/data/web/inc/lib/sieve/extensions/virustest.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' standalone='yes'?>
+
+<extension name="virustest">
+
+	<test name="virustest">
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="string" name="value" />
+	</test>
+
+</extension>
diff --git a/data/web/inc/lib/sieve/keywords.xml b/data/web/inc/lib/sieve/keywords.xml
new file mode 100644
index 00000000..1ab7c4d2
--- /dev/null
+++ b/data/web/inc/lib/sieve/keywords.xml
@@ -0,0 +1,91 @@
+<?xml version='1.0' standalone='yes'?>
+
+<keywords>
+
+	<matchtype name="is" />
+	<matchtype name="contains" />
+	<matchtype name="matches" />
+	<matchtype name="value">
+		<parameter type="string" name="operator" regex="(gt|ge|eq|le|lt)" />
+	</matchtype>
+
+
+	<comparator name="i;octet" />
+	<comparator name="i;ascii-casemap" />
+	<comparator name="i;unicode-casemap" />
+
+	<addresspart name="all" />
+	<addresspart name="localpart" />
+	<addresspart name="domain" />
+
+
+	<command name="discard" />
+
+	<command name="elsif">
+		<parameter type="test" name="test command" />
+		<parameter type="block" />
+	</command>
+
+	<command name="else">
+		<parameter type="block" />
+	</command>
+
+	<command name="if">
+		<parameter type="test" name="test command" />
+		<parameter type="block" />
+	</command>
+
+	<command name="keep" />
+
+	<command name="redirect">
+		<parameter type="string" name="address string" />
+	</command>
+
+	<command name="require">
+		<parameter type="requirestrings" name="require string" />
+	</command>
+
+	<command name="stop" />
+
+
+	<test name="address">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="addresspart" occurrence="optional" />
+		<parameter type="stringlist" name="header list" />
+		<parameter type="stringlist" name="key list" />
+	</test>
+
+	<test name="allof">
+		<parameter type="testlist" name="test" />
+	</test>
+
+	<test name="anyof">
+		<parameter type="testlist" name="test" />
+	</test>
+
+	<test name="exists">
+		<parameter type="stringlist" name="header names" />
+	</test>
+
+	<test name="false" />
+	
+	<test name="header">
+		<parameter type="matchtype" occurrence="optional" />
+		<parameter type="comparator" occurrence="optional" />
+		<parameter type="stringlist" name="header names" />
+		<parameter type="stringlist" name="key list" />
+	</test>
+
+	<test name="not">
+		<parameter type="test" />
+	</test>
+
+	<test name="size">
+		<parameter type="tag" regex="(over|under)" />
+		<parameter type="number" name="limit" />
+	</test>
+
+	<test name="true" />
+
+</keywords>
diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php
index cdb88804..1533e24f 100644
--- a/data/web/inc/prerequisites.inc.php
+++ b/data/web/inc/prerequisites.inc.php
@@ -16,6 +16,9 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/Yubico.php';
 // Autoload composer
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/vendor/autoload.php';
 
+// Load Sieve
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/sieve/SieveParser.php';
+
 // U2F API + T/HOTP API
 $u2f = new u2flib_server\U2F('https://' . $_SERVER['HTTP_HOST']);
 $tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL);
@@ -70,6 +73,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.relayhost.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.docker.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php';
 init_db_schema();
diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php
index abbef40f..fbdabfdc 100644
--- a/data/web/inc/sessions.inc.php
+++ b/data/web/inc/sessions.inc.php
@@ -62,6 +62,8 @@ if (isset($_POST["logout"])) {
     $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
     $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
     unset($_SESSION["dual-login"]);
+    header("Location: /mailbox.php");
+    exit();
   }
   else {
     session_regenerate_id(true);
diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php
index 98ceee7c..2b93af15 100644
--- a/data/web/inc/vars.inc.php
+++ b/data/web/inc/vars.inc.php
@@ -104,6 +104,9 @@ $MAILCOW_APPS = array(
 // Rows until pagination begins
 $PAGINATION_SIZE = 10;
 
+// Default number of rows/lines to display (log table)
+$LOG_LINES = 100;
+
 // Rows until pagination begins (log table)
 $LOG_PAGINATION_SIZE = 30;
 
diff --git a/data/web/js/admin.js b/data/web/js/admin.js
index 83829ab7..c368cbe7 100644
--- a/data/web/js/admin.js
+++ b/data/web/js/admin.js
@@ -1,125 +1,10 @@
-var Base64 = {
-    _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
-    encode: function(e) {
-        var t = "";
-        var n, r, i, s, o, u, a;
-        var f = 0;
-        e = Base64._utf8_encode(e);
-        while (f < e.length) {
-            n = e.charCodeAt(f++);
-            r = e.charCodeAt(f++);
-            i = e.charCodeAt(f++);
-            s = n >> 2;
-            o = (n & 3) << 4 | r >> 4;
-            u = (r & 15) << 2 | i >> 6;
-            a = i & 63;
-            if (isNaN(r)) {
-                u = a = 64
-            } else if (isNaN(i)) {
-                a = 64
-            }
-            t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) +
-                this._keyStr.charAt(u) + this._keyStr.charAt(a)
-        }
-        return t
-    },
-    decode: function(e) {
-        var t = "";
-        var n, r, i;
-        var s, o, u, a;
-        var f = 0;
-        e = e.replace(/[^A-Za-z0-9\+\/\=]/g, "");
-        while (f < e.length) {
-            s = this._keyStr.indexOf(e.charAt(f++));
-            o = this._keyStr.indexOf(e.charAt(f++));
-            u = this._keyStr.indexOf(e.charAt(f++));
-            a = this._keyStr.indexOf(e.charAt(f++));
-            n = s << 2 | o >> 4;
-            r = (o & 15) << 4 | u >> 2;
-            i = (u & 3) << 6 | a;
-            t = t + String.fromCharCode(n);
-            if (u != 64) {
-                t = t + String.fromCharCode(r)
-            }
-            if (a != 64) {
-                t = t + String.fromCharCode(i)
-            }
-        }
-        t = Base64._utf8_decode(t);
-        return t
-    },
-    _utf8_encode: function(e) {
-        e = e.replace(/\r\n/g, "\n");
-        var t = "";
-        for (var n = 0; n < e.length; n++) {
-            var r = e.charCodeAt(n);
-            if (r < 128) {
-                t += String.fromCharCode(r)
-            } else if (r > 127 && r < 2048) {
-                t += String.fromCharCode(r >> 6 | 192);
-                t += String.fromCharCode(r & 63 | 128)
-            } else {
-                t += String.fromCharCode(r >> 12 | 224);
-                t += String.fromCharCode(r >> 6 & 63 | 128);
-                t += String.fromCharCode(r & 63 | 128)
-            }
-        }
-        return t
-    },
-    _utf8_decode: function(e) {
-        var t = "";
-        var n = 0;
-        var r = c1 = c2 = 0;
-        while (n < e.length) {
-            r = e.charCodeAt(n);
-            if (r < 128) {
-                t += String.fromCharCode(r);
-                n++
-            } else if (r > 191 && r < 224) {
-                c2 = e.charCodeAt(n + 1);
-                t += String.fromCharCode((r & 31) << 6 | c2 & 63);
-                n += 2
-            } else {
-                c2 = e.charCodeAt(n + 1);
-                c3 = e.charCodeAt(n + 2);
-                t += String.fromCharCode((r & 15) << 12 | (c2 & 63) <<
-                    6 | c3 & 63);
-                n += 3
-            }
-        }
-        return t
-    }
-}
-  
+// 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 escapeHtml(string) {
-    return String(string).replace(/[&<>"'`=\/]/g, function (s) {
-      return entityMap[s];
-    });
-  }
-  function humanFileSize(bytes) {
-    if(Math.abs(bytes) < 1024) {
-        return bytes + ' B';
-    }
-    var units = ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
-    var u = -1;
-    do {
-        bytes /= 1024;
-        ++u;
-    } while(Math.abs(bytes) >= 1024 && u < units.length - 1);
-    return bytes.toFixed(1)+' '+units[u];
-  }
+  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_postfix_log").on('click', function(e) {
     e.preventDefault();
     draw_postfix_logs();
@@ -148,54 +33,6 @@ jQuery(function($){
     e.preventDefault();
     $('#import_dkim_arrow').toggleClass("animation"); 
   });
-  function draw_postfix_logs() {
-    ft_postfix_logs = FooTable.init('#postfix_log', {
-      "columns": [
-        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
-        {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
-        {"name":"message","title":lang.message},
-      ],
-      "rows": $.ajax({
-        dataType: 'json',
-        url: '/api/v1/get/logs/postfix/1000',
-        jsonp: false,
-        error: function () {
-          console.log('Cannot draw postfix log table');
-        },
-        success: function (data) {
-          $.each(data, function (i, item) {
-            item.message = escapeHtml(item.message);
-            var danger_class = ["emerg", "alert", "crit", "err"];
-            var warning_class = ["warning", "warn"];
-            var info_class = ["notice", "info", "debug"];
-            if (jQuery.inArray(item.priority, danger_class) !== -1) {
-              item.priority = '<span class="label label-danger">' + item.priority + '</span>';
-            } 
-            else if (jQuery.inArray(item.priority, warning_class) !== -1) {
-              item.priority = '<span class="label label-warning">' + item.priority + '</span>';
-            }
-            else if (jQuery.inArray(item.priority, info_class) !== -1) {
-              item.priority = '<span class="label label-info">' + item.priority + '</span>';
-            }
-          });
-        }
-      }),
-      "empty": lang.empty,
-      "paging": {
-        "enabled": true,
-        "limit": 5,
-        "size": log_pagination_size
-      },
-      "filtering": {
-        "enabled": true,
-        "position": "left",
-        "placeholder": lang.filter_table
-      },
-      "sorting": {
-        "enabled": true
-      }
-    });
-  }
   function draw_autodiscover_logs() {
     ft_autodiscover_logs = FooTable.init('#autodiscover_log', {
       "columns": [
@@ -212,33 +49,53 @@ jQuery(function($){
           console.log('Cannot draw autodiscover log table');
         },
         success: function (data) {
-          $.each(data, function (i, item) {
-            item.ua = '<span style="font-size:small">' + item.ua + '</span>';
-            if (item.service == "activesync") {
-              item.service = '<span class="label label-info">ActiveSync</span>';
-            }
-            else if (item.service == "imap") {
-              item.service = '<span class="label label-success">IMAP, SMTP, Cal-/CardDAV</span>';
-            }
-            else {
-              item.service = '<span class="label label-danger">' + item.service + '</span>';
-            }
-          });
+          return process_table_data(data, 'autodiscover_log');
         }
       }),
       "empty": lang.empty,
-      "paging": {
-        "enabled": true,
-        "limit": 5,
-        "size": log_pagination_size
-      },
-      "filtering": {
-        "enabled": true,
-        "position": "left",
-        "placeholder": lang.filter_table
-      },
-      "sorting": {
-        "enabled": true
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {"ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
+      }
+    });
+  }
+  function draw_postfix_logs() {
+    ft_postfix_logs = FooTable.init('#postfix_log', {
+      "columns": [
+        {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+        {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
+        {"name":"message","title":lang.message},
+      ],
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/logs/postfix',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw postfix log table');
+        },
+        success: function (data) {
+          return process_table_data(data, 'general_syslog');
+        }
+      }),
+      "empty": lang.empty,
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
       }
     });
   }
@@ -251,43 +108,27 @@ jQuery(function($){
       ],
       "rows": $.ajax({
         dataType: 'json',
-        url: '/api/v1/get/logs/fail2ban/1000',
+        url: '/api/v1/get/logs/fail2ban',
         jsonp: false,
         error: function () {
           console.log('Cannot draw fail2ban log table');
         },
         success: function (data) {
-          $.each(data, function (i, item) {
-            var danger_class = ["emerg", "alert", "crit", "err"];
-            var warning_class = ["warning", "warn"];
-            var info_class = ["notice", "info", "debug"];
-            item.message = escapeHtml(item.message);
-            if (jQuery.inArray(item.priority, danger_class) !== -1) {
-              item.priority = '<span class="label label-danger">' + item.priority + '</span>';
-            } 
-            else if (jQuery.inArray(item.priority, warning_class) !== -1) {
-              item.priority = '<span class="label label-warning">' + item.priority + '</span>';
-            }
-            else if (jQuery.inArray(item.priority, info_class) !== -1) {
-              item.priority = '<span class="label label-info">' + item.priority + '</span>';
-            }
-          });
+          return process_table_data(data, 'general_syslog');
         }
       }),
       "empty": lang.empty,
-      "paging": {
-        "enabled": true,
-        "limit": 5,
-        "size": log_pagination_size
-      },
-      "filtering": {
-        "enabled": true,
-        "position": "left",
-        "connectors": false,
-        "placeholder": lang.filter_table
-      },
-      "sorting": {
-        "enabled": true
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
       }
     });
   }
@@ -300,48 +141,32 @@ jQuery(function($){
       ],
       "rows": $.ajax({
         dataType: 'json',
-        url: '/api/v1/get/logs/sogo/1000',
+        url: '/api/v1/get/logs/sogo',
         jsonp: false,
         error: function () {
           console.log('Cannot draw sogo log table');
         },
         success: function (data) {
-          $.each(data, function (i, item) {
-            var danger_class = ["emerg", "alert", "crit", "err"];
-            var warning_class = ["warning", "warn"];
-            var info_class = ["notice", "info", "debug"];
-            item.message = escapeHtml(item.message);
-            if (jQuery.inArray(item.priority, danger_class) !== -1) {
-              item.priority = '<span class="label label-danger">' + item.priority + '</span>';
-            } 
-            else if (jQuery.inArray(item.priority, warning_class) !== -1) {
-              item.priority = '<span class="label label-warning">' + item.priority + '</span>';
-            }
-            else if (jQuery.inArray(item.priority, info_class) !== -1) {
-              item.priority = '<span class="label label-info">' + item.priority + '</span>';
-            }
-          });
+          return process_table_data(data, 'general_syslog');
         }
       }),
       "empty": lang.empty,
-      "paging": {
-        "enabled": true,
-        "limit": 5,
-        "size": log_pagination_size
-      },
-      "filtering": {
-        "enabled": true,
-        "position": "left",
-        "connectors": false,
-        "placeholder": lang.filter_table
-      },
-      "sorting": {
-        "enabled": true
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
       }
     });
   }
   function draw_dovecot_logs() {
-    ft_postfix_logs = FooTable.init('#dovecot_log', {
+    ft_dovecot_logs = FooTable.init('#dovecot_log', {
       "columns": [
         {"name":"time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
         {"name":"priority","title":lang.priority,"style":{"width":"80px"}},
@@ -349,43 +174,27 @@ jQuery(function($){
       ],
       "rows": $.ajax({
         dataType: 'json',
-        url: '/api/v1/get/logs/dovecot/1000',
+        url: '/api/v1/get/logs/dovecot',
         jsonp: false,
         error: function () {
           console.log('Cannot draw dovecot log table');
         },
         success: function (data) {
-          $.each(data, function (i, item) {
-            var danger_class = ["emerg", "alert", "crit", "err"];
-            var warning_class = ["warning", "warn"];
-            var info_class = ["notice", "info", "debug"];
-            item.message = escapeHtml(item.message);
-            if (jQuery.inArray(item.priority, danger_class) !== -1) {
-              item.priority = '<span class="label label-danger">' + item.priority + '</span>';
-            } 
-            else if (jQuery.inArray(item.priority, warning_class) !== -1) {
-              item.priority = '<span class="label label-warning">' + item.priority + '</span>';
-            }
-            else if (jQuery.inArray(item.priority, info_class) !== -1) {
-              item.priority = '<span class="label label-info">' + item.priority + '</span>';
-            }
-          });
+          return process_table_data(data, 'general_syslog');
         }
       }),
       "empty": lang.empty,
-      "paging": {
-        "enabled": true,
-        "limit": 5,
-        "size": log_pagination_size
-      },
-      "filtering": {
-        "enabled": true,
-        "position": "left",
-        "connectors": false,
-        "placeholder": lang.filter_table
-      },
-      "sorting": {
-        "enabled": true
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
       }
     });
   }
@@ -407,30 +216,14 @@ jQuery(function($){
           console.log('Cannot draw domain admin table');
         },
         success: function (data) {
-          $.each(data, function (i, item) {
-            item.chkbox = '<input type="checkbox" data-id="domain_admins" name="multi_select" value="' + item.username + '" />';
-            item.action = '<div class="btn-group">' +
-              '<a href="/edit.php?domainadmin=' + encodeURI(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-              '<a href="#" id="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
-              '</div>';
-          });
+          return process_table_data(data, 'domainadminstable');
         }
       }),
       "empty": lang.empty,
-      "paging": {
-        "enabled": true,
-        "limit": 5,
-        "size": log_pagination_size
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table
       },
-      "filtering": {
-        "enabled": true,
-        "position": "left",
-        "connectors": false,
-        "placeholder": lang.filter_table
-      },
-      "sorting": {
-        "enabled": true
-      }
+      "sorting": {"enabled": true}
     });
   }
   function draw_fwd_hosts() {
@@ -450,33 +243,16 @@ jQuery(function($){
           console.log('Cannot draw forwarding hosts table');
         },
         success: function (data) {
-          $.each(data, function (i, item) {
-            item.action = '<div class="btn-group">' +
-              '<a href="#" id="delete_selected" data-id="single-fwdhost" data-api-url="delete/fwdhost" data-item="' + encodeURI(item.host) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
-              '</div>';
-            if (item.keep_spam == "yes") {
-              item.keep_spam = lang.no;
-            }
-            else {
-              item.keep_spam = lang.yes;
-            }
-            item.chkbox = '<input type="checkbox" data-id="fwdhosts" name="multi_select" value="' + item.host + '" />';
-          });
+          return process_table_data(data, 'forwardinghoststable');
         }
       }),
       "empty": lang.empty,
-      "paging": {
-        "enabled": true,
-        "limit": 5,
-        "size": log_pagination_size
-      },
-      "sorting": {
-        "enabled": true
-      }
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "sorting": {"enabled": true}
     });
   }
   function draw_relayhosts() {
-    ft_forwardinghoststable = FooTable.init('#relayhoststable', {
+    ft_relayhoststable = FooTable.init('#relayhoststable', {
       "columns": [
         {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
         {"name":"id","type":"text","title":"ID","style":{"width":"50px"}},
@@ -494,115 +270,30 @@ jQuery(function($){
           console.log('Cannot draw forwarding hosts table');
         },
         success: function (data) {
-          $.each(data, function (i, item) {
-            item.action = '<div class="btn-group">' +
-              '<a href="#" data-toggle="modal" id="miau" data-target="#testRelayhostModal" data-relayhost-id="' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-stats"></span> Test</a>' +
-              '<a href="/edit.php?relayhost=' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
-              '<a href="#" id="delete_selected" data-id="single-rlshost" data-api-url="delete/relayhost" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
-              '</div>';
-            item.chkbox = '<input type="checkbox" data-id="rlyhosts" name="multi_select" value="' + item.id + '" />';
-          });
+          return process_table_data(data, 'relayhoststable');
         }
       }),
       "empty": lang.empty,
-      "paging": {
-        "enabled": true,
-        "limit": 5,
-        "size": log_pagination_size
-      },
-      "sorting": {
-        "enabled": true
-      }
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "sorting": {"enabled": true}
     });
   }
   function draw_rspamd_history() {
-    ft_postfix_logs = FooTable.init('#rspamd_history', {
-      "columns": [{
-          "name":"unix_time",
-          "formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},
-          "title":lang.time,
-          "style":{
-            "width":"170px"
-          }
-        }, {
-          "name": "ip",
-          "title": "IP address",
-          "breakpoints": "all",
-          "style": {
-            "minWidth": 88
-          }
-        }, {
-          "name": "sender_mime",
-          "title": "From",
-          "breakpoints": "xs sm md",
-          "style": {
-            "minWidth": 100
-          }
-        }, {
-          "name": "rcpt_mime",
-          "title": "To",
-          "breakpoints": "xs sm md",
-          "style": {
-            "minWidth": 100
-          }
-        }, {
-          "name": "subject",
-          "title": "Subject",
-          "breakpoints": "all",
-          "style": {
-            "word-break": "break-all",
-            "minWidth": 150
-          }
-        }, {
-          "name": "action",
-          "title": "Action",
-          "style": {
-            "minwidth": 82
-          }
-        }, {
-          "name": "score",
-          "title": "Score",
-          "style": {
-            "maxWidth": 110
-          },
-        }, {
-          "name": "symbols",
-          "title": "Symbols",
-          "breakpoints": "all",
-        }, {
-          "name": "size",
-          "title": "Msg size",
-          "breakpoints": "all",
-          "style": {
-            "minwidth": 50,
-          },
-          "formatter": function(value) { return humanFileSize(value); }
-        }, {
-          "name": "scan_time",
-          "title": "Scan time",
-          "breakpoints": "all",
-          "style": {
-            "maxWidth": 72
-          },
-        }, {
-        "name": "message-id",
-        "title": "ID",
-        "breakpoints": "all",
-        "style": {
-          "minWidth": 130,
-          "overflow": "hidden",
-          "textOverflow": "ellipsis",
-          "wordBreak": "break-all",
-          "whiteSpace": "normal"
-        }
-        }, {
-          "name": "user",
-          "title": "Authenticated user",
-          "breakpoints": "xs sm md",
-          "style": {
-            "minWidth": 100
-          }
-        }],
+    ft_rspamd_history = FooTable.init('#rspamd_history', {
+      "columns": [
+        {"name":"unix_time","formatter":function unix_time_format(tm) { var date = new Date(tm ? tm * 1000 : 0); return date.toLocaleString();},"title":lang.time,"style":{"width":"170px"}},
+        {"name": "ip","title": "IP address","breakpoints": "all","style": {"minWidth": 88}},
+        {"name": "sender_mime","title": "From","breakpoints": "xs sm md","style": {"minWidth": 100}},
+        {"name": "rcpt_mime","title": "To","breakpoints": "xs sm md","style": {"minWidth": 100}},
+        {"name": "subject","title": "Subject","breakpoints": "all","style": {"word-break": "break-all","minWidth": 150}},
+        {"name": "action","title": "Action","style": {"minwidth": 82}},
+        {"name": "score","title": "Score","style": {"maxWidth": 110},},
+        {"name": "symbols","title": "Symbols","breakpoints": "all",},
+        {"name": "size","title": "Msg size","breakpoints": "all","style": {"minwidth": 50},"formatter": function(value){return humanFileSize(value);}},
+        {"name": "scan_time","title": "Scan time","breakpoints": "all","style": {"maxWidth": 72},},
+        {"name": "message-id","title": "ID","breakpoints": "all","style": {"minWidth": 130,"overflow": "hidden","textOverflow": "ellipsis","wordBreak": "break-all","whiteSpace": "normal"}},
+        {"name": "user","title": "Authenticated user","breakpoints": "xs sm md","style": {"minWidth": 100}}
+      ],
       "rows": $.ajax({
         dataType: 'json',
         url: '/api/v1/get/logs/rspamd-history',
@@ -611,81 +302,146 @@ jQuery(function($){
           console.log('Cannot draw rspamd history table');
         },
         success: function (data) {
-          $.each(data, function (i, item) {
-            item.rcpt_mime = item.rcpt_mime.join(",&#8203;");
-            Object.keys(item.symbols).map(function(key) {
-              var sym = item.symbols[key];
-              if (sym.score <= 0) {
-                sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
-              }
-              else {
-                sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
-              }
-              var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
-              if (sym.options) {
-                str += ' [' + sym.options.join(",") + "]";
-              }
-              item.symbols[key].str = str;
-            });
-            item.symbols = Object.keys(item.symbols).
-            map(function(key) {
-              return item.symbols[key];
-            }).sort(function(e1, e2) {
-              return Math.abs(e1.score) < Math.abs(e2.score);
-            }).map(function(e) {
-              return e.str;
-            }).join("<br>\n");
-            var scan_time = item.time_real.toFixed(3) + ' / ' + item.time_virtual.toFixed(3);
-            item.scan_time = {
-              "options": {
-                "sortValue": item.time_real
-              },
-              "value": scan_time
-            };
-            if (item.action === 'clean' || item.action === 'no action') {
-              item.action = "<div class='label label-success'>" + item.action + "</div>";
-            } else if (item.action === 'rewrite subject' || item.action === 'add header' || item.action === 'probable spam') {
-              item.action = "<div class='label label-warning'>" + item.action + "</div>";
-            } else if (item.action === 'spam' || item.action === 'reject') {
-              item.action = "<div class='label label-danger'>" + item.action + "</div>";
-            } else {
-              item.action = "<div class='label label-info'>" + item.action + "</div>";
-            }
-            var score_content;
-            if (item.score < item.required_score) {
-              score_content = "[ <span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
-            } else {
-              score_content = "[ <span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
-            }
-            item.score = {
-              "options": {
-                "sortValue": item.score
-              },
-              "value": score_content
-            };
-            if (item.user == null) {
-              item.user = "none";
-            }
-          });
+          return process_table_data(data, 'rspamd_history');
         }
       }),
       "empty": lang.empty,
-      "paging": {
-        "enabled": true,
-        "limit": 5,
-        "size": log_pagination_size
-      },
-      "filtering": {
-        "enabled": true,
-        "position": "left",
-        "connectors": false,
-        "placeholder": lang.filter_table
-      },
-      "sorting": {
-        "enabled": true
+      "paging": {"enabled": true,"limit": 5,"size": log_pagination_size},
+      "filtering": {"enabled": true,"position": "left","connectors": false,"placeholder": lang.filter_table},
+      "sorting": {"enabled": true},
+      "on": {
+        "ready.ft.table": function(e, ft){
+          heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+          $(heading).children('.log-lines').text(function(){
+            var ft_paging = ft.use(FooTable.Paging)
+            return ft_paging.totalRows;
+          })
+        }
       }
     });
   }
+
+  function process_table_data(data, table) {
+    if (table == 'rspamd_history') {
+    $.each(data, function (i, item) {
+      item.rcpt_mime = item.rcpt_mime.join(",&#8203;");
+      Object.keys(item.symbols).map(function(key) {
+        var sym = item.symbols[key];
+        if (sym.score <= 0) {
+          sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
+        }
+        else {
+          sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
+        }
+        var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
+        if (sym.options) {
+          str += ' [' + sym.options.join(",") + "]";
+        }
+        item.symbols[key].str = str;
+      });
+      item.symbols = Object.keys(item.symbols).
+      map(function(key) {
+        return item.symbols[key];
+      }).sort(function(e1, e2) {
+        return Math.abs(e1.score) < Math.abs(e2.score);
+      }).map(function(e) {
+        return e.str;
+      }).join("<br>\n");
+      var scan_time = item.time_real.toFixed(3) + ' / ' + item.time_virtual.toFixed(3);
+      item.scan_time = {
+        "options": {
+          "sortValue": item.time_real
+        },
+        "value": scan_time
+      };
+      if (item.action === 'clean' || item.action === 'no action') {
+        item.action = "<div class='label label-success'>" + item.action + "</div>";
+      } else if (item.action === 'rewrite subject' || item.action === 'add header' || item.action === 'probable spam') {
+        item.action = "<div class='label label-warning'>" + item.action + "</div>";
+      } else if (item.action === 'spam' || item.action === 'reject') {
+        item.action = "<div class='label label-danger'>" + item.action + "</div>";
+      } else {
+        item.action = "<div class='label label-info'>" + item.action + "</div>";
+      }
+      var score_content;
+      if (item.score < item.required_score) {
+        score_content = "[ <span class='text-success'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
+      } else {
+        score_content = "[ <span class='text-danger'>" + item.score.toFixed(2) + " / " + item.required_score + "</span> ]";
+      }
+      item.score = {
+        "options": {
+          "sortValue": item.score
+        },
+        "value": score_content
+      };
+      if (item.user == null) {
+        item.user = "none";
+      }
+    });
+    } else if (table == 'relayhoststable') {
+      $.each(data, function (i, item) {
+        item.action = '<div class="btn-group">' +
+          '<a href="#" data-toggle="modal" id="miau" data-target="#testRelayhostModal" data-relayhost-id="' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-stats"></span> Test</a>' +
+          '<a href="/edit.php?relayhost=' + encodeURI(item.id) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+          '<a href="#" id="delete_selected" data-id="single-rlshost" data-api-url="delete/relayhost" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+          '</div>';
+        item.chkbox = '<input type="checkbox" data-id="rlyhosts" name="multi_select" value="' + item.id + '" />';
+      });
+    } else if (table == 'forwardinghoststable') {
+      $.each(data, function (i, item) {
+        item.action = '<div class="btn-group">' +
+          '<a href="#" id="delete_selected" data-id="single-fwdhost" data-api-url="delete/fwdhost" data-item="' + encodeURI(item.host) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+          '</div>';
+        if (item.keep_spam == "yes") {
+          item.keep_spam = lang.no;
+        }
+        else {
+          item.keep_spam = lang.yes;
+        }
+        item.chkbox = '<input type="checkbox" data-id="fwdhosts" name="multi_select" value="' + item.host + '" />';
+      });
+    } else if (table == 'domainadminstable') {
+      $.each(data, function (i, item) {
+        item.chkbox = '<input type="checkbox" data-id="domain_admins" name="multi_select" value="' + item.username + '" />';
+        item.action = '<div class="btn-group">' +
+          '<a href="/edit.php?domainadmin=' + encodeURI(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+          '<a href="#" id="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+          '</div>';
+      });
+    } else if (table == 'autodiscover_log') {
+      $.each(data, function (i, item) {
+        item.ua = '<span style="font-size:small">' + item.ua + '</span>';
+        if (item.service == "activesync") {
+          item.service = '<span class="label label-info">ActiveSync</span>';
+        }
+        else if (item.service == "imap") {
+          item.service = '<span class="label label-success">IMAP, SMTP, Cal-/CardDAV</span>';
+        }
+        else {
+          item.service = '<span class="label label-danger">' + item.service + '</span>';
+        }
+      });
+    } else if (table == 'general_syslog') {
+      $.each(data, function (i, item) {
+        item.message = escapeHtml(item.message);
+        var danger_class = ["emerg", "alert", "crit", "err"];
+        var warning_class = ["warning", "warn"];
+        var info_class = ["notice", "info", "debug"];
+        if (jQuery.inArray(item.priority, danger_class) !== -1) {
+          item.priority = '<span class="label label-danger">' + item.priority + '</span>';
+        } 
+        else if (jQuery.inArray(item.priority, warning_class) !== -1) {
+          item.priority = '<span class="label label-warning">' + item.priority + '</span>';
+        }
+        else if (jQuery.inArray(item.priority, info_class) !== -1) {
+          item.priority = '<span class="label label-info">' + item.priority + '</span>';
+        }
+      });
+    }
+    return data
+  };
+  // Initial table drawings
   draw_postfix_logs();
   draw_autodiscover_logs();
   draw_dovecot_logs();
@@ -695,7 +451,7 @@ jQuery(function($){
   draw_fwd_hosts();
   draw_relayhosts();
   draw_rspamd_history();
-
+  // Relayhost
   $('#testRelayhostModal').on('show.bs.modal', function (e) {
     $('#test_relayhost_result').text("-");
     button = $(e.relatedTarget)
@@ -703,16 +459,6 @@ jQuery(function($){
       $('#relayhost_id').val(button.data('relayhost-id'));
     }
   })
-
-  $('#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);
-    }
-  })
-
   $('#test_relayhost').on('click', function (e) {
     e.preventDefault();
     prev = $('#test_relayhost').text();
@@ -730,7 +476,41 @@ jQuery(function($){
         }
     });
   })
+  // 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);
+    }
+  })
 
+  $('.add_log_lines').on('click', function (e) {
+    e.preventDefault();
+    var log_table= $(this).data("table")
+    var new_nrows = ($(this).data("nrows") - 1)
+    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;
+    }
+    if (ft = FooTable.get($('#' + log_table))) {
+      var heading = ft.$el.parents('.tab-pane').find('.panel-heading')
+      var ft_paging = ft.use(FooTable.Paging)
+      var load_rows = ft_paging.totalRows + '-' + (ft_paging.totalRows + new_nrows)
+      $.get('/api/v1/get/logs/' + log_url + '/' + load_rows).then(function(data){
+        if (data.length === undefined) { mailcow_alert_box(lang.no_new_rows, "info"); return; }
+        var rows = process_table_data(data, post_process);
+        var rows_now = (ft_paging.totalRows + data.length);
+        $(heading).children('.log-lines').text(rows_now)
+        mailcow_alert_box(data.length + lang.additional_rows, "success");
+        ft.rows.load(rows, true);
+      });
+    }
+  })
+  // App links
   function add_table_row(table_id) {
     var row = $('<tr />');
     cols = '<td><input class="input-sm form-control" data-id="app_links" type="text" name="app" required></td>';
@@ -739,17 +519,14 @@ jQuery(function($){
     row.append(cols);
     table_id.append(row);
   }
-
   $('#app_link_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'));
   });
 });
-
 $(window).load(function(){
   initial_width = $("#sidebar-admin").width();
   $("#scrollbox").css("width", initial_width);
@@ -761,13 +538,10 @@ $(window).load(function(){
     }
   });
 });
-
 function resizeScrollbox() {
   on_resize_width = $("#sidebar-admin").width();
   $("#scrollbox").removeAttr("style");
   $("#scrollbox").css("width", on_resize_width);
 }
-
 $(window).on('resize', resizeScrollbox);
 $('a[data-toggle="tab"]').on('shown.bs.tab', resizeScrollbox);
-
diff --git a/data/web/js/api.js b/data/web/js/api.js
index 8935f856..23bf229e 100644
--- a/data/web/js/api.js
+++ b/data/web/js/api.js
@@ -61,6 +61,11 @@ $(document).ready(function() {
     var id = $(this).data('id');
     var api_url = $(this).data('api-url');
     var api_attr = $(this).data('api-attr');
+    if (typeof $(this).data('api-reload-window') !== 'undefined') {
+      api_reload_window = $(this).data('api-reload-window');
+    } else {
+      api_reload_window = true;
+    }
     // If clicked element #edit_selected is in a form with the same data-id as the button,
     // we merge all input fields by {"name":"value"} into api-attr
     if ($(this).closest("form").data('id') == id) {
@@ -106,10 +111,10 @@ $(document).ready(function() {
         jsonp: false,
         complete: function(data) {
           var response = (data.responseText);
-          // alert(response);
-          // console.log(reponse.type);
-          // console.log(reponse.msg);
-          window.location = window.location.href.split("#")[0];
+          response_obj = JSON.parse(response);
+          if (api_reload_window === true) {
+            window.location = window.location.href.split("#")[0];
+          }
         }
       });
     }
diff --git a/data/web/js/edit.js b/data/web/js/edit.js
index 057a9ecb..fbd5deca 100644
--- a/data/web/js/edit.js
+++ b/data/web/js/edit.js
@@ -10,6 +10,7 @@ $(document).ready(function() {
       $("#textarea_alias_goto").removeAttr('disabled');
     }
   });
+  $("#script_data").numberedtextarea({allowTabChar: true});
 });
 
 jQuery(function($){
diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js
index c914f644..d55b1913 100644
--- a/data/web/js/mailbox.js
+++ b/data/web/js/mailbox.js
@@ -40,10 +40,61 @@ $(document).ready(function() {
   });
 
   // Log modal
-  $('#logModal').on('show.bs.modal', function(e) {
-    var logText = $(e.relatedTarget).data('log-text');
-    $(e.currentTarget).find('#logText').html('<pre style="background:none;font-size:11px;line-height:1.1;border:0px">' + logText + '</pre>');
+  $('#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);
+      }
+    });
   });
+
+  // Sieve data modal
+  $('#sieveDataModal').on('show.bs.modal', function(e) {
+    var sieveScript = $(e.relatedTarget).data('sieve-script');
+    $(e.currentTarget).find('#sieveDataText').html('<pre style="font-size:14px;line-height:1.1">' + sieveScript + '</pre>');
+  });
+
+  // Set line numbers for textarea
+  $("#script_data").numberedtextarea({allowTabChar: true});
+  // Disable submit button on script change
+	$('#script_data').on('keyup', function() {
+    $('#add_filter_btns > #add_item').attr({"disabled": true});
+    $('#validation_msg').html('-');
+	});
+
+  // Validate script data
+  $("#validate_sieve").click(function( event ) {
+    event.preventDefault();
+    var script = $('#script_data').val();
+    $.ajax({
+      dataType: 'jsonp',
+      url: "/inc/ajax/sieve_validation.php",
+      type: "get",
+      data: { script: script },
+      complete: function(data) {
+        var response = (data.responseText);
+        response_obj = JSON.parse(response);
+        if (response_obj.type == "success") {
+          $('#add_filter_btns > #add_item').attr({"disabled": false});
+        }
+        mailcow_alert_box(response_obj.msg, response_obj.type);
+      },
+    });
+  });
+  // $(document).on('DOMNodeInserted', '#prefilter_table', function () {
+    // $("#active-script").closest('td').css('background-color','#b0f0a0');
+    // $("#inactive-script").closest('td').css('background-color','#b0f0a0');
+  // });
+
+
+
 });
 jQuery(function($){
   // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
@@ -87,7 +138,7 @@ jQuery(function($){
   function draw_domain_table() {
     ft_domain_table = FooTable.init('#domain_table', {
       "columns": [
-        {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+        {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
         {"sorted": true,"name":"domain_name","title":lang.domain,"style":{"width":"250px"}},
         {"name":"aliases","title":lang.aliases,"breakpoints":"xs sm"},
         {"name":"mailboxes","title":lang.mailboxes},
@@ -152,7 +203,7 @@ jQuery(function($){
   function draw_mailbox_table() {
     ft_mailbox_table = FooTable.init('#mailbox_table', {
       "columns": [
-        {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+        {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
         {"sorted": true,"name":"username","style":{"word-break":"break-all","min-width":"120px"},"title":lang.username},
         {"name":"name","title":lang.fname,"style":{"word-break":"break-all","min-width":"120px"},"breakpoints":"xs sm"},
         {"name":"domain","title":lang.domain,"breakpoints":"xs sm"},
@@ -222,7 +273,7 @@ jQuery(function($){
   function draw_resource_table() {
     ft_resource_table = FooTable.init('#resource_table', {
       "columns": [
-        {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+        {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
         {"sorted": true,"name":"description","title":lang.description,"style":{"width":"250px"}},
         {"name":"kind","title":lang.kind},
         {"name":"domain","title":lang.domain,"breakpoints":"xs sm"},
@@ -267,7 +318,7 @@ jQuery(function($){
   function draw_alias_table() {
     ft_alias_table = FooTable.init('#alias_table', {
       "columns": [
-        {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+        {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
         {"sorted": true,"name":"address","title":lang.alias,"style":{"width":"250px"}},
         {"name":"goto","title":lang.target_address},
         {"name":"domain","title":lang.domain,"breakpoints":"xs sm"},
@@ -320,7 +371,7 @@ jQuery(function($){
   function draw_aliasdomain_table() {
     ft_aliasdomain_table = FooTable.init('#aliasdomain_table', {
       "columns": [
-        {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"},
+        {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
         {"sorted": true,"name":"alias_domain","title":lang.alias,"style":{"width":"250px"}},
         {"name":"target_domain","title":lang.target_domain},
         {"name":"active","filterable": false,"style":{"maxWidth":"50px","width":"70px"},"title":lang.active},
@@ -363,7 +414,7 @@ jQuery(function($){
   function draw_sync_job_table() {
     ft_syncjob_table = FooTable.init('#sync_job_table', {
       "columns": [
-        {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"},
+        {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"},
         {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}},
         {"name":"user2","title":lang.owner},
         {"name":"server_w_port","title":"Server","breakpoints":"xs"},
@@ -376,14 +427,14 @@ jQuery(function($){
       "empty": lang.empty,
       "rows": $.ajax({
         dataType: 'json',
-        url: '/api/v1/get/syncjobs/all',
+        url: '/api/v1/get/syncjobs/all/no_log',
         jsonp: false,
         error: function () {
           console.log('Cannot draw sync job table');
         },
         success: function (data) {
           $.each(data, function (i, item) {
-            item.log = '<a href="#logModal" data-toggle="modal" data-log-text="' + escapeHtml(item.returned_text) + '">Open logs</a>'
+            item.log = '<a href="#syncjobLogModal" data-toggle="modal" data-syncjob-id="' + encodeURI(item.id) + '">Open logs</a>'
             item.exclude = '<code>' + item.exclude + '</code>'
             item.server_w_port = item.user1 + '@' + item.host1 + ':' + item.port1;
             item.action = '<div class="btn-group">' +
@@ -399,16 +450,76 @@ jQuery(function($){
         "limit": 5,
         "size": pagination_size
       },
+      "filtering": {
+        "enabled": true,
+        "position": "left",
+        "placeholder": lang.filter_table
+      },
       "sorting": {
         "enabled": true
       }
     });
   }
 
+  function draw_filter_table() {
+    ft_filter_table = FooTable.init('#filter_table', {
+      "columns": [
+        {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"},
+        {"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}},
+        {"name":"active","style":{"maxWidth":"80px","width":"80px"},"title":lang.active},
+        {"name":"filter_type","style":{"maxWidth":"80px","width":"80px"},"title":"Type"},
+        {"sorted": true,"name":"username","title":lang.owner,"style":{"maxWidth":"550px","width":"350px"}},
+        {"name":"script_desc","title":lang.description,"breakpoints":"xs"},
+        {"name":"script_data","title":"Script","breakpoints":"all"},
+        {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
+      ],
+      "empty": lang.empty,
+      "rows": $.ajax({
+        dataType: 'json',
+        url: '/api/v1/get/filters/all',
+        jsonp: false,
+        error: function () {
+          console.log('Cannot draw filter table');
+        },
+        success: function (data) {
+          $.each(data, function (i, item) {
+            if (item.active_int == 1) {
+              item.active = '<span id="active-script" class="label label-success">' + lang.active + '</span>';
+            } else {
+              item.active = '<span id="inactive-script" class="label label-warning">' + lang.inactive + '</span>';
+            }
+            item.script_data = '<pre style="margin:0px">' + escapeHtml(item.script_data) + '</pre>'
+            item.filter_type = '<div class="label label-default">' + item.filter_type.charAt(0).toUpperCase() + item.filter_type.slice(1).toLowerCase() + '</div>'
+            item.action = '<div class="btn-group">' +
+              '<a href="/edit.php?filter=' + item.id + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' +
+              '<a href="#" id="delete_selected" data-id="single-filter" data-api-url="delete/filter" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
+              '</div>';
+            item.chkbox = '<input type="checkbox" data-id="filter_item" name="multi_select" value="' + item.id + '" />'
+          });
+        }
+      }),
+      "paging": {
+        "enabled": true,
+        "limit": 5,
+        "size": pagination_size
+      },
+      "filtering": {
+        "enabled": true,
+        "position": "left",
+        "placeholder": lang.filter_table
+      },
+      "sorting": {
+        "enabled": true
+      }
+    });
+  };
+
   draw_domain_table();
   draw_mailbox_table();
   draw_resource_table();
   draw_alias_table();
   draw_aliasdomain_table();
   draw_sync_job_table();
-});
\ No newline at end of file
+  draw_filter_table();
+
+});
diff --git a/data/web/js/numberedtextarea.min.js b/data/web/js/numberedtextarea.min.js
new file mode 100644
index 00000000..c99329f7
--- /dev/null
+++ b/data/web/js/numberedtextarea.min.js
@@ -0,0 +1 @@
+!function(e){function t(t,n){var a=e('<div class="numberedtextarea-wrapper"></div>').insertAfter(t);e(t).detach().appendTo(a)}function n(t,n){(t=e(t)).parents(".numberedtextarea-wrapper");var i=parseFloat(t.css("padding-left")),o=parseFloat(t.css("padding-top")),s=(parseFloat(t.css("padding-bottom")),e('<div class="numberedtextarea-line-numbers"></div>').insertAfter(t));t.css({paddingLeft:i+s.width()+"px"}).on("input propertychange change keyup paste",function(){a(t,n)}).on("scroll",function(){r(t,n)}),s.css({paddingLeft:i+"px",paddingTop:o+"px",lineHeight:t.css("line-height"),fontFamily:t.css("font-family"),width:s.width()-i+"px"}),t.trigger("change")}function a(t,n){var a=(t=e(t)).parent().find(".numberedtextarea-line-numbers"),r=t.val().split("\n").length,o=parseFloat(t.css("padding-bottom"));for(a.find(".numberedtextarea-number").remove(),i=1;i<=r;i++){var s=e('<div class="numberedtextarea-number numberedtextarea-number-'+i+'">'+i+"</div>").appendTo(a);i===r&&s.css("margin-bottom",o+"px")}}function r(t,n){(t=e(t)).parent().find(".numberedtextarea-line-numbers").scrollTop(t.scrollTop())}function o(e,t){if(e.focus(),"number"==typeof e.selectionStart){var n=e.value,a=e.selectionStart;e.value=n.slice(0,a)+t+n.slice(e.selectionEnd),e.selectionEnd=e.selectionStart=a+t.length}else if(void 0!==document.selection){var i=document.selection.createRange();i.text=t,i.collapse(!1),i.select()}}function s(t){e(t).keydown(function(e){if(9==e.which)return o(this,"\t"),!1}),e(t).keypress(function(e){if(9==e.which)return!1})}e.fn.numberedtextarea=function(a){var i=e.extend({color:null,borderColor:null,class:null,allowTabChar:!1},a);return this.each(function(){if("textarea"!==this.nodeName.toLowerCase())return console.log("This is not a <textarea>, so no way Jose..."),!1;t(this,i),n(this,i),i.allowTabChar&&e(this).allowTabChar()}),this},e.fn.allowTabChar=function(){return this.jquery&&this.each(function(){if(1==this.nodeType){var e=this.nodeName.toLowerCase();("textarea"==e||"input"==e&&"text"==this.type)&&s(this)}}),this}}(jQuery);
diff --git a/data/web/js/user.js b/data/web/js/user.js
index c091ac85..239507f4 100644
--- a/data/web/js/user.js
+++ b/data/web/js/user.js
@@ -1,13 +1,19 @@
 $(document).ready(function() {
-
-  // Log modal
-  $('#logModal').on('show.bs.modal', function(e) {
-    var logText = $(e.relatedTarget).data('log-text');
-    $(e.currentTarget).find('#logText').html('<pre style="background:none;font-size:11px;line-height:1.1;border:0px">' + logText + '</pre>');
+  $('#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);
+      }
+    });
   });
-
 });
-
 jQuery(function($){
   // http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
   var entityMap = {
@@ -95,14 +101,14 @@ jQuery(function($){
       "empty": lang.empty,
       "rows": $.ajax({
         dataType: 'json',
-        url: '/api/v1/get/syncjobs/' + mailcow_cc_username,
+        url: '/api/v1/get/syncjobs/' + mailcow_cc_username + '/no_log',
         jsonp: false,
         error: function () {
           console.log('Cannot draw sync job table');
         },
         success: function (data) {
           $.each(data, function (i, item) {
-            item.log = '<a href="#logModal" data-toggle="modal" data-log-text="' + escapeHtml(item.returned_text) + '">Open logs</a>'
+            item.log = '<a href="#syncjobLogModal" data-toggle="modal" data-syncjob-id="' + encodeURI(item.id) + '">Open logs</a>'
             item.exclude = '<code>' + item.exclude + '</code>'
             item.server_w_port = item.user1 + '@' + item.host1 + ':' + item.port1;
             if (acl_data.syncjobs === 1) {
@@ -213,4 +219,27 @@ jQuery(function($){
   draw_tla_table();
   draw_wl_policy_mailbox_table();
   draw_bl_policy_mailbox_table();
+
+  // 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/' + 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);
+  });
 });
\ No newline at end of file
diff --git a/data/web/json_api.php b/data/web/json_api.php
index 0e4f9e87..0bc5e3a6 100644
--- a/data/web/json_api.php
+++ b/data/web/json_api.php
@@ -225,10 +225,10 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               ));
             }
           break;
-          case "syncjob":
+          case "filter":
             if (isset($_POST['attr'])) {
               $attr = (array)json_decode($_POST['attr'], true);
-              if (mailbox('add', 'syncjob', $attr) === false) {
+              if (mailbox('add', 'filter', $attr) === false) {
                 if (isset($_SESSION['return'])) {
                   echo json_encode($_SESSION['return']);
                 }
@@ -725,12 +725,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
           case "logs":
             switch ($object) {
               case "dovecot":
-                if (isset($extra) && !empty($extra)) {
-                  $extra = intval($extra);
+                // 0 is first record, so empty is fine
+                if (isset($extra)) {
+                  $extra = preg_replace('/[^\d\-]/i', '', $extra);
                   $logs = get_logs('dovecot-mailcow', $extra);
                 }
                 else {
-                  $logs = get_logs('dovecot-mailcow', -1);
+                  $logs = get_logs('dovecot-mailcow');
                 }
                 if (isset($logs) && !empty($logs)) {
                   echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
@@ -740,12 +741,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
                 }
               break;
               case "fail2ban":
-                if (isset($extra) && !empty($extra)) {
-                  $extra = intval($extra);
+                // 0 is first record, so empty is fine
+                if (isset($extra)) {
+                  $extra = preg_replace('/[^\d\-]/i', '', $extra);
                   $logs = get_logs('fail2ban-mailcow', $extra);
                 }
                 else {
-                  $logs = get_logs('fail2ban-mailcow', -1);
+                  $logs = get_logs('fail2ban-mailcow');
                 }
                 if (isset($logs) && !empty($logs)) {
                   echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
@@ -755,12 +757,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
                 }
               break;
               case "postfix":
-                if (isset($extra) && !empty($extra)) {
-                  $extra = intval($extra);
+                // 0 is first record, so empty is fine
+                if (isset($extra)) {
+                  $extra = preg_replace('/[^\d\-]/i', '', $extra);
                   $logs = get_logs('postfix-mailcow', $extra);
                 }
                 else {
-                  $logs = get_logs('postfix-mailcow', -1);
+                  $logs = get_logs('postfix-mailcow');
                 }
                 if (isset($logs) && !empty($logs)) {
                   echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
@@ -770,12 +773,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
                 }
               break;
               case "autodiscover":
-                if (isset($extra) && !empty($extra)) {
-                  $extra = intval($extra);
+                // 0 is first record, so empty is fine
+                if (isset($extra)) {
+                  $extra = preg_replace('/[^\d\-]/i', '', $extra);
                   $logs = get_logs('autodiscover-mailcow', $extra);
                 }
                 else {
-                  $logs = get_logs('autodiscover-mailcow', -1);
+                  $logs = get_logs('autodiscover-mailcow');
                 }
                 if (isset($logs) && !empty($logs)) {
                   echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
@@ -785,12 +789,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
                 }
               break;
               case "sogo":
-                if (isset($extra) && !empty($extra)) {
-                  $extra = intval($extra);
+                // 0 is first record, so empty is fine
+                if (isset($extra)) {
+                  $extra = preg_replace('/[^\d\-]/i', '', $extra);
                   $logs = get_logs('sogo-mailcow', $extra);
                 }
                 else {
-                  $logs = get_logs('sogo-mailcow', -1);
+                  $logs = get_logs('sogo-mailcow');
                 }
                 if (isset($logs) && !empty($logs)) {
                   echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
@@ -800,7 +805,14 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
                 }
               break;
               case "rspamd-history":
-                $logs = get_logs('rspamd-history');
+                // 0 is first record, so empty is fine
+                if (isset($extra)) {
+                  $extra = preg_replace('/[^\d\-]/i', '', $extra);
+                  $logs = get_logs('rspamd-history', $extra);
+                }
+                else {
+                  $logs = get_logs('rspamd-history');
+                }
                 if (isset($logs) && !empty($logs)) {
                   echo json_encode($logs, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
                 }
@@ -863,7 +875,13 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
                         $syncjobs = mailbox('get', 'syncjobs', $mailbox);
                         if (!empty($syncjobs)) {
                           foreach ($syncjobs as $syncjob) {
-                            if ($details = mailbox('get', 'syncjob_details', $syncjob)) {
+                            if (isset($extra)) {
+                              $details = mailbox('get', 'syncjob_details', $syncjob, explode(',', $extra));
+                            }
+                            else {
+                              $details = mailbox('get', 'syncjob_details', $syncjob);
+                            }
+                            if ($details) {
                               $data[] = $details;
                             }
                             else {
@@ -890,7 +908,83 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
                 $syncjobs = mailbox('get', 'syncjobs', $object);
                 if (!empty($syncjobs)) {
                   foreach ($syncjobs as $syncjob) {
-                    if ($details = mailbox('get', 'syncjob_details', $syncjob)) {
+                    if (isset($extra)) {
+                      $details = mailbox('get', 'syncjob_details', $syncjob, explode(',', $extra));
+                    }
+                    else {
+                      $details = mailbox('get', 'syncjob_details', $syncjob);
+                    }
+                    if ($details) {
+                      $data[] = $details;
+                    }
+                    else {
+                      continue;
+                    }
+                  }
+                }
+                if (!isset($data) || empty($data)) {
+                  echo '{}';
+                }
+                else {
+                  echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+                }
+              break;
+            }
+          break;
+          case "active-user-sieve":
+            if (isset($object)) {
+              $sieve_filter = mailbox('get', 'active_user_sieve', $object);
+              if (!empty($sieve_filter)) {
+                $data[] = $sieve_filter;
+              }
+            }
+            if (!isset($data) || empty($data)) {
+              echo '{}';
+            }
+            else {
+              echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+            }
+          break;
+          case "filters":
+            switch ($object) {
+              case "all":
+                $domains = mailbox('get', 'domains');
+                if (!empty($domains)) {
+                  foreach ($domains as $domain) {
+                    $mailboxes = mailbox('get', 'mailboxes', $domain);
+                    if (!empty($mailboxes)) {
+                      foreach ($mailboxes as $mailbox) {
+                        $filters = mailbox('get', 'filters', $mailbox);
+                        if (!empty($filters)) {
+                          foreach ($filters as $filter) {
+                            if ($details = mailbox('get', 'filter_details', $filter)) {
+                              $data[] = $details;
+                            }
+                            else {
+                              continue;
+                            }
+                          }
+                        }
+                      }
+                    }
+                  }
+                  if (!isset($data) || empty($data)) {
+                    echo '{}';
+                  }
+                  else {
+                    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+                  }
+                }
+                else {
+                  echo '{}';
+                }
+              break;
+
+              default:
+                $filters = mailbox('get', 'filters', $object);
+                if (!empty($filters)) {
+                  foreach ($filters as $filter) {
+                    if ($details = mailbox('get', 'filter_details', $filter)) {
                       $data[] = $details;
                     }
                     else {
@@ -1296,6 +1390,47 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               ));
             }
           break;
+          case "filter":
+            if (isset($_POST['items'])) {
+              $items = (array)json_decode($_POST['items'], true);
+              if (is_array($items)) {
+                if (mailbox('delete', 'filter', array('id' => $items)) === false) {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'error',
+                      'msg' => 'Deletion of items/s failed'
+                    ));
+                  }
+                }
+                else {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'success',
+                      'msg' => 'Task completed'
+                    ));
+                  }
+                }
+              }
+              else {
+                echo json_encode(array(
+                  'type' => 'error',
+                  'msg' => 'Cannot find id array in post data'
+                ));
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Cannot find items in post data'
+              ));
+            }
+          break;
           case "fwdhost":
             if (isset($_POST['items'])) {
               $items = (array)json_decode($_POST['items'], true);
@@ -2102,6 +2237,50 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               ));
             }
           break;
+          case "filter":
+            if (isset($_POST['items']) && isset($_POST['attr'])) {
+              $items = (array)json_decode($_POST['items'], true);
+              $attr = (array)json_decode($_POST['attr'], true);
+              $postarray = array_merge(array('id' => $items), $attr);
+              if (is_array($postarray['id'])) {
+                if (mailbox('edit', 'filter', $postarray) === false) {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'error',
+                      'msg' => 'Edit failed'
+                    ));
+                  }
+                  exit();
+                }
+                else {
+                  if (isset($_SESSION['return'])) {
+                    echo json_encode($_SESSION['return']);
+                  }
+                  else {
+                    echo json_encode(array(
+                      'type' => 'success',
+                      'msg' => 'Task completed'
+                    ));
+                  }
+                }
+              }
+              else {
+                echo json_encode(array(
+                  'type' => 'error',
+                  'msg' => 'Incomplete post data'
+                ));
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Incomplete post data'
+              ));
+            }
+          break;          
           case "resource":
             if (isset($_POST['items']) && isset($_POST['attr'])) {
               $items = (array)json_decode($_POST['items'], true);
diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php
index 681dca4e..bd329fa1 100644
--- a/data/web/lang/lang.de.php
+++ b/data/web/lang/lang.de.php
@@ -99,6 +99,9 @@ $lang['danger']['domain_not_empty'] = 'Kann nur leere Domains entfernen';
 $lang['warning']['spam_alias_temp_error'] = 'Kann zur Zeit keinen Spam-Alias erstellen, bitte versuchen Sie es später noch einmal.';
 $lang['danger']['spam_alias_max_exceeded'] = 'Maximale Anzahl an Spam-Alias-Adressen erreicht';
 $lang['danger']['validity_missing'] = 'Bitte geben Sie eine Gültigkeitsdauer an';
+$lang['user']['loading'] = "Lade...";
+$lang['user']['active_sieve'] = "Aktiver Filter";
+$lang['user']['no_active_filter'] = "Kein aktiver Filter vorhanden";
 $lang['user']['on'] = 'Ein';
 $lang['user']['off'] = 'Aus';
 $lang['user']['messages'] = "Nachrichten";
@@ -287,7 +290,7 @@ $lang['edit']['syncjob'] = 'Sync-Job bearbeiten';
 $lang['edit']['save'] = 'Änderungen speichern';
 $lang['edit']['username'] = 'Benutzername';
 $lang['edit']['hostname'] = 'Servername';
-$lang['edit']['encryption'] = 'Verschlüsselungsmethode';
+$lang['edit']['encryption'] = 'Verschlüsselung';
 $lang['edit']['maxage'] = 'Maximales Alter in Tagen einer Nachricht, die kopiert werden soll</br ><small>(0 = alle Nachrichten kopieren)</small>';
 $lang['edit']['subfolder2'] = 'Ziel-Ordner<br><small>(leer = kein Unterordner)</small>';
 $lang['edit']['mins_interval'] = 'Intervall (min)';
@@ -339,7 +342,7 @@ $lang['add']['syncjob_hint'] = 'Passwörter werden unverschlüsselt abgelegt!';
 $lang['add']['hostname'] = 'Servername';
 $lang['add']['port'] = 'Port';
 $lang['add']['username'] = 'Benutzername';
-$lang['add']['enc_method'] = 'Verschlüsselungsmethode';
+$lang['add']['enc_method'] = 'Verschlüsselung';
 $lang['add']['maxage'] = 'Maximales Alter von Nachrichten, welche vom Remote abgefragt werden (0 = Alter ignorieren)';
 $lang['add']['subfolder2'] = 'Synchronisation in Unterordner am Ziel';
 $lang['add']['mins_interval'] = 'Abrufintervall (Minuten)';
@@ -388,6 +391,21 @@ $lang['add']['password_repeat'] = 'Passwort (Wiederholung)';
 $lang['add']['previous'] = 'Vorherige Seite';
 $lang['add']['restart_sogo_hint'] = 'Der SOGo Container muss nach dem Hinzufügen einer neuen Domain neugestartet werden!';
 $lang['add']['goto_null'] = 'Nachrichten sofort verwerfen';
+$lang['add']['validation_success'] = 'Erfolgreich validiert';
+$lang['add']['activate_filter_warn'] = 'Alle anderen Filter diesen Typs werden deaktiviert, falls dieses Script aktiv markiert wird.';
+$lang['add']['validate'] = 'Validieren';
+$lang['mailbox']['add_filter'] = 'Filter erstellen';
+$lang['add']['sieve_desc'] = 'Kurze Beschreibung';
+$lang['edit']['sieve_desc'] = 'Kurze Beschreibung';
+$lang['add']['sieve_type'] = 'Filtertyp';
+$lang['edit']['sieve_type'] = 'Filtertyp';
+$lang['mailbox']['set_prefilter'] = 'Als Prefilter markieren';
+$lang['mailbox']['set_postfilter'] = 'Als Postfilter markieren';
+$lang['mailbox']['filters'] = 'Filter';
+$lang['mailbox']['sync_jobs'] = 'Synchronisationen';
+$lang['mailbox']['inactive'] = 'Inaktiv';
+$lang['edit']['validate_save'] = 'Validieren und speichern';
+
 
 $lang['login']['title'] = 'Anmeldung';
 $lang['login']['administration'] = 'Administration';
@@ -425,6 +443,8 @@ $lang['tfa']['scan_qr_code'] = "Bitte scannen Sie jetzt den angezeigten QR-Code:
 $lang['tfa']['enter_qr_code'] = "Falls Sie den angezeigten QR-Code nicht scannen können, verwenden Sie bitte nachstehenden Sicherheitsschlüssel";
 $lang['tfa']['confirm_totp_token'] = "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens";
 
+$lang['admin']['no_new_rows'] = 'Keine weiteren Zeilen vorhanden';
+$lang['admin']['additional_rows'] = ' zusätzliche Zeilen geladen'; // parses to 'n additional rows were added'
 $lang['admin']['private_key'] = 'Private Key';
 $lang['admin']['import'] = 'Importieren';
 $lang['admin']['import_private_key'] = 'Private Key importieren';
diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php
index c29d8e17..efa19dd2 100644
--- a/data/web/lang/lang.en.php
+++ b/data/web/lang/lang.en.php
@@ -101,6 +101,9 @@ $lang['danger']['domain_not_empty'] = "Cannot remove non-empty domain";
 $lang['warning']['spam_alias_temp_error'] = "Temporary error: Cannot add spam alias, please try again later.";
 $lang['danger']['spam_alias_max_exceeded'] = "Max. allowed spam alias addresses exceeded";
 $lang['danger']['validity_missing'] = 'Please assign a period of validity';
+$lang['user']['loading'] = "Loading...";
+$lang['user']['active_sieve'] = "Active filter";
+$lang['user']['no_active_filter'] = "No active filter available";
 $lang['user']['on'] = "On";
 $lang['user']['off'] = "Off";
 $lang['user']['messages'] = "messages"; // "123 messages"
@@ -393,6 +396,21 @@ $lang['add']['password_repeat'] = 'Confirmation password (repeat)';
 $lang['add']['previous'] = 'Previous page';
 $lang['add']['restart_sogo_hint'] = 'You will need to restart the SOGo service container after adding a new domain!';
 $lang['add']['goto_null'] = 'Silently discard mail';
+$lang['add']['validation_success'] = 'Validated successfully';
+$lang['add']['activate_filter_warn'] = 'All other filters will be deactivated, when active is checked.';
+$lang['add']['validate'] = 'Validate';
+$lang['mailbox']['add_filter'] = 'Add filter';
+$lang['add']['sieve_desc'] = 'Short description';
+$lang['edit']['sieve_desc'] = 'Short description';
+$lang['add']['sieve_type'] = 'Filter type';
+$lang['edit']['sieve_type'] = 'Filter type';
+$lang['mailbox']['set_prefilter'] = 'Mark as prefilter';
+$lang['mailbox']['set_postfilter'] = 'Mark as postfilter';
+$lang['mailbox']['filters'] = 'Filters';
+$lang['mailbox']['sync_jobs'] = 'Sync jobs';
+$lang['mailbox']['inactive'] = 'Inactive';
+$lang['edit']['validate_save'] = 'Validate and save';
+
 
 $lang['login']['title'] = 'Login';
 $lang['login']['administration'] = 'Administration';
@@ -430,6 +448,8 @@ $lang['tfa']['scan_qr_code'] = "Please scan the following code with your authent
 $lang['tfa']['enter_qr_code'] = "Your TOTP code if your device cannot scan QR codes";
 $lang['tfa']['confirm_totp_token'] = "Please confirm your changes by entering the generated token";
 
+$lang['admin']['no_new_rows'] = 'No further rows available';
+$lang['admin']['additional_rows'] = ' additional rows were added'; // parses to 'n additional rows were added'
 $lang['admin']['private_key'] = 'Private key';
 $lang['admin']['import'] = 'Import';
 $lang['admin']['import_private_key'] = 'Import private key';
diff --git a/data/web/mailbox.php b/data/web/mailbox.php
index 0325e7f7..a2d72f22 100644
--- a/data/web/mailbox.php
+++ b/data/web/mailbox.php
@@ -19,7 +19,8 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
         <li role="presentation"><a href="#tab-domain-aliases" aria-controls="tab-domain-aliases" role="tab" data-toggle="tab"><?=$lang['mailbox']['domain_aliases'];?></a></li>
       </ul>
     </li>
-    <li role="presentation"><a href="#tab-syncjobs" aria-controls="tab-resources" role="tab" data-toggle="tab">Sync jobs</a></li>
+    <li role="presentation"><a href="#tab-syncjobs" aria-controls="tab-syncjobs" role="tab" data-toggle="tab"><?=$lang['mailbox']['sync_jobs'];?></a></li>
+    <li role="presentation"><a href="#tab-filters" aria-controls="tab-filters" role="tab" data-toggle="tab"><?=$lang['mailbox']['filters'];?></a></li>
   </ul>
 
 	<div class="row">
@@ -151,7 +152,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
         <div role="tabpanel" class="tab-pane" id="tab-syncjobs">
           <div class="panel panel-default">
             <div class="panel-heading">
-              <h3 class="panel-title">Sync jobs</h3>
+              <h3 class="panel-title"><?=$lang['mailbox']['sync_jobs'];?></h3>
             </div>
             <div class="table-responsive">
               <table class="table table-striped" id="sync_job_table"></table>
@@ -169,6 +170,37 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
                 <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addSyncJobModalAdmin"><span class="glyphicon glyphicon-plus"></span> <?=$lang['user']['create_syncjob'];?></a>
               </div>
             </div>
+          </div>
+        </div>
+
+        <div role="tabpanel" class="tab-pane" id="tab-filters">
+          <div class="panel panel-default">
+            <div class="panel-heading">
+              <h3 class="panel-title"><?=$lang['mailbox']['filters'];?></h3>
+            </div>
+            <p style="margin:10px" class="help-block">You can store multiple filters per user, but only one prefilter and one postfilter can be active at the same time.</p>
+            <p style="margin:10px" class="help-block">Each filter will be processed in the described order. A failed script will not stop processing of further scripts.</a></p>
+            <p style="margin:10px" class="help-block">Prefilter → User scripts → Postfilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/sieve_after" target="_blank">global sieve postfilter</a></p>
+            <div class="table-responsive">
+              <table class="table table-striped" id="filter_table"></table>
+            </div>
+            <div class="mass-actions-mailbox">
+              <div class="btn-group">
+                <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="filter_item" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
+                <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
+                <ul class="dropdown-menu">
+                  <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                  <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                  <li role="separator" class="divider"></li>
+                  <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"prefilter"}' href="#"><?=$lang['mailbox']['set_prefilter'];?></a></li>
+                  <li><a id="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"postfilter"}' href="#"><?=$lang['mailbox']['set_postfilter'];?></a></li>
+                  <li role="separator" class="divider"></li>
+                  <li><a id="delete_selected" data-text="<?=$lang['user']['eas_reset'];?>?" data-id="filter_item" data-api-url='delete/filter' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                </ul>
+                <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addFilterModalAdmin"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_filter'];?></a>
+              </div>
+            </div>
+          </div>
         </div>
 
       </div> <!-- /tab-content -->
diff --git a/data/web/modals/mailbox.php b/data/web/modals/mailbox.php
index 3ce30a3f..61dd747d 100644
--- a/data/web/modals/mailbox.php
+++ b/data/web/modals/mailbox.php
@@ -314,7 +314,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
         <h3 class="modal-title"><?=$lang['add']['syncjob'];?></h3>
       </div>
       <div class="modal-body">
-        <p><?=$lang['add']['syncjob_hint'];?></p>
+        <p class="help-block"><?=$lang['add']['syncjob_hint'];?></p>
 				<form class="form-horizontal" role="form" data-id="add_syncjob">
           <div class="form-group">
             <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?>:</label>
@@ -437,12 +437,81 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
     </div>
   </div>
 </div><!-- add sync job modal -->
-<!-- log modal -->
-<div class="modal fade" id="logModal" tabindex="-1" role="dialog" aria-labelledby="logTextLabel">
-  <div class="modal-dialog" style="width:90%" role="document">
+<!-- add add_filter modal -->
+<div class="modal fade" id="addFilterModalAdmin" tabindex="-1" role="dialog" aria-hidden="true">
+  <div class="modal-dialog modal-lg">
     <div class="modal-content">
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
+        <h3 class="modal-title">Filter</h3>
+      </div>
       <div class="modal-body">
-        <span id="logText"></span>
+				<form class="form-horizontal" role="form" data-id="add_filter">
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="username"><?=$lang['add']['username'];?>:</label>
+            <div class="col-sm-10">
+              <select id="addSelectUsername" name="username" id="username" required>
+              <?php
+              $domains = mailbox('get', 'domains');
+              if (!empty($domains)) {
+                foreach ($domains as $domain) {
+                  $mailboxes = mailbox('get', 'mailboxes', $domain);
+                  foreach ($mailboxes as $mailbox) {
+                    echo "<option>".htmlspecialchars($mailbox)."</option>";
+                  }
+                }
+              }
+              ?>
+              </select>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-2" for="filter_type"><?=$lang['add']['sieve_type'];?>:</label>
+            <div class="col-sm-10">
+              <select id="addFilterType" name="filter_type" id="filter_type" required>
+                <option value="prefilter">Prefilter</option>
+                <option value="postfilter">Postfilter</option>
+              </select>
+            </div>
+          </div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="script_desc"><?=$lang['add']['sieve_desc'];?>:</label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="script_desc" id="script_desc" required maxlength="255">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="script_data">Script:</label>
+						<div class="col-sm-10">
+							<textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control" rows="20" id="script_data" name="script_data" required></textarea>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+              <p class="help-block"><?=$lang['add']['activate_filter_warn'];?></p>
+							<div class="checkbox">
+							<label><input type="checkbox" value="1" name="active" checked> <?=$lang['add']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10" id="add_filter_btns">
+              <button class="btn btn-default" id="validate_sieve" href="#"><?=$lang['add']['validate'];?></button>
+              <button class="btn btn-success" id="add_item" data-id="add_filter" data-api-url='add/filter' data-api-attr='{}' href="#" disabled><?=$lang['admin']['add'];?></button>
+						</div>
+					</div>
+				</form>
+      </div>
+    </div>
+  </div>
+</div><!-- add add_filter modal -->
+<!-- log modal -->
+<div class="modal fade" id="syncjobLogModal" tabindex="-1" role="dialog" aria-labelledby="syncjobLogModalLabel">
+  <div class="modal-dialog modal-lg" role="document">
+    <div class="modal-content">
+      <div class="modal-header"><h4 class="modal-title">Log</h4></div>
+      <div class="modal-body">
+        <textarea class="form-control" rows="20" id="logText"></textarea>
       </div>
     </div>
   </div>
diff --git a/data/web/modals/user.php b/data/web/modals/user.php
index e237a74b..9b4394f3 100644
--- a/data/web/modals/user.php
+++ b/data/web/modals/user.php
@@ -112,11 +112,12 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
   </div>
 </div><!-- add sync job modal -->
 <!-- log modal -->
-<div class="modal fade" id="logModal" tabindex="-1" role="dialog" aria-labelledby="logTextLabel">
-  <div class="modal-dialog" style="width:90%" role="document">
+<div class="modal fade" id="syncjobLogModal" tabindex="-1" role="dialog" aria-labelledby="syncjobLogModalLabel">
+  <div class="modal-dialog modal-lg" role="document">
     <div class="modal-content">
+      <div class="modal-header"><h4 class="modal-title">Log</h4></div>
       <div class="modal-body">
-        <span id="logText"></span>
+        <textarea class="form-control" rows="20" id="logText"></textarea>
       </div>
     </div>
   </div>
@@ -156,4 +157,18 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
       </div>
     </div>
   </div>
-</div><!-- pw change modal -->
\ No newline at end of file
+</div><!-- pw change modal -->
+<!-- sieve filter modal -->
+<div class="modal fade" id="userFilterModal" tabindex="-1" role="dialog" aria-labelledby="pwChangeModalLabel">
+  <div class="modal-dialog" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
+        <h3 class="modal-title"><?=$lang['user']['active_sieve'];?></h3>
+      </div>
+      <div class="modal-body">
+      <pre id="user_sieve_filter"></pre>
+      </div>
+    </div>
+  </div>
+</div><!-- sieve filter modal -->
\ No newline at end of file
diff --git a/data/web/user.php b/data/web/user.php
index 50583515..5c5d6720 100644
--- a/data/web/user.php
+++ b/data/web/user.php
@@ -94,6 +94,13 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
     </div>
   </div>
   <hr>
+  <div class="row">
+    <div class="col-md-3 col-xs-5 text-right">  <span class="glyphicon glyphicon-filter"></span></div>
+    <div class="col-md-9 col-xs-7">
+    <p><a href="#userFilterModal" data-toggle="modal">[Show active user sieve filter]</a></p>
+    </div>
+  </div>
+  <hr>
   <?php // Get user information about aliases
   $user_get_alias_details = user_get_alias_details($username);
   ?>

From bcdccf9c927e0971d42475f44b77b76fcb1f0906 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Fri, 3 Nov 2017 20:38:59 +0100
Subject: [PATCH 11/23] [Compose] New images, add restart to dockerapi, remove
 stop grace period (container now handles stop signals better)

---
 docker-compose.yml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index ae908b1f..92ff25f5 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,4 @@
-version: '2.3'
+version: '2.1'
 services:
 
     unbound-mailcow:
@@ -92,7 +92,7 @@ services:
             - rspamd
 
     php-fpm-mailcow:
-      image: mailcow/phpfpm:1.3
+      image: mailcow/phpfpm:1.4
       build: ./data/Dockerfiles/phpfpm
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
@@ -143,7 +143,7 @@ services:
             - sogo
 
     dovecot-mailcow:
-      image: mailcow/dovecot:1.9
+      image: mailcow/dovecot:1.10
       build: ./data/Dockerfiles/dovecot
       volumes:
         - ./data/conf/dovecot:/usr/local/etc/dovecot
@@ -312,8 +312,8 @@ services:
             - watchdog
 
     dockerapi-mailcow:
-      image: mailcow/dockerapi:1.1
-      stop_grace_period: 3s
+      image: mailcow/dockerapi:1.2
+      restart: always
       build: ./data/Dockerfiles/dockerapi
       volumes:
         - /var/run/docker.sock:/var/run/docker.sock:ro

From 639ba941f3a1c223c481f03b4dfc41c9fa52ddd1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Fri, 3 Nov 2017 20:39:58 +0100
Subject: [PATCH 12/23] [Helper] Nextcloud helper script (wip)

---
 helper-scripts/nextcloud.sh | 116 ++++++++++++++++++++++++++++++++++++
 1 file changed, 116 insertions(+)
 create mode 100755 helper-scripts/nextcloud.sh

diff --git a/helper-scripts/nextcloud.sh b/helper-scripts/nextcloud.sh
new file mode 100755
index 00000000..175f3cd5
--- /dev/null
+++ b/helper-scripts/nextcloud.sh
@@ -0,0 +1,116 @@
+#!/bin/bash
+
+[[ -z ${1} ]] && { echo "No parameters given"; exit 1; }
+
+for bin in curl dirmngr; do
+  if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
+done
+
+while [ "$1" != '' ]; do
+  case "${1}" in
+    -p|--purge) NC_PURGE=y && shift;;
+    -i|--install) NC_INSTALL=y && shift;;
+    *) echo "Unknown parameter: ${1}" && shift;;
+  esac
+done
+
+[[ ${NC_PURGE} == "y" ]] && [[ ${NC_INSTALL} == "y" ]] && { echo "Cannot use -p and -i at the same time"; }
+
+source ./mailcow.conf
+
+if [[ ${NC_PURGE} == "y" ]]; then
+
+	docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e \
+	  "$(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "SELECT GROUP_CONCAT('DROP TABLE ', TABLE_SCHEMA, '.', TABLE_NAME SEPARATOR ';') FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'nc_%' AND TABLE_SCHEMA = '${DBNAME}';" -BN)"
+	docker exec -it $(docker ps -f name=redis-mailcow -q) /bin/sh -c 'redis-cli KEYS "*nextcloud*" | xargs redis-cli DEL'
+	if [ -d ./data/web/nextcloud/config ]; then
+	  mv ./data/web/nextcloud/config/ ./data/conf/nextcloud-config-folder-$(date +%s).bak
+	fi
+	[[ -d ./data/web/nextcloud ]] && rm -rf ./data/web/nextcloud
+
+	[[ -f ./data/conf/nginx/site.nextcloud.custom ]] && mv ./data/conf/nginx/site.nextcloud.custom ./data/conf/nginx/site.nextcloud.custom-$(date +%s).bak
+	[[ -f ./data/conf/nginx/nextcloud.conf ]] && mv ./data/conf/nginx/nextcloud.conf ./data/conf/nginx/nextcloud.conf-$(date +%s).bak
+
+	docker-compose restart nginx-mailcow
+
+elif [[ ${NC_INSTALL} == "y" ]]; then
+
+	NC_TYPE=
+	while [[ ! ${NC_TYPE} =~ ^subfolder$|^subdomain$ ]]; do
+		read -p "Configure as subdomain or subfolder? [subdomain/subfolder] " NC_TYPE
+	done
+
+
+	if [[ ${NC_TYPE} == "subdomain" ]]; then
+		NC_SUBD=
+	    while [[ -z ${NC_SUBD} ]]; do
+    	    read -p "Which subdomain? [format: nextcloud.domain.tld] " NC_SUBD
+    	done
+		if ! ping -q -c2 ${NC_SUBD} > /dev/null 2>&1 ; then
+			read -p "Cannot ping subdomain, continue anyway? [y|N] " NC_CONT_FAIL
+			[[ ! ${NC_CONT_FAIL,,} =~ ^(yes|y)$ ]] && { echo "Ok, exiting..."; exit 1; }
+		fi
+	fi
+
+	ADMIN_NC_PASS=$(</dev/urandom tr -dc A-Za-z0-9 | head -c 28)
+	NEXTCLOUD_VERSION=$(curl -s https://www.servercow.de/nextcloud/latest.php)
+
+	[[ -z ${NEXTCLOUD_VERSION} ]] && { echo "Error, cannot determine nextcloud version, exiting..."; exit 1; }
+
+	curl -L# -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/nextcloud-${NEXTCLOUD_VERSION}.tar.bz2" \
+	  && curl -L# -o nextcloud.tar.bz2.asc "https://download.nextcloud.com/server/releases/nextcloud-${NEXTCLOUD_VERSION}.tar.bz2.asc" \
+	  && export GNUPGHOME="$(mktemp -d)" \
+	  && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys 28806A878AE423A28372792ED75899B9A724937A \
+	  && gpg --batch --verify nextcloud.tar.bz2.asc nextcloud.tar.bz2 \
+	  && rm -r "$GNUPGHOME" nextcloud.tar.bz2.asc \
+	  && tar -xjf nextcloud.tar.bz2 -C ./data/web/ \
+	  && rm nextcloud.tar.bz2 \
+	  && rm -rf ./data/web/nextcloud/updater \
+	  && mkdir -p ./data/web/nextcloud/data \
+	  && mkdir -p ./data/web/nextcloud/custom_apps \
+	  && chmod +x ./data/web/nextcloud/occ
+
+	docker exec -it $(docker ps -f name=php-fpm-mailcow -q) /bin/bash -c "chown -R www-data:www-data /web/nextcloud/data /web/nextcloud/config /web/nextcloud/apps /web/nextcloud/custom_apps"
+	docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ maintenance:install \
+	  --database mysql \
+	  --database-host mysql \
+	  --database-name ${DBNAME} \
+	  --database-user ${DBUSER} \
+	  --database-pass ${DBPASS} \
+	  --database-table-prefix nc_ \
+	  --admin-user admin \
+	  --admin-pass ${ADMIN_NC_PASS} \
+      --data-dir /web/nextcloud/data
+
+	docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) bash -c "/web/nextcloud/occ config:system:set redis host --value=redis --type=string; \
+	  /web/nextcloud/occ config:system:set redis port --value=6379 --type=integer; \
+	  /web/nextcloud/occ config:system:set memcache.locking --value='\OC\Memcache\Redis' --type=string; \
+	  /web/nextcloud/occ config:system:set memcache.local --value='\OC\Memcache\Redis' --type=string; \
+	  /web/nextcloud/occ config:system:set trusted_proxies 0 --value=fd4d:6169:6c63:6f77::1; \
+	  /web/nextcloud/occ config:system:set trusted_proxies 1 --value=172.22.1.0/24; \
+	  /web/nextcloud/occ config:system:set overwritewebroot --value=/nextcloud; \
+	  /web/nextcloud/occ config:system:set overwritehost --value=${MAILCOW_HOSTNAME}; \
+	  /web/nextcloud/occ config:system:set mail_smtpmode --value=smtp; \
+	  /web/nextcloud/occ config:system:set mail_smtpauthtype --value=LOGIN; \
+	  /web/nextcloud/occ config:system:set mail_from_address --value=nextcloud; \
+	  /web/nextcloud/occ config:system:set mail_domain --value=${MAILCOW_HOSTNAME}; \
+	  /web/nextcloud/occ config:system:set mail_smtphost --value=postfix; \
+	  /web/nextcloud/occ config:system:set mail_smtpport --value=588
+	  /web/nextcloud/occ app:enable user_external
+	  /web/nextcloud/occ config:system:set user_backends 0 arguments 0 --value={dovecot:143/imap/tls/novalidate-cert}
+	  /web/nextcloud/occ config:system:set user_backends 0 class --value=OC_User_IMAP"
+
+	if [[ ${NC_TYPE} == "subdomain" ]]; then
+		docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ config:system:set overwritewebroot --value=/
+		docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ config:system:set overwritehost --value=nextcloud.develcow.de
+		cp ./data/assets/nextcloud/nextcloud.conf ./data/conf/nginx/
+		sed -i 's/NC_SUBD/${NC_SUBD}/g' ./data/conf/nginx/nextcloud.conf
+	elif [[ ${NC_TYPE} == "subfolder" ]]; then
+		cp ./data/assets/nextcloud/site.nextcloud.custom ./data/conf/nginx/
+	fi
+
+	docker-compose restart nginx-mailcow
+
+	echo "Login as admin with password: ${ADMIN_NC_PASS}"
+
+fi

From 548fe979ec2078bdde0dce4fa8b9462561f03440 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Sun, 5 Nov 2017 12:17:37 +0100
Subject: [PATCH 13/23] [Compose] add net_bind_service cap to Dovecot, new
 images, reduce oom_score for dockerapi

---
 docker-compose.yml | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index 92ff25f5..ac85c6fd 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -69,7 +69,7 @@ services:
             - clamd
 
     rspamd-mailcow:
-      image: mailcow/rspamd:1.12
+      image: mailcow/rspamd:1.13
       build: ./data/Dockerfiles/rspamd
       stop_grace_period: 30s
       depends_on:
@@ -143,8 +143,10 @@ services:
             - sogo
 
     dovecot-mailcow:
-      image: mailcow/dovecot:1.10
+      image: mailcow/dovecot:1.11
       build: ./data/Dockerfiles/dovecot
+      cap_add:
+        - NET_BIND_SERVICE
       volumes:
         - ./data/conf/dovecot:/usr/local/etc/dovecot
         - ./data/assets/ssl:/etc/ssl/mail/:ro
@@ -315,6 +317,7 @@ services:
       image: mailcow/dockerapi:1.2
       restart: always
       build: ./data/Dockerfiles/dockerapi
+      oom_score_adj: -10
       volumes:
         - /var/run/docker.sock:/var/run/docker.sock:ro
       networks:

From 586a0b0e051a573f8d4f923f55df2c8905def1a7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Sun, 5 Nov 2017 12:18:52 +0100
Subject: [PATCH 14/23] [Dovecot] Add bindirs to cache compiled scripts, drop
 some privileges, run one login proc per user

---
 data/conf/dovecot/dovecot.conf | 19 +++++++++++++++----
 1 file changed, 15 insertions(+), 4 deletions(-)

diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf
index a9e6f0af..6cf1897b 100644
--- a/data/conf/dovecot/dovecot.conf
+++ b/data/conf/dovecot/dovecot.conf
@@ -173,6 +173,9 @@ service dict {
     group = vmail
   }
 }
+service log {
+  user = dovenull
+}
 service auth {
   inet_listener auth-inet {
     port = 10001
@@ -185,7 +188,6 @@ service auth {
     mode = 0600
     user = vmail
   }
-  user = root
 }
 service managesieve-login {
   inet_listener sieve {
@@ -193,10 +195,19 @@ service managesieve-login {
   }
   service_count = 1
   process_min_avail = 2
-  vsz_limit = 128M
+  vsz_limit = 64M
+}
+service imap-login {
+  service_count = 1
+  vsz_limit = 64M
+  user = dovenull
+}
+service pop3-login {
+  service_count = 1
 }
 service imap {
   executable = imap imap-postlogin
+  user = dovenull
 }
 service managesieve {
   process_limit = 256
@@ -249,8 +260,8 @@ plugin {
   sieve_quota_max_scripts = 0
   sieve_quota_max_storage = 0
   listescape_char = "\\"
-  sieve_before = dict:proxy::sieve_before;name=active
-  sieve_after = dict:proxy::sieve_after;name=active
+  sieve_before = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir
+  sieve_after = dict:proxy::sieve_after;name=active;bindir=/var/vmail/sieve_after_bindir
   sieve_after2 = /var/vmail/sieve/global.sieve
   #mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
   #mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem

From a36a8828c2abd0ef0766f76c9edbb92d42baac7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Sun, 5 Nov 2017 12:19:18 +0100
Subject: [PATCH 15/23] [Dovecot] Specify supervisord user

---
 data/Dockerfiles/dovecot/supervisord.conf | 1 +
 1 file changed, 1 insertion(+)

diff --git a/data/Dockerfiles/dovecot/supervisord.conf b/data/Dockerfiles/dovecot/supervisord.conf
index 33d488b8..f517c388 100644
--- a/data/Dockerfiles/dovecot/supervisord.conf
+++ b/data/Dockerfiles/dovecot/supervisord.conf
@@ -1,5 +1,6 @@
 [supervisord]
 nodaemon=true
+user=root
 
 [program:syslog-ng]
 command=/usr/sbin/syslog-ng --foreground --no-caps

From f603008440b28f0d8423c56ac49a56b5481d4089 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Sun, 5 Nov 2017 12:20:05 +0100
Subject: [PATCH 16/23] [Web] Reset last_run for imapsync jobs to run next,
 other minor changes...

---
 data/web/inc/functions.mailbox.inc.php | 12 ++++++++++--
 data/web/lang/lang.de.php              |  4 ++++
 data/web/lang/lang.en.php              |  5 ++++-
 data/web/mailbox.php                   |  6 +++---
 data/web/modals/mailbox.php            |  2 +-
 data/web/modals/user.php               |  2 +-
 6 files changed, 23 insertions(+), 8 deletions(-)

diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php
index ed3caaa7..2ca86e81 100644
--- a/data/web/inc/functions.mailbox.inc.php
+++ b/data/web/inc/functions.mailbox.inc.php
@@ -1390,11 +1390,12 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
             return false;
           }
           foreach ($ids as $id) {
-            $is_now = mailbox('get', 'syncjob_details', $id);
+            $is_now = mailbox('get', 'syncjob_details', $id, array('with_password'));
             if (!empty($is_now)) {
               $username = $is_now['user2'];
               $user1 = (!empty($_data['user1'])) ? $_data['user1'] : $is_now['user1'];
               $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int'];
+              $last_run = (isset($_data['last_run'])) ? NULL : $is_now['last_run'];
               $delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates'];
               $delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1'];
               $delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2'];
@@ -1456,7 +1457,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
               return false;
             }
             try {
-              $stmt = $pdo->prepare("UPDATE `imapsync` SET `delete1` = :delete1, `delete2` = :delete2, `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active
+              $stmt = $pdo->prepare("UPDATE `imapsync` SET `delete1` = :delete1, `delete2` = :delete2, `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `last_run` = :last_run, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active
                 WHERE `id` = :id");
               $stmt->execute(array(
                 ':delete1' => $delete1,
@@ -1468,6 +1469,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
                 ':host1' => $host1,
                 ':user1' => $user1,
                 ':password1' => $password1,
+                ':last_run' => $last_run,
                 ':mins_interval' => $mins_interval,
                 ':port1' => $port1,
                 ':enc1' => $enc1,
@@ -2446,6 +2448,12 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
                 CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
                   FROM `imapsync` WHERE id = :id");
             }
+            elseif (isset($attr) && in_array('with_password', $attr)) {
+              $stmt = $pdo->prepare("SELECT *,
+                `active` AS `active_int`,
+                CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
+                  FROM `imapsync` WHERE id = :id");
+            }
             else {
               $field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("password1")');
               $fields = $field_query->fetchAll(PDO::FETCH_ASSOC);
diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php
index bd329fa1..938ef26a 100644
--- a/data/web/lang/lang.de.php
+++ b/data/web/lang/lang.de.php
@@ -268,6 +268,10 @@ $lang['mailbox']['deactivate'] = 'Deaktivieren';
 $lang['mailbox']['owner'] = 'Besitzer';
 $lang['mailbox']['mins_interval'] = 'Intervall (min)';
 $lang['mailbox']['last_run'] = 'Letzte Ausführung';
+$lang['mailbox']['last_run_reset'] = 'Als nächstes ausführen';
+$lang['mailbox']['sieve_info'] = 'Es können mehrere Filter pro Benutzer existieren, aber nur ein Filter eines Typs (Pre-/Postfilter) kann gleichzeitig aktiv sein.<br>
+Die Ausführung erfolgt in nachstehender Reihenfolge. Ein fehlgeschlagenes Script sowie der Befehl "keep;" stoppen die weitere Verarbeitung <b>nicht</b>.<br>
+Prefilter → User scripts → Postfilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/sieve_after" target="_blank">global sieve postfilter</a>';
 
 $lang['info']['no_action'] = 'Keine Aktion anwendbar';
 $lang['delete']['title'] = 'Objekt entfernen';
diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php
index efa19dd2..cdf0474b 100644
--- a/data/web/lang/lang.en.php
+++ b/data/web/lang/lang.en.php
@@ -271,7 +271,10 @@ $lang['mailbox']['deactivate'] = 'Deactivate';
 $lang['mailbox']['owner'] = 'Owner';
 $lang['mailbox']['mins_interval'] = 'Interval (min)';
 $lang['mailbox']['last_run'] = 'Last run';
-
+$lang['mailbox']['last_run_reset'] = 'Schedule next';
+$lang['mailbox']['sieve_info'] = 'You can store multiple filters per user, but only one prefilter and one postfilter can be active at the same time.<br>
+Each filter will be processed in the described order. Neither a failed script nor an issued "keep;" will stop processing of further scripts.<br>
+Prefilter → User scripts → Postfilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/sieve_after" target="_blank">global sieve postfilter</a>';
 $lang['info']['no_action'] = 'No action applicable';
 
 $lang['delete']['title'] = 'Remove object';
diff --git a/data/web/mailbox.php b/data/web/mailbox.php
index a2d72f22..149a59d3 100644
--- a/data/web/mailbox.php
+++ b/data/web/mailbox.php
@@ -162,6 +162,8 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
                 <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="syncjob" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
                 <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
                 <ul class="dropdown-menu">
+                  <li><a id="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"last_run":""}' href="#"><?=$lang['mailbox']['last_run_reset'];?></a></li>
+                  <li role="separator" class="divider"></li>
                   <li><a id="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
                   <li><a id="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
                   <li role="separator" class="divider"></li>
@@ -178,9 +180,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
             <div class="panel-heading">
               <h3 class="panel-title"><?=$lang['mailbox']['filters'];?></h3>
             </div>
-            <p style="margin:10px" class="help-block">You can store multiple filters per user, but only one prefilter and one postfilter can be active at the same time.</p>
-            <p style="margin:10px" class="help-block">Each filter will be processed in the described order. A failed script will not stop processing of further scripts.</a></p>
-            <p style="margin:10px" class="help-block">Prefilter → User scripts → Postfilter → <a href="https://github.com/mailcow/mailcow-dockerized/blob/master/data/conf/dovecot/sieve_after" target="_blank">global sieve postfilter</a></p>
+            <p style="margin:10px" class="help-block"><?=$lang['mailbox']['sieve_info'];?></p>
             <div class="table-responsive">
               <table class="table table-striped" id="filter_table"></table>
             </div>
diff --git a/data/web/modals/mailbox.php b/data/web/modals/mailbox.php
index 61dd747d..0dcdfd44 100644
--- a/data/web/modals/mailbox.php
+++ b/data/web/modals/mailbox.php
@@ -511,7 +511,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
     <div class="modal-content">
       <div class="modal-header"><h4 class="modal-title">Log</h4></div>
       <div class="modal-body">
-        <textarea class="form-control" rows="20" id="logText"></textarea>
+        <textarea class="form-control" rows="20" id="logText" spellcheck="false"></textarea>
       </div>
     </div>
   </div>
diff --git a/data/web/modals/user.php b/data/web/modals/user.php
index 9b4394f3..da1e98ce 100644
--- a/data/web/modals/user.php
+++ b/data/web/modals/user.php
@@ -117,7 +117,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
     <div class="modal-content">
       <div class="modal-header"><h4 class="modal-title">Log</h4></div>
       <div class="modal-body">
-        <textarea class="form-control" rows="20" id="logText"></textarea>
+        <textarea class="form-control" rows="20" id="logText" spellcheck="false"></textarea>
       </div>
     </div>
   </div>

From 3873e38919457902c10324ec1c026a68dbbc85ca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Mon, 6 Nov 2017 13:35:48 +0100
Subject: [PATCH 17/23] [SOGo] Use SOGoMaximumSyncResponseSize of 2048

---
 data/conf/sogo/sogo.conf | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/data/conf/sogo/sogo.conf b/data/conf/sogo/sogo.conf
index 404fd451..bf79358a 100644
--- a/data/conf/sogo/sogo.conf
+++ b/data/conf/sogo/sogo.conf
@@ -46,7 +46,7 @@
     // 100 seems to break some Android clients
     //SOGoMaximumSyncWindowSize = 100;
     // This should do the trick for Outlook 2016
-    SOGoMaximumSyncResponseSize = 5172;
+    SOGoMaximumSyncResponseSize = 2048;
 
     WOWatchDogRequestTimeout = 10;
     WOListenQueueSize = 300;

From 2372949162e3c34b47e0cc2b478d5f13c0ed8b80 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Mon, 6 Nov 2017 21:58:08 +0100
Subject: [PATCH 18/23] [Web] Fix check for existing domain when adding alias
 domains

---
 data/web/inc/functions.mailbox.inc.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php
index 2ca86e81..654f8972 100644
--- a/data/web/inc/functions.mailbox.inc.php
+++ b/data/web/inc/functions.mailbox.inc.php
@@ -654,13 +654,13 @@ function mailbox($_action, $_type, $_data = null, $attr = null) {
               }
               $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain
                 UNION
-                SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain_in_domain");
+                SELECT `domain` FROM `domain` WHERE `domain`= :alias_domain_in_domain");
               $stmt->execute(array(':alias_domain' => $alias_domain, ':alias_domain_in_domain' => $alias_domain));
               $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
               if ($num_results != 0) {
                 $_SESSION['return'] = array(
                   'type' => 'danger',
-                  'msg' => sprintf($lang['danger']['aliasd_exists'])
+                  'msg' => sprintf($lang['danger']['alias_domain_invalid'])
                 );
                 return false;
               }

From b4cc5a98914477122db23cff6cd01a7cf4353c8a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Tue, 7 Nov 2017 18:38:40 +0100
Subject: [PATCH 19/23] generate_config: Added hint for FQDN

---
 generate_config.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/generate_config.sh b/generate_config.sh
index ecf5723a..34be645e 100755
--- a/generate_config.sh
+++ b/generate_config.sh
@@ -13,7 +13,7 @@ if [[ -f mailcow.conf ]]; then
 fi
 
 if [ -z "$MAILCOW_HOSTNAME" ]; then
-  read -p "Hostname (FQDN): " -ei "mx.example.org" MAILCOW_HOSTNAME
+  read -p "Hostname (FQDN - example.org is not a valid FQDN): " -ei "mx.example.org" MAILCOW_HOSTNAME
 fi
 
 if [[ -a /etc/timezone ]]; then

From 60e97503f711ed776c68a8adaa0a5cd2c20fd1a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Wed, 8 Nov 2017 11:07:32 +0100
Subject: [PATCH 20/23] [Web, Dovecot] Show wether a sync job is running,
 validate min max input attr and validate these values

---
 data/Dockerfiles/dovecot/imapsync_cron.pl |  6 +++-
 data/web/inc/init_db.inc.php              |  3 +-
 data/web/js/api.js                        | 36 +++++++++++++++++++++--
 data/web/js/mailbox.js                    | 11 ++++++-
 data/web/js/user.js                       | 23 +++++++++++----
 data/web/json_api.php                     | 35 +++++++++++++++++++++-
 data/web/lang/lang.de.php                 |  6 ++++
 data/web/lang/lang.en.php                 |  6 ++++
 data/web/modals/mailbox.php               |  4 +++
 data/web/modals/user.php                  |  3 ++
 docker-compose.yml                        |  2 +-
 11 files changed, 122 insertions(+), 13 deletions(-)

diff --git a/data/Dockerfiles/dovecot/imapsync_cron.pl b/data/Dockerfiles/dovecot/imapsync_cron.pl
index 132e536e..ddd4746a 100755
--- a/data/Dockerfiles/dovecot/imapsync_cron.pl
+++ b/data/Dockerfiles/dovecot/imapsync_cron.pl
@@ -54,6 +54,10 @@ while ($row = $sth->fetchrow_arrayref()) {
   $delete1            = @$row[12];
   $delete2            = @$row[13];
 
+  $is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1 WHERE id = ?");
+  $is_running->bind_param( 1, ${id} );
+  $is_running->execute();
+
   if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
 
   my $template = $run_dir . '/imapsync.XXXXXXX';
@@ -83,7 +87,7 @@ while ($row = $sth->fetchrow_arrayref()) {
 	"--passfile2", $passfile2->filename,
 	'--no-modulesversion'], ">", \my $stdout;
 
-  $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, last_run = NOW() WHERE id = ?");
+  $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, last_run = NOW(), is_running = 0 WHERE id = ?");
   $update->bind_param( 1, ${stdout} );
   $update->bind_param( 2, ${id} );
   $update->execute();
diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php
index 9b6a63c9..1bc02497 100644
--- a/data/web/inc/init_db.inc.php
+++ b/data/web/inc/init_db.inc.php
@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
     global $pdo;
 
-    $db_version = "31102017_1049";
+    $db_version = "08112017_1049";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); 
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -314,6 +314,7 @@ function init_db_schema() {
           "delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "delete1" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "delete2" => "TINYINT(1) NOT NULL DEFAULT '0'",
+          "is_running" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "returned_text" => "TEXT",
           "last_run" => "TIMESTAMP NULL DEFAULT NULL",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
diff --git a/data/web/js/api.js b/data/web/js/api.js
index 23bf229e..824e2d64 100644
--- a/data/web/js/api.js
+++ b/data/web/js/api.js
@@ -79,6 +79,21 @@ $(document).ready(function() {
             $(this).removeClass('inputMissingAttr');
           }
         }
+        if ($(this).attr("max")) {
+          if ($(this).val() > $(this).attr("max")) {
+            invalid = true;
+            $(this).addClass('inputMissingAttr');
+          } else {
+            if ($(this).attr("min")) {
+              if ($(this).val() < $(this).attr("min")) {
+                invalid = true;
+                $(this).addClass('inputMissingAttr');
+              } else {
+                $(this).removeClass('inputMissingAttr');
+              }
+            }
+          }
+        }
       });
       if (!req_empty) {
         var attr_to_merge = $(this).closest("form").serializeObject();
@@ -129,18 +144,33 @@ $(document).ready(function() {
     // If clicked button is in a form with the same data-id as the button,
     // we merge all input fields by {"name":"value"} into api-attr
     if ($(this).closest("form").data('id') == id) {
-      var req_empty = false;
+      var invalid = false;
       $(this).closest("form").find('select, textarea, input').each(function() {
         if ($(this).prop('required')) {
           if (!$(this).val() && $(this).prop('disabled') === false) {
-            req_empty = true;
+            invalid = true;
             $(this).addClass('inputMissingAttr');
           } else {
             $(this).removeClass('inputMissingAttr');
           }
         }
+        if ($(this).attr("max")) {
+          if ($(this).val() > $(this).attr("max")) {
+            invalid = true;
+            $(this).addClass('inputMissingAttr');
+          } else {
+            if ($(this).attr("min")) {
+              if ($(this).val() < $(this).attr("min")) {
+                invalid = true;
+                $(this).addClass('inputMissingAttr');
+              } else {
+                $(this).removeClass('inputMissingAttr');
+              }
+            }
+          }
+        }
       });
-      if (!req_empty) {
+      if (!invalid) {
         var attr_to_merge = $(this).closest("form").serializeObject();
         var api_attr = $.extend(api_attr, attr_to_merge)
       } else {
diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js
index d55b1913..9e2afa2c 100644
--- a/data/web/js/mailbox.js
+++ b/data/web/js/mailbox.js
@@ -421,7 +421,8 @@ jQuery(function($){
         {"name":"mins_interval","title":lang.mins_interval,"breakpoints":"all"},
         {"name":"last_run","title":lang.last_run,"breakpoints":"all"},
         {"name":"log","title":"Log"},
-        {"name":"active","filterable": false,"style":{"maxWidth":"50px","width":"70px"},"title":lang.active},
+        {"name":"active","filterable": false,"style":{"maxWidth":"70px","width":"70px"},"title":lang.active},
+        {"name":"is_running","filterable": false,"style":{"maxWidth":"120px","width":"100px"},"title":lang.status},
         {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
       ],
       "empty": lang.empty,
@@ -442,6 +443,14 @@ jQuery(function($){
               '<a href="#" id="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' +
               '</div>';
             item.chkbox = '<input type="checkbox" data-id="syncjob" name="multi_select" value="' + item.id + '" />';
+            if (item.is_running == 1) {
+              item.is_running = '<span id="active-script" class="label label-success">' + lang.running + '</span>';
+            } else {
+              item.is_running = '<span id="inactive-script" class="label label-warning">' + lang.waiting + '</span>';
+            }
+            if (!item.last_run > 0) {
+              item.last_run = lang.waiting;
+            }
           });
         }
       }),
diff --git a/data/web/js/user.js b/data/web/js/user.js
index 239507f4..900dced1 100644
--- a/data/web/js/user.js
+++ b/data/web/js/user.js
@@ -89,13 +89,14 @@ jQuery(function($){
         {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"},
         {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}},
         {"name":"server_w_port","title":"Server"},
-        {"name":"enc1","title":lang.encryption},
+        {"name":"enc1","title":lang.encryption,"breakpoints":"xs sm"},
         {"name":"user1","title":lang.username},
-        {"name":"exclude","title":lang.excludes},
+        {"name":"exclude","title":lang.excludes,"breakpoints":"xs sm"},
         {"name":"mins_interval","title":lang.interval + " (min)"},
-        {"name":"last_run","title":lang.last_run},
+        {"name":"last_run","title":lang.last_run,"breakpoints":"xs sm"},
         {"name":"log","title":"Log"},
-        {"name":"active","filterable": false,"style":{"maxWidth":"50px","width":"70px"},"title":lang.active},
+        {"name":"active","filterable": false,"style":{"maxWidth":"70px","width":"70px"},"title":lang.active},
+        {"name":"is_running","filterable": false,"style":{"maxWidth":"120px","width":"100px"},"title":lang.status},
         {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"}
       ],
       "empty": lang.empty,
@@ -109,7 +110,11 @@ jQuery(function($){
         success: function (data) {
           $.each(data, function (i, item) {
             item.log = '<a href="#syncjobLogModal" data-toggle="modal" data-syncjob-id="' + encodeURI(item.id) + '">Open logs</a>'
-            item.exclude = '<code>' + item.exclude + '</code>'
+            if (!item.exclude > 0) {
+              item.exclude = '-';
+            } else {
+              item.exclude  = '<code>' + item.exclude + '</code>';
+            }
             item.server_w_port = item.user1 + '@' + item.host1 + ':' + item.port1;
             if (acl_data.syncjobs === 1) {
               item.action = '<div class="btn-group">' +
@@ -122,6 +127,14 @@ jQuery(function($){
               item.action = '<span>-</span>';
               item.chkbox = '<input type="checkbox" disabled />';
             }
+            if (item.is_running == 1) {
+              item.is_running = '<span id="active-script" class="label label-success">' + lang.running + '</span>';
+            } else {
+              item.is_running = '<span id="inactive-script" class="label label-warning">' + lang.waiting + '</span>';
+            }
+            if (!item.last_run > 0) {
+              item.last_run = lang.waiting;
+            }
           });
         }
       }),
diff --git a/data/web/json_api.php b/data/web/json_api.php
index 0bc5e3a6..b622c312 100644
--- a/data/web/json_api.php
+++ b/data/web/json_api.php
@@ -13,7 +13,7 @@ delete/alias => POST data:
 
 */
 header('Content-Type: application/json');
-require_once 'inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
 error_reporting(0);
 if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_username'])) {
   if (isset($_GET['query'])) {
@@ -489,6 +489,39 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
               ));
             }
           break;
+          case "syncjob":
+            if (isset($_POST['attr'])) {
+              $attr = (array)json_decode($_POST['attr'], true);
+              if (mailbox('add', 'syncjob', $attr) === false) {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'error',
+                    'msg' => 'Cannot add item'
+                  ));
+                }
+              }
+              else {
+                if (isset($_SESSION['return'])) {
+                  echo json_encode($_SESSION['return']);
+                }
+                else {
+                  echo json_encode(array(
+                    'type' => 'success',
+                    'msg' => 'Task completed'
+                  ));
+                }
+              }
+            }
+            else {
+              echo json_encode(array(
+                'type' => 'error',
+                'msg' => 'Cannot find attributes in post data'
+              ));
+            }
+          break;
         }
       break;
       case "get":
diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php
index 86c4505f..2dd5cd9e 100644
--- a/data/web/lang/lang.de.php
+++ b/data/web/lang/lang.de.php
@@ -158,6 +158,9 @@ $lang['user']['spamfilter_red'] = 'Rot: Die Nachricht ist eindeutig Spam und wir
 $lang['user']['spamfilter_default_score'] = 'Standardwert:';
 $lang['user']['spamfilter_hint'] = 'Der erste Wert beschreibt den "low spam score", der zweite Wert den "high spam score".';
 $lang['user']['spamfilter_table_domain_policy'] = "n.v. (Domainrichtlinie)";
+$lang['user']['waiting'] = "Warte auf Ausführung";
+$lang['user']['status'] = "Status";
+$lang['user']['running'] = "Wird ausgeführt";
 
 $lang['user']['tls_policy_warning'] = '<strong>Vorsicht:</strong> Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.<br>Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.<br>Diese Einstellung ist aktiv für die primäre Mailbox, für alle Alias-Adressen, die dieser Mailbox <b>direkt zugeordnet</b> sind (lediglich eine einzige Ziel-Adresse) und der Adressen, die sich aus Alias-Domains ergeben. Ausgeschlossen sind temporäre Aliasse ("Spam-Alias-Adressen"), Catch-All Alias-Adressen sowie Alias-Adressen mit mehreren Zielen.';
 $lang['user']['tls_policy'] = 'Verschlüsselungsrichtlinie';
@@ -554,3 +557,6 @@ $lang['admin']['remove_row'] = "Zeile entfernen";
 $lang['admin']['add_row'] = "Zeile hinzufügen";
 $lang['admin']['reset_default'] = "Auf Standard zurücksetzen";
 $lang['admin']['merged_vars_hint'] = 'Ausgegraute Zeilen wurden aus der Datei <code>vars.inc.(local.)php</code> gelesen und können nicht mittels UI verändert werden.';
+$lang['mailbox']['waiting'] = "Wartend";
+$lang['mailbox']['status'] = "Status";
+$lang['mailbox']['running'] = "In Ausführung";
\ No newline at end of file
diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php
index 01996066..8832c334 100644
--- a/data/web/lang/lang.en.php
+++ b/data/web/lang/lang.en.php
@@ -160,6 +160,9 @@ $lang['user']['spamfilter_red'] = 'Red: This message is spam and will be rejecte
 $lang['user']['spamfilter_default_score'] = 'Default values:';
 $lang['user']['spamfilter_hint'] = 'The first value describes the "low spam score", the second represents the "high spam score".';
 $lang['user']['spamfilter_table_domain_policy'] = "n/a (domain policy)";
+$lang['user']['waiting'] = "Waiting";
+$lang['user']['status'] = "Status";
+$lang['user']['running'] = "Running";
 
 $lang['user']['tls_policy_warning'] = '<strong>Warning:</strong> If you decide to enforce encrypted mail transfer, you may lose emails.<br>Messages to not satisfy the policy will be bounced with a hard fail by the mail system.<br>This option applies to your primary email address (login name), all addresses derived from alias domains as well as alias addresses <b>with only this single mailbox</b> as target.';
 $lang['user']['tls_policy'] = 'Encryption policy';
@@ -567,6 +570,9 @@ $lang['admin']['remove_row'] = "Remove row";
 $lang['admin']['add_row'] = "Add row";
 $lang['admin']['reset_default'] = "Reset to default";
 $lang['admin']['merged_vars_hint'] = 'Greyed out rows were merged from <code>vars.inc.(local.)php</code> and cannot be modified.';
+$lang['mailbox']['waiting'] = "Waiting";
+$lang['mailbox']['status'] = "Status";
+$lang['mailbox']['running'] = "Running";
 
 $lang['edit']['tls_policy'] = "Change TLS policy";
 $lang['edit']['spam_score'] = "Set a custom spam score";
diff --git a/data/web/modals/mailbox.php b/data/web/modals/mailbox.php
index 0dcdfd44..192124c2 100644
--- a/data/web/modals/mailbox.php
+++ b/data/web/modals/mailbox.php
@@ -44,6 +44,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
             </label>
             <div class="col-sm-10">
             <input type="text" class="form-control" name="quota" min="1" max="" id="addInputQuota" disabled value="<?=$lang['add']['select_domain'];?>" required>
+            <small class="help-block">min. 1</small>
             </div>
           </div>
           <div class="form-group">
@@ -344,6 +345,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 						<label class="control-label col-sm-2" for="port1"><?=$lang['add']['port'];?></label>
 						<div class="col-sm-10">
 						<input type="number" class="form-control" name="port1" id="port1" min="1" max="65535" value="143" required>
+            <small class="help-block">1-65535</small>
 						</div>
 					</div>
 					<div class="form-group">
@@ -372,6 +374,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 						<label class="control-label col-sm-2" for="mins_interval"><?=$lang['add']['mins_interval'];?></label>
 						<div class="col-sm-10">
               <input type="number" class="form-control" name="mins_interval" min="10" max="3600" value="20" required>
+              <small class="help-block">10-3600</small>
 						</div>
 					</div>
 					<div class="form-group">
@@ -384,6 +387,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 						<label class="control-label col-sm-2" for="maxage"><?=$lang['edit']['maxage'];?></label>
 						<div class="col-sm-10">
 						<input type="number" class="form-control" name="maxage" id="maxage" min="0" max="32000" value="0">
+            <small class="help-block">0-32000</small>
 						</div>
 					</div>
 					<div class="form-group">
diff --git a/data/web/modals/user.php b/data/web/modals/user.php
index da1e98ce..a7cc3995 100644
--- a/data/web/modals/user.php
+++ b/data/web/modals/user.php
@@ -25,6 +25,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 						<label class="control-label col-sm-2" for="port1"><?=$lang['add']['port'];?></label>
 						<div class="col-sm-10">
 						<input type="number" class="form-control" name="port1" id="port1" min="1" max="65535" value="143" required>
+            <small class="help-block">1-65535</small>
 						</div>
 					</div>
 					<div class="form-group">
@@ -53,6 +54,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 						<label class="control-label col-sm-2" for="mins_interval"><?=$lang['add']['mins_interval'];?></label>
 						<div class="col-sm-10">
               <input type="number" class="form-control" name="mins_interval" min="10" max="3600" value="20" required>
+              <small class="help-block">10-3600</small>
 						</div>
 					</div>
 					<div class="form-group">
@@ -65,6 +67,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
 						<label class="control-label col-sm-2" for="maxage"><?=$lang['edit']['maxage'];?></label>
 						<div class="col-sm-10">
 						<input type="number" class="form-control" name="maxage" id="maxage" min="0" max="32000" value="0">
+            <small class="help-block">0-32000</small>
 						</div>
 					</div>
 					<div class="form-group">
diff --git a/docker-compose.yml b/docker-compose.yml
index ac85c6fd..e4b6e858 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -143,7 +143,7 @@ services:
             - sogo
 
     dovecot-mailcow:
-      image: mailcow/dovecot:1.11
+      image: mailcow/dovecot:1.12
       build: ./data/Dockerfiles/dovecot
       cap_add:
         - NET_BIND_SERVICE

From ec37c6b0c2e1f1521fb1c1003687729214a9d46c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Fri, 10 Nov 2017 19:58:17 +0100
Subject: [PATCH 21/23] [Web] Minor changes

---
 data/web/inc/{ => ajax}/relay_check.php |  0
 data/web/inc/header.inc.php             |  5 -----
 data/web/js/admin.js                    |  2 +-
 data/web/mailbox.php                    | 14 +++++++++-----
 4 files changed, 10 insertions(+), 11 deletions(-)
 rename data/web/inc/{ => ajax}/relay_check.php (100%)

diff --git a/data/web/inc/relay_check.php b/data/web/inc/ajax/relay_check.php
similarity index 100%
rename from data/web/inc/relay_check.php
rename to data/web/inc/ajax/relay_check.php
diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php
index d6d29c59..cedd07ca 100644
--- a/data/web/inc/header.inc.php
+++ b/data/web/inc/header.inc.php
@@ -78,11 +78,6 @@
                 <li<?= (preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/mailbox.php"><?= $lang['header']['mailboxes']; ?></a></li>
               <?php
               }
-              if ($_SESSION['mailcow_cc_role'] == 'admin') {
-              ?>
-                <li<?= (preg_match("/diagnostics/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/diagnostics.php"><?= $lang['header']['diagnostics']; ?></a></li>
-              <?php
-              }
               if ($_SESSION['mailcow_cc_role'] != 'admin') {
               ?>
                 <li<?= (preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? ' class="active"' : ''; ?>><a href="/user.php"><?= $lang['header']['user_settings']; ?></a></li>
diff --git a/data/web/js/admin.js b/data/web/js/admin.js
index c368cbe7..997e76a3 100644
--- a/data/web/js/admin.js
+++ b/data/web/js/admin.js
@@ -466,7 +466,7 @@ jQuery(function($){
     $(this).html('<span class="glyphicon glyphicon-refresh glyphicon-spin"></span> ');
     $.ajax({
         type: 'GET',
-        url: 'inc/relay_check.php',
+        url: 'inc/ajax/relay_check.php',
         dataType: 'text',
         data: $('#test_relayhost_form').serialize(),
         complete: function (data) {
diff --git a/data/web/mailbox.php b/data/web/mailbox.php
index 149a59d3..30c3ecdb 100644
--- a/data/web/mailbox.php
+++ b/data/web/mailbox.php
@@ -39,12 +39,16 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
                 <a class="btn btn-sm btn-default" id="toggle_multi_select_all" data-id="domain" href="#"><span class="glyphicon glyphicon-check" aria-hidden="true"></span> <?=$lang['mailbox']['toggle_all'];?></a>
                 <a class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['quick_actions'];?> <span class="caret"></span></a>
                 <ul class="dropdown-menu">
-                  <li><a id="edit_selected" data-id="domain" data-api-url='edit/domain' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
-                  <li><a id="edit_selected" data-id="domain" data-api-url='edit/domain' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
-                  <li role="separator" class="divider"></li>
-                  <li><a id="delete_selected" data-id="domain" data-api-url='delete/domain' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                  <? if($_SESSION['mailcow_cc_role'] == "admin"): ?>
+                    <li><a id="edit_selected" data-id="domain" data-api-url='edit/domain' data-api-attr='{"active":"1"}' href="#"><?=$lang['mailbox']['activate'];?></a></li>
+                    <li><a id="edit_selected" data-id="domain" data-api-url='edit/domain' data-api-attr='{"active":"0"}' href="#"><?=$lang['mailbox']['deactivate'];?></a></li>
+                    <li role="separator" class="divider"></li>
+                    <li><a id="delete_selected" data-id="domain" data-api-url='delete/domain' href="#"><?=$lang['mailbox']['remove'];?></a></li>
+                  <? endif; ?>
                 </ul>
-                <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addDomainModal"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_domain'];?></a>
+                <? if($_SESSION['mailcow_cc_role'] == "admin"): ?>
+                  <a class="btn btn-sm btn-success" href="#" data-toggle="modal" data-target="#addDomainModal"><span class="glyphicon glyphicon-plus"></span> <?=$lang['mailbox']['add_domain'];?></a>
+                <? endif; ?>
               </div>
             </div>
           </div>

From c2d9928f8f075340c5c81a010bb9a33f61a9a676 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Fri, 10 Nov 2017 19:58:56 +0100
Subject: [PATCH 22/23] [Rspamd] Set task timeout to 12s

---
 data/conf/rspamd/override.d/worker-normal.inc | 1 +
 1 file changed, 1 insertion(+)

diff --git a/data/conf/rspamd/override.d/worker-normal.inc b/data/conf/rspamd/override.d/worker-normal.inc
index aac8fc17..71569636 100644
--- a/data/conf/rspamd/override.d/worker-normal.inc
+++ b/data/conf/rspamd/override.d/worker-normal.inc
@@ -1 +1,2 @@
 bind_socket = "*:11333";
+task_timeout = 12s;

From 84a7a1a2e7e0015018f23f2e10a7fc1c89711dca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9?= <andre.peters@debinux.de>
Date: Tue, 14 Nov 2017 10:44:00 +0100
Subject: [PATCH 23/23] [Compose] New images, Nginx checks for SOGo before
 bootstrapping [PHP-FPM] Some more modules (primarily for Horde) [Fail2ban] Do
 not log matches of local and private ips [Watchdog] Some changes in log
 system for further processing (wip) [ACME] Fixes #745

---
 data/Dockerfiles/acme/docker-entrypoint.sh | 11 ++--
 data/Dockerfiles/fail2ban/logwatch.py      |  3 ++
 data/Dockerfiles/phpfpm/Dockerfile         |  8 +--
 data/Dockerfiles/watchdog/watchdog.sh      | 58 ++++++++++------------
 docker-compose.yml                         |  9 ++--
 5 files changed, 45 insertions(+), 44 deletions(-)

diff --git a/data/Dockerfiles/acme/docker-entrypoint.sh b/data/Dockerfiles/acme/docker-entrypoint.sh
index 6d3d7273..a859684d 100755
--- a/data/Dockerfiles/acme/docker-entrypoint.sh
+++ b/data/Dockerfiles/acme/docker-entrypoint.sh
@@ -2,6 +2,12 @@
 set -o pipefail
 exec 5>&1
 
+if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+  log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
+  sleep 365d
+  exec $(readlink -f "$0")
+fi
+
 ACME_BASE=/var/lib/acme
 SSL_EXAMPLE=/var/lib/ssl-example
 
@@ -102,11 +108,6 @@ while ! mysqladmin ping --host mysql -u${DBUSER} -p${DBPASS} --silent; do
 done
 
 while true; do
-  if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-    log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
-    sleep 365d
-    exec $(readlink -f "$0")
-  fi
   if [[ "${SKIP_IP_CHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
     SKIP_IP_CHECK=y
   fi
diff --git a/data/Dockerfiles/fail2ban/logwatch.py b/data/Dockerfiles/fail2ban/logwatch.py
index f1954489..6be96023 100644
--- a/data/Dockerfiles/fail2ban/logwatch.py
+++ b/data/Dockerfiles/fail2ban/logwatch.py
@@ -147,6 +147,9 @@ def watch():
 					result = re.search(rule_regex, item['data'])
 					if result:
 						addr = result.group(1)
+            ip = ipaddress.ip_address(addr.decode('ascii'))
+            if ip.is_private or ip.is_loopback:
+              continue
 						print "%s matched rule id %d" % (addr, rule_id)
 						log['time'] = int(round(time.time()))
 						log['priority'] = "warn"
diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile
index 6a1e90a3..84a1f67b 100644
--- a/data/Dockerfiles/phpfpm/Dockerfile
+++ b/data/Dockerfiles/phpfpm/Dockerfile
@@ -32,6 +32,8 @@ RUN apk add -U --no-cache libxml2-dev \
   imagemagick-dev \
   imagemagick \
   libtool \
+  gettext-dev \
+  openldap-dev \
   librsvg \
   && pear install channel://pear.php.net/Net_IDNA2-0.2.0 \
     channel://pear.php.net/Auth_SASL-1.1.0 \
@@ -43,7 +45,7 @@ RUN apk add -U --no-cache libxml2-dev \
 	&& docker-php-ext-enable redis apcu memcached imagick \
 	&& pecl clear-cache \
 	&& docker-php-ext-configure intl \
-  && docker-php-ext-install -j 4 intl pdo pdo_mysql xmlrpc gd zip pcntl opcache \
+  && docker-php-ext-install -j 4 intl gettext ldap sockets soap pdo pdo_mysql xmlrpc gd zip pcntl opcache \
   && docker-php-ext-configure imap --with-imap --with-imap-ssl \
 	&& docker-php-ext-install -j 4 imap \
 	&& apk del --purge autoconf g++ make libxml2-dev icu-dev imap-dev openssl-dev cyrus-sasl-dev pcre-dev libpng-dev libpng-dev libjpeg-turbo-dev libwebp-dev zlib-dev imagemagick-dev \
@@ -55,9 +57,7 @@ RUN apk add -U --no-cache libxml2-dev \
   echo 'opcache.memory_consumption=128'; \
   echo 'opcache.save_comments=1'; \
   echo 'opcache.revalidate_freq=1'; \
-} > /usr/local/etc/php/conf.d/opcache-recommended.ini \
-  && rm -rf /usr/src/php*
-
+} > /usr/local/etc/php/conf.d/opcache-recommended.ini
 
 COPY ./docker-entrypoint.sh /
 
diff --git a/data/Dockerfiles/watchdog/watchdog.sh b/data/Dockerfiles/watchdog/watchdog.sh
index d7351936..902f66dc 100755
--- a/data/Dockerfiles/watchdog/watchdog.sh
+++ b/data/Dockerfiles/watchdog/watchdog.sh
@@ -28,13 +28,19 @@ progress() {
   [[ ${CURRENT} -gt ${TOTAL} ]] && return
   [[ ${CURRENT} -lt 0 ]] && CURRENT=0
   PERCENT=$(( 200 * ${CURRENT} / ${TOTAL} % 2 + 100 * ${CURRENT} / ${TOTAL} ))
-  echo -ne "$(date) - ${SERVICE} health level: \e[7m${PERCENT}%\e[0m (${CURRENT}/${TOTAL}), health trend: "
-  [[ ${DIFF} =~ ^-[1-9] ]] && echo -en '[\e[41m  \e[0m] ' || echo -en '[\e[42m  \e[0m] '
-  echo "(${DIFF})"
+  log_msg "${SERVICE} health level: ${PERCENT}% (${CURRENT}/${TOTAL}), health trend: ${DIFF}"
+  log_data "$(printf "%d,%d,%d,%d" ${PERCENT} ${CURRENT} ${TOTAL} ${DIFF})" "${SERVICE}"
 }
 
-log_to_redis() {
-	redis-cli -h redis LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}")\"}"
+log_msg() {
+	redis-cli -h redis LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}")\"}" > /dev/null
+  echo $(date) $(printf '%s\n' "${1}")
+}
+
+log_data() {
+  [[ -z ${1} ]] && return 1
+  [[ -z ${2} ]] && return 2
+	redis-cli -h redis LPUSH WATCHDOG_DATA "{\"time\":\"$(date +%s)\",\"service\":\"data\",\"$(printf '%s' "${2}")\":\"$(printf '%s' "${1}")\"}" > /dev/null
 }
 
 function mail_error() {
@@ -43,8 +49,7 @@ function mail_error() {
   RCPT_DOMAIN=$(echo ${1} | awk -F @ {'print $NF'})
   RCPT_MX=$(dig +short ${RCPT_DOMAIN} mx | sort -n | awk '{print $2; exit}')
   if [[ -z ${RCPT_MX} ]]; then
-    log_to_redis "Cannot determine MX for ${1}, skipping email notification..."
-    echo "Cannot determine MX for ${1}"
+    log_msg "Cannot determine MX for ${1}, skipping email notification..."
     return 1
   fi
   ./smtp-cli --missing-modules-ok \
@@ -54,6 +59,7 @@ function mail_error() {
     --from="watchdog@${MAILCOW_HOSTNAME}" \
     --server="${RCPT_MX}" \
     --hello-host=${MAILCOW_HOSTNAME}
+  log_msg "Sent notification email to ${1}"
 }
 
 
@@ -66,8 +72,8 @@ get_container_ip() {
     sleep 1
     CONTAINER_ID=$(curl --silent http://dockerapi:8080/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${1}\")) | .id")
     if [[ ! -z ${CONTAINER_ID} ]]; then
-    	CONTAINER_IP=$(curl --silent http://dockerapi:8080/containers/${CONTAINER_ID}/json | jq -r '.NetworkSettings.Networks[].IPAddress')
-	fi
+      CONTAINER_IP=$(curl --silent http://dockerapi:8080/containers/${CONTAINER_ID}/json | jq -r '.NetworkSettings.Networks[].IPAddress')
+    fi
     LOOP_C=$((LOOP_C + 1))
   done
   [[ ${LOOP_C} -gt 5 ]] && echo 240.0.0.0 || echo ${CONTAINER_IP}
@@ -253,9 +259,8 @@ dns_checks() {
 (
 while true; do
   if ! nginx_checks; then
-    log_to_redis "Nginx hit error limit"
+    log_msg "Nginx hit error limit"
     [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "nginx-mailcow"
-    echo -e "\e[31m$(date) - Nginx hit error limit\e[0m"
     echo nginx-mailcow > /tmp/com_pipe
   fi
 done
@@ -265,9 +270,8 @@ BACKGROUND_TASKS+=($!)
 (
 while true; do
   if ! mysql_checks; then
-    log_to_redis "MySQL hit error limit"
+    log_msg "MySQL hit error limit"
     [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "mysql-mailcow"
-    echo -e "\e[31m$(date) - MySQL hit error limit\e[0m"
     echo mysql-mailcow > /tmp/com_pipe
   fi
 done
@@ -277,9 +281,8 @@ BACKGROUND_TASKS+=($!)
 (
 while true; do
   if ! phpfpm_checks; then
-    log_to_redis "PHP-FPM hit error limit"
+    log_msg "PHP-FPM hit error limit"
     [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "php-fpm-mailcow"
-    echo -e "\e[31m$(date) - PHP-FPM hit error limit\e[0m"
     echo php-fpm-mailcow > /tmp/com_pipe
   fi
 done
@@ -289,9 +292,8 @@ BACKGROUND_TASKS+=($!)
 (
 while true; do
   if ! sogo_checks; then
-    log_to_redis "SOGo hit error limit"
+    log_msg "SOGo hit error limit"
     [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "sogo-mailcow"
-    echo -e "\e[31m$(date) - SOGo hit error limit\e[0m"
     echo sogo-mailcow > /tmp/com_pipe
   fi
 done
@@ -301,9 +303,8 @@ BACKGROUND_TASKS+=($!)
 (
 while true; do
   if ! postfix_checks; then
-    log_to_redis "Postfix hit error limit"
+    log_msg "Postfix hit error limit"
     [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "postfix-mailcow"
-    echo -e "\e[31m$(date) - Postfix hit error limit\e[0m"
     echo postfix-mailcow > /tmp/com_pipe
   fi
 done
@@ -313,9 +314,8 @@ BACKGROUND_TASKS+=($!)
 (
 while true; do
   if ! dovecot_checks; then
-    log_to_redis "Dovecot hit error limit"
+    log_msg "Dovecot hit error limit"
     [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "dovecot-mailcow"
-    echo -e "\e[31m$(date) - Dovecot hit error limit\e[0m"
     echo dovecot-mailcow > /tmp/com_pipe
   fi
 done
@@ -325,9 +325,8 @@ BACKGROUND_TASKS+=($!)
 (
 while true; do
   if ! dns_checks; then
-    log_to_redis "Unbound hit error limit"
+    log_msg "Unbound hit error limit"
     [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "unbound-mailcow"
-    echo -e "\e[31m$(date) - Unbound hit error limit\e[0m"
     #echo unbound-mailcow > /tmp/com_pipe
   fi
 done
@@ -337,9 +336,8 @@ BACKGROUND_TASKS+=($!)
 (
 while true; do
   if ! rspamd_checks; then
-    log_to_redis "Rspamd hit error limit"
+    log_msg "Rspamd hit error limit"
     [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${WATCHDOG_NOTIFY_EMAIL}" "rspamd-mailcow"
-    echo -e "\e[31m$(date) - Rspamd hit error limit\e[0m"
     echo rspamd-mailcow > /tmp/com_pipe
   fi
 done
@@ -351,8 +349,7 @@ BACKGROUND_TASKS+=($!)
 while true; do
   for bg_task in ${BACKGROUND_TASKS[*]}; do
     if ! kill -0 ${bg_task} 1>&2; then
-      echo "Worker ${bg_task} died, stopping watchdog and waiting for respawn..."
-      log_to_redis "Worker ${bg_task} died, stopping watchdog and waiting for respawn..."
+      log_msg "Worker ${bg_task} died, stopping watchdog and waiting for respawn..."
       kill -TERM 1
     fi
     sleep 10
@@ -366,7 +363,7 @@ while true; do
   while nc -z dockerapi 8080; do
     sleep 3
   done
-  echo "Cannot find dockerapi-mailcow, waiting to recover..."
+  log_msg "Cannot find dockerapi-mailcow, waiting to recover..."
   kill -STOP ${BACKGROUND_TASKS[*]}
   until nc -z dockerapi 8080; do
     sleep 3
@@ -385,11 +382,10 @@ while true; do
     sleep 3
     CONTAINER_ID=$(curl --silent http://dockerapi:8080/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | .id")
     if [[ ! -z ${CONTAINER_ID} ]]; then
-      log_to_redis "Sending restart command to ${CONTAINER_ID}..."
-      echo "Sending restart command to ${CONTAINER_ID}..."
+      log_msg "Sending restart command to ${CONTAINER_ID}..."
       curl --silent -XPOST http://dockerapi:8080/containers/${CONTAINER_ID}/restart
     fi
-    echo "Wait for restarted container to settle and continue watching..."
+    log_msg "Wait for restarted container to settle and continue watching..."
     sleep 30s
     kill -CONT ${BACKGROUND_TASKS[*]}
     kill -USR1 ${BACKGROUND_TASKS[*]}
diff --git a/docker-compose.yml b/docker-compose.yml
index e4b6e858..478f8718 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -92,7 +92,7 @@ services:
             - rspamd
 
     php-fpm-mailcow:
-      image: mailcow/phpfpm:1.4
+      image: mailcow/phpfpm:1.5
       build: ./data/Dockerfiles/phpfpm
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
@@ -224,6 +224,7 @@ services:
         envsubst < /etc/nginx/conf.d/templates/server_name.template > /etc/nginx/conf.d/server_name.active &&
         nginx -qt &&
         until ping phpfpm -c1 > /dev/null; do sleep 1; done &&
+        until ping sogo -c1 > /dev/null; do sleep 1; done &&
         until ping redis -c1 > /dev/null; do sleep 1; done &&
         exec nginx -g 'daemon off;'"
       environment:
@@ -251,7 +252,7 @@ services:
       depends_on:
         - nginx-mailcow
         - mysql-mailcow
-      image: mailcow/acme:1.22
+      image: mailcow/acme:1.23
       build: ./data/Dockerfiles/acme
       dns:
         - 172.22.1.254
@@ -274,7 +275,7 @@ services:
             - acme
 
     fail2ban-mailcow:
-      image: mailcow/fail2ban:1.7
+      image: mailcow/fail2ban:1.8
       build: ./data/Dockerfiles/fail2ban
       stop_grace_period: 30s
       depends_on:
@@ -295,7 +296,7 @@ services:
         - /lib/modules:/lib/modules:ro
 
     watchdog-mailcow:
-      image: mailcow/watchdog:1.9
+      image: mailcow/watchdog:1.10
       build: ./data/Dockerfiles/watchdog
       volumes:
         - vmail-vol-1:/vmail:ro