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', "✓"); +define('state_missing', "✗"); +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', "✓"); +define('state_missing', "✗"); +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', "✓"); -define('state_missing', "✗"); -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 = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/', - '`': '`', - '=': '=' - }; - 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={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="}; + 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(",​"); - 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(",​"); + 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