diff --git a/.gitignore b/.gitignore index 0d081fc4..b945f8cf 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,3 @@ data/conf/nginx/*.conf data/conf/nginx/*.custom data/conf/nginx/*.bak data/conf/dovecot/extra.conf -data/conf/rspamd/custom/* diff --git a/data/web/admin.php b/data/web/admin.php index 0d7c6466..e1fe74be 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -8,26 +8,8 @@ $tfa_data = get_tfa(); ?>
@@ -58,7 +40,7 @@ $tfa_data = get_tfa();
- +
@@ -96,6 +78,42 @@ $tfa_data = get_tfa();
+ +
+
API
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+
@@ -121,17 +139,15 @@ $tfa_data = get_tfa();
-
-
@@ -332,14 +347,13 @@ $tfa_data = get_tfa();
- +
-
@@ -381,6 +395,43 @@ $tfa_data = get_tfa();
+ +
+
Quarantäne
+
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ +
+ +
+
+
+
@@ -449,10 +500,29 @@ $tfa_data = get_tfa(); endforeach; ?> -
- - -
+

+ + +

+ + + +
+
+ + +
+
+ + +
+
+ + +
+
@@ -460,111 +530,6 @@ $tfa_data = get_tfa(); -
-
-
Postfix -
- - - -
-
-
-
-
-
-
-
-
- -
-
-
Dovecot -
- - - -
-
-
-
-
-
-
-
-
- -
-
-
SOGo -
- - - -
-
-
-
-
-
-
-
-
- - - -
-
-
Fail2ban -
- - - -
-
-
-
-
-
-
-
-
- - -
-
-
Rspamd history -
- - - -
-
-
-
-
-
-
-
-
- -
-
-
Autodiscover -
- - - -
-
-
-
-
-
-
-
-
- li{float:none}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px}.collapse.in{display:block!important}.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}} \ No newline at end of file diff --git a/data/web/css/debug.css b/data/web/css/debug.css new file mode 100644 index 00000000..585d1905 --- /dev/null +++ b/data/web/css/debug.css @@ -0,0 +1,37 @@ +table.footable>tbody>tr.footable-empty>td { + font-size:15px !important; + font-style:italic; +} +.pagination a { + text-decoration: none !important; +} +.panel panel-default { + overflow: visible !important; +} +.table-responsive { + overflow: visible !important; +} +@media screen and (max-width: 767px) { + .table-responsive { + overflow-x: scroll !important; + } +} +.footer-add-item { + display:block; + text-align: center; + font-style: italic; + padding: 10px; + background: #F5F5F5; +} +@media (min-width: 992px) { + .container { + width: 80%; + } +} +.mass-actions-debug { + user-select: none; + padding:10px 0 10px 10px; +} +.inputMissingAttr { + border-color: #FF4136; +} \ No newline at end of file diff --git a/data/web/css/quarantaine.css b/data/web/css/quarantaine.css new file mode 100644 index 00000000..7a5ee761 --- /dev/null +++ b/data/web/css/quarantaine.css @@ -0,0 +1,37 @@ +table.footable>tbody>tr.footable-empty>td { + font-size:15px !important; + font-style:italic; +} +.pagination a { + text-decoration: none !important; +} +.panel panel-default { + overflow: visible !important; +} +.table-responsive { + overflow: visible !important; +} +@media screen and (max-width: 767px) { + .table-responsive { + overflow-x: scroll !important; + } +} +.footer-add-item { + display:block; + text-align: center; + font-style: italic; + padding: 10px; + background: #F5F5F5; +} +@media (min-width: 992px) { + .container { + width: 80%; + } +} +.mass-actions-quarantaine { + user-select: none; + padding:10px 0 10px 10px; +} +.inputMissingAttr { + border-color: #FF4136; +} \ No newline at end of file diff --git a/data/web/debug.php b/data/web/debug.php new file mode 100644 index 00000000..289aa84a --- /dev/null +++ b/data/web/debug.php @@ -0,0 +1,328 @@ + +
+ + + +
+
+
+ +
+
+
+

Rspamd UI

+
+
+
+
+
+
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+
+
+
+ Rspamd UI +
+
+
+
+
+ +
+
+
+

Rspamd settings map

+
+
+ +
+
+
+ +
+
+
+

Container information

+
+
+
    + +
  • + + setTimestamp(mktime( + $StartedAt['hour'], + $StartedAt['minute'], + $StartedAt['second'], + $StartedAt['month'], + $StartedAt['day'], + $StartedAt['year'])); + $user_tz = new DateTimeZone(getenv('TZ')); + $date->setTimezone($user_tz); + $started = $date->format('r'); + ?> + (Started on ), + Restart +     +
  • + +
+
+
+
+ +
+
+
Postfix +
+ + + +
+
+
+
+
+
+
+
+
+ +
+
+
Dovecot +
+ + + +
+
+
+
+
+
+
+
+
+ +
+
+
SOGo +
+ + + +
+
+
+
+
+
+
+
+
+ +
+
+
Fail2ban +
+ + + +
+
+
+
+
+
+
+
+
+ +
+
+
Rspamd history +
+ + + +
+
+
+
+
+
+
+
+
+ +
+
+
Autodiscover +
+ + + +
+
+
+
+
+
+
+
+
+ +
+
+
Watchdog +
+ + + +
+
+
+
+
+
+
+
+
+ +
+
+
ACME +
+ + + +
+
+
+
+
+
+
+
+
+ +
+
+
API +
+ + + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ + + + + diff --git a/data/web/img/rspamd_logo.png b/data/web/img/rspamd_logo.png new file mode 100644 index 00000000..0e97426d Binary files /dev/null and b/data/web/img/rspamd_logo.png differ diff --git a/data/web/inc/ajax/container_ctrl.php b/data/web/inc/ajax/container_ctrl.php new file mode 100644 index 00000000..d12f5767 --- /dev/null +++ b/data/web/inc/ajax/container_ctrl.php @@ -0,0 +1,53 @@ +OK' : 'Error: ' . $response['msg'] . ''; + if ($response['type'] == "success") { + break; + } + usleep(1500000); + $retry++; + } + echo (!isset($last_response)) ? 'Already running' : $last_response; + } + if ($_GET['action'] == "stop") { + header('Content-Type: text/html; charset=utf-8'); + $retry = 0; + while (docker($_GET['service'], 'info')['State']['Running'] == 1 && $retry <= 3) { + $response = docker($_GET['service'], 'post', 'stop'); + $response = json_decode($response, true); + $last_response = ($response['type'] == "success") ? 'OK' : 'Error: ' . $response['msg'] . ''; + if ($response['type'] == "success") { + break; + } + usleep(1500000); + $retry++; + } + echo (!isset($last_response)) ? 'Not running' : $last_response; + } + if ($_GET['action'] == "restart") { + header('Content-Type: text/html; charset=utf-8'); + $response = docker($_GET['service'], 'post', 'restart'); + $response = json_decode($response, true); + $last_response = ($response['type'] == "success") ? 'OK' : 'Error: ' . $response['msg'] . ''; + echo (!isset($last_response)) ? 'Cannot restart container' : $last_response; + } + if ($_GET['action'] == "logs") { + $lines = (empty($_GET['lines']) || !is_numeric($_GET['lines'])) ? 1000 : $_GET['lines']; + header('Content-Type: text/plain; charset=utf-8'); + print_r(preg_split('/\n/', docker($_GET['service'], 'logs', $lines))); + } +} + +?> diff --git a/data/web/inc/ajax/log_driver.php b/data/web/inc/ajax/log_driver.php new file mode 100644 index 00000000..319f672d --- /dev/null +++ b/data/web/inc/ajax/log_driver.php @@ -0,0 +1,12 @@ + diff --git a/data/web/inc/ajax/qitem_details.php b/data/web/inc/ajax/qitem_details.php new file mode 100644 index 00000000..a4b80be1 --- /dev/null +++ b/data/web/inc/ajax/qitem_details.php @@ -0,0 +1,83 @@ + 10485760) { + echo json_encode(array('error' => 'Message size exceeds 10 MiB.')); + exit; + } + if (!empty($mailc['msg'])) { + // Init message array + $data = array(); + // Init parser + $mail_parser = new PhpMimeMailParser\Parser(); + // Load msg to parser + $mail_parser->setText($mailc['msg']); + // Get text/plain content + $data['text_plain'] = $mail_parser->getMessageBody('text'); + // Get subject + $data['subject'] = $mail_parser->getHeader('subject'); + // Get attachments + if (is_dir($tmpdir)) { + rrmdir($tmpdir); + } + mkdir('/tmp/' . $_GET['id']); + $mail_parser->saveAttachments($tmpdir, true); + $atts = $mail_parser->getAttachments(true); + if (count($atts) > 0) { + foreach ($atts as $key => $val) { + $data['attachments'][$key] = array( + // Index + // 0 => file name + // 1 => mime type + // 2 => file size + // 3 => vt link by sha256 + $val->getFilename(), + $val->getContentType(), + filesize($tmpdir . $val->getFilename()), + 'https://www.virustotal.com/file/' . hash_file('SHA256', $tmpdir . $val->getFilename()) . '/analysis/' + ); + } + } + if (isset($_GET['att'])) { + $dl_id = intval($_GET['att']); + $dl_filename = $data['attachments'][$dl_id][0]; + if (!is_dir($tmpdir . $dl_filename) && file_exists($tmpdir . $dl_filename)) { + header('Pragma: public'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Cache-Control: private', false); + header('Content-Type: ' . $data['attachments'][$dl_id][1]); + header('Content-Disposition: attachment; filename="'. $dl_filename . '";'); + header('Content-Transfer-Encoding: binary'); + header('Content-Length: ' . $data['attachments'][$dl_id][2]); + readfile($tmpdir . $dl_filename); + exit; + } + } + echo json_encode($data); + } +} +?> diff --git a/data/web/inc/ajax/sogo_ctrl.php b/data/web/inc/ajax/sogo_ctrl.php deleted file mode 100644 index e238d9c0..00000000 --- a/data/web/inc/ajax/sogo_ctrl.php +++ /dev/null @@ -1,39 +0,0 @@ -OK' : 'Error: ' . $response['msg'] . ''; - if ($response['type'] == "success") { - break; - } - usleep(1500000); - $retry++; - } - echo (!isset($last_response)) ? 'Already running' : $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") ? 'OK' : 'Error: ' . $response['msg'] . ''; - if ($response['type'] == "success") { - break; - } - usleep(1500000); - $retry++; - } - echo (!isset($last_response)) ? 'Not running' : $last_response; -} - -?> diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 3ba758be..a082211b 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'; + @@ -26,11 +27,19 @@ $(document).ready(function() { msg = $('').html(message).text(); if (type == 'danger') { auto_hide = 0; + $('#' + localStorage.getItem("add_modal")).modal('show'); + localStorage.removeItem("add_modal"); } else { auto_hide = 5000; } + $.ajax({ + url: '/inc/ajax/log_driver.php', + data: {"type": type,"msg": msg}, + type: "GET" + }); $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}}); } + $('[data-cached-form="true"]').formcache({key: $(this).data('id')}); mailcow_alert_box(, ""); @@ -118,13 +127,8 @@ $(document).ready(function() { } }); - // Activate tooltips $(function () { $('[data-toggle="tooltip"]').tooltip() - }) - // Hide alerts after n seconds - $("#alert-fade").fadeTo(7000, 500).slideUp(500, function(){ - $("#alert-fade").alert('close'); }); // Remember last navigation pill @@ -173,36 +177,32 @@ $(document).ready(function() { // Init Bootstrap Selectpicker $('select').selectpicker(); - // Trigger SOGo restart - $('#triggerRestartSogo').click(function(){ - $(this).prop("disabled",true); - $(this).html(' '); - $('#statusTriggerRestartSogo').text('Stopping SOGo workers, this may take a while... '); - $.ajax({ - method: 'get', - url: '/inc/ajax/sogo_ctrl.php', - data: { - 'ajax': true, - 'ACTION': 'stop' - }, - success: function(data) { - $('#statusTriggerRestartSogo').append(data); - $('#statusTriggerRestartSogo').append('
Starting SOGo...'); - $.ajax({ - method: 'get', - url: '/inc/ajax/sogo_ctrl.php', - data: { - 'ajax': true, - 'ACTION': 'start' - }, - success: function(data) { - $('#statusTriggerRestartSogo').append(data); - $('#triggerRestartSogo').html(' '); - } - }); - } + // Trigger container restart + $('#RestartContainer').on('show.bs.modal', function(e) { + var container = $(e.relatedTarget).data('container'); + $('#containerName').text(container); + $('#triggerRestartContainer').click(function(){ + $(this).prop("disabled",true); + $(this).html(' '); + $('#statusTriggerRestartContainer').text('Restarting container, this may take a while... '); + $.ajax({ + method: 'get', + url: '/inc/ajax/container_ctrl.php', + timeout: 3000, + data: { + 'service': container, + 'action': 'restart' + }, + error: function() { + window.location = window.location.href.split("#")[0]; + }, + success: function(data) { + $('#statusTriggerRestartContainer').append(data); + $('#triggerRestartContainer').html(' '); + } + }); }); - }); + }) // CSRF $('').attr('id', 'csrf_token').attr('name', 'csrf_token').appendTo('form'); @@ -216,4 +216,4 @@ $(document).ready(function() { 'danger', - 'msg' => 'Cannot validate image file: Temporary file not found' + 'msg' => $lang['danger']['img_tmp_missing'] ); return false; } @@ -26,7 +26,7 @@ function customize($_action, $_item, $_data = null) { if ($image->valid() !== true) { $_SESSION['return'] = array( 'type' => 'danger', - 'msg' => 'Cannot validate image file' + 'msg' => $lang['danger']['img_invalid'] ); return false; } @@ -35,7 +35,7 @@ function customize($_action, $_item, $_data = null) { catch (ImagickException $e) { $_SESSION['return'] = array( 'type' => 'danger', - 'msg' => 'Cannot validate image file' + 'msg' => $lang['danger']['img_invalid'] ); return false; } @@ -43,7 +43,7 @@ function customize($_action, $_item, $_data = null) { else { $_SESSION['return'] = array( 'type' => 'danger', - 'msg' => 'Invalid mime type' + 'msg' => $lang['danger']['invalid_mime_type'] ); return false; } @@ -59,7 +59,7 @@ function customize($_action, $_item, $_data = null) { } $_SESSION['return'] = array( 'type' => 'success', - 'msg' => 'File uploaded successfully' + 'msg' => $lang['success']['upload_success'] ); break; } @@ -77,7 +77,7 @@ function customize($_action, $_item, $_data = null) { $apps = (array)$_data['app']; $links = (array)$_data['href']; $out = array(); - if (count($apps) == count($links)) {; + if (count($apps) == count($links)) { for ($i = 0; $i < count($apps); $i++) { $out[] = array($apps[$i] => $links[$i]); } @@ -94,7 +94,28 @@ function customize($_action, $_item, $_data = null) { } $_SESSION['return'] = array( 'type' => 'success', - 'msg' => 'Saved changes to app links' + 'msg' => $lang['success']['app_links'] + ); + break; + case 'ui_texts': + $main_name = $_data['main_name']; + $apps_name = $_data['apps_name']; + $help_text = $_data['help_text']; + try { + $redis->set('MAIN_NAME', htmlspecialchars($main_name)); + $redis->set('APPS_NAME', htmlspecialchars($apps_name)); + $redis->set('HELP_TEXT', $help_text); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => $lang['success']['ui_texts'] ); break; } @@ -113,7 +134,7 @@ function customize($_action, $_item, $_data = null) { if ($redis->del('MAIN_LOGO')) { $_SESSION['return'] = array( 'type' => 'success', - 'msg' => 'Reset default logo' + 'msg' => $lang['success']['reset_main_logo'] ); return true; } @@ -155,6 +176,21 @@ function customize($_action, $_item, $_data = null) { return false; } break; + case 'ui_texts': + try { + $data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : 'mailcow UI'; + $data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : 'mailcow Apps'; + $data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false; + return $data; + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + break; case 'main_logo_specs': try { $image = new Imagick(); @@ -167,7 +203,7 @@ function customize($_action, $_item, $_data = null) { catch (ImagickException $e) { $_SESSION['return'] = array( 'type' => 'danger', - 'msg' => 'Error: Imagick exception while reading image' + 'msg' => $lang['danger']['imagick_exception'] ); return false; } diff --git a/data/web/inc/functions.docker.inc.php b/data/web/inc/functions.docker.inc.php index a5f2581c..7cd5ed4e 100644 --- a/data/web/inc/functions.docker.inc.php +++ b/data/web/inc/functions.docker.inc.php @@ -1,5 +1,12 @@ 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } $curl = curl_init(); curl_setopt($curl, CURLOPT_HTTPHEADER,array( 'Content-Type: application/json' )); switch($action) { @@ -52,14 +59,44 @@ function docker($service_name, $action, $post_action = null, $post_fields = null return false; } break; + case 'logs': + $container_id = docker($service_name, 'get_id'); + if (ctype_xdigit($container_id)) { + $lines = (empty($attr1) || !is_numeric($attr1)) ? 100 : $attr1; + curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/' . $container_id . '/logs/' . $lines); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_POST, 0); + $response = curl_exec($curl); + if ($response === false) { + $err = curl_error($curl); + curl_close($curl); + return $err; + } + else { + curl_close($curl); + if (empty($response)) { + return true; + } + else { + return json_decode($response, true); + } + } + } + else { + return false; + } + break; case 'post': - if (!empty($post_action)) { + if (!empty($attr1)) { $container_id = docker($service_name, 'get_id'); - if (ctype_xdigit($container_id) && ctype_alnum($post_action)) { - curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/' . $container_id . '/' . $post_action); + if (ctype_xdigit($container_id) && ctype_alnum($attr1)) { + curl_setopt($curl, CURLOPT_URL, 'http://dockerapi:8080/containers/' . $container_id . '/' . $attr1); curl_setopt($curl, CURLOPT_POST, 1); - if (!empty($post_fields)) { - curl_setopt( $curl, CURLOPT_POSTFIELDS, json_encode($post_fields)); + if (!empty($attr2)) { + curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($attr2)); + } + if (!empty($extra_headers) && is_array($extra_headers)) { + curl_setopt($curl, CURLOPT_HTTPHEADER, $extra_headers); } curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); $response = curl_exec($curl); diff --git a/data/web/inc/functions.fail2ban.inc.php b/data/web/inc/functions.fail2ban.inc.php index 6c9e1692..e1801be7 100644 --- a/data/web/inc/functions.fail2ban.inc.php +++ b/data/web/inc/functions.fail2ban.inc.php @@ -1,5 +1,4 @@ prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '✘') AS `aliases` FROM `alias` + $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '✘') AS `shared_aliases` FROM `alias` WHERE `goto` REGEXP :username_goto AND `address` NOT LIKE '@%' + AND `goto` != :username_goto2 AND `address` != :username_address"); - $stmt->execute(array(':username_goto' => '(^|,)'.$username.'($|,)', ':username_address' => $username)); + $stmt->execute(array( + ':username_goto' => '(^|,)'.$username.'($|,)', + ':username_goto2' => $username, + ':username_address' => $username + )); $run = $stmt->fetchAll(PDO::FETCH_ASSOC); while ($row = array_shift($run)) { - $data['aliases'] = $row['aliases']; + $data['shared_aliases'] = $row['shared_aliases']; + } + $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '✘') AS `direct_aliases` FROM `alias` + WHERE `goto` = :username_goto + AND `address` != :username_address"); + $stmt->execute( + array( + ':username_goto' => $username, + ':username_address' => $username + )); + $run = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($run)) { + $data['direct_aliases'] = $row['direct_aliases']; } $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ', '), '✘') AS `ad_alias` FROM `mailbox` LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain` @@ -851,6 +868,135 @@ function verify_tfa_login($username, $token) { } return false; } +function admin_api($action, $data = null) { + global $pdo; + global $lang; + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + switch ($action) { + case "edit": + $regen_key = $data['admin_api_regen_key']; + $active = (isset($data['active'])) ? 1 : 0; + $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from'])); + foreach ($allow_from as $key => $val) { + if (!filter_var($val, FILTER_VALIDATE_IP)) { + unset($allow_from[$key]); + continue; + } + } + $allow_from = implode(',', array_unique(array_filter($allow_from))); + if (empty($allow_from)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'List of allowed IPs cannot be empty' + ); + return false; + } + $api_key = implode('-', array( + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))) + )); + $stmt = $pdo->prepare("INSERT INTO `api` (`username`, `api_key`, `active`, `allow_from`) + SELECT `username`, :api_key, :active, :allow_from FROM `admin` WHERE `superadmin`='1' AND `active`='1' + ON DUPLICATE KEY UPDATE `active` = :active_u, `allow_from` = :allow_from_u ;"); + $stmt->execute(array( + ':api_key' => $api_key, + ':active' => $active, + ':active_u' => $active, + ':allow_from' => $allow_from, + ':allow_from_u' => $allow_from + )); + break; + case "regen_key": + $api_key = implode('-', array( + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))) + )); + $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `username` IN + (SELECT `username` FROM `admin` WHERE `superadmin`='1' AND `active`='1')"); + $stmt->execute(array( + ':api_key' => $api_key + )); + break; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['admin_modified']) + ); +} +function rspamd_ui($action, $data = null) { + global $lang; + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + switch ($action) { + case "edit": + $rspamd_ui_pass = $data['rspamd_ui_pass']; + $rspamd_ui_pass2 = $data['rspamd_ui_pass2']; + if (empty($rspamd_ui_pass) || empty($rspamd_ui_pass2)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Password cannot be empty' + ); + return false; + } + if ($rspamd_ui_pass != $rspamd_ui_pass2) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Passwords do not match' + ); + return false; + } + if (strlen($rspamd_ui_pass) < 6) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Please use at least 6 characters for your password' + ); + return false; + } + $docker_return = docker('rspamd-mailcow', 'post', 'exec', array('cmd' => 'worker_password', 'raw' => $rspamd_ui_pass), array('Content-Type: application/json')); + if ($docker_return_array = json_decode($docker_return, true)) { + if ($docker_return_array['type'] == 'success') { + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'Rspamd UI password set successfully' + ); + return true; + } + else { + $_SESSION['return'] = array( + 'type' => $docker_return_array['type'], + 'msg' => $docker_return_array['msg'] + ); + return false; + } + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Unknown error' + ); + return false; + } + break; + } + +} function get_admin_details() { // No parameter to be given, only one admin should exist global $pdo; @@ -860,8 +1006,10 @@ function get_admin_details() { return false; } try { - $stmt = $pdo->prepare("SELECT `username`, `modified`, `created` FROM `admin` WHERE `superadmin`='1' AND active='1'"); - $stmt->execute(); + $stmt = $pdo->query("SELECT `admin`.`username`, `api`.`active` AS `api_active`, `api`.`api_key`, `api`.`allow_from` FROM `admin` + INNER JOIN `api` ON `admin`.`username` = `api`.`username` + WHERE `admin`.`superadmin`='1' + AND `admin`.`active`='1'"); $data = $stmt->fetch(PDO::FETCH_ASSOC); } catch(PDOException $e) { @@ -932,6 +1080,51 @@ function get_logs($container, $lines = false) { return $data_array; } } + if ($container == "watchdog-mailcow") { + if (!is_numeric($lines)) { + list ($from, $to) = explode('-', $lines); + $data = $redis->lRange('WATCHDOG_LOG', intval($from), intval($to)); + } + else { + $data = $redis->lRange('WATCHDOG_LOG', 0, intval($lines)); + } + if ($data) { + foreach ($data as $json_line) { + $data_array[] = json_decode($json_line, true); + } + return $data_array; + } + } + if ($container == "acme-mailcow") { + if (!is_numeric($lines)) { + list ($from, $to) = explode('-', $lines); + $data = $redis->lRange('ACME_LOG', intval($from), intval($to)); + } + else { + $data = $redis->lRange('ACME_LOG', 0, intval($lines)); + } + if ($data) { + foreach ($data as $json_line) { + $data_array[] = json_decode($json_line, true); + } + return $data_array; + } + } + if ($container == "api-mailcow") { + if (!is_numeric($lines)) { + list ($from, $to) = explode('-', $lines); + $data = $redis->lRange('API_LOG', intval($from), intval($to)); + } + else { + $data = $redis->lRange('API_LOG', 0, intval($lines)); + } + if ($data) { + foreach ($data as $json_line) { + $data_array[] = json_decode($json_line, true); + } + return $data_array; + } + } if ($container == "fail2ban-mailcow") { if (!is_numeric($lines)) { list ($from, $to) = explode('-', $lines); diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index d1410f97..054a9499 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -490,9 +490,20 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { if (in_array($address, $gotos)) { continue; } + $domain = idn_to_ascii(substr(strstr($address, '@'), 1)); + $local_part = strstr($address, '@', true); + $address = $local_part.'@'.$domain; $stmt = $pdo->prepare("SELECT `address` FROM `alias` - WHERE `address`= :address"); - $stmt->execute(array(':address' => $address)); + WHERE `address`= :address OR `address` IN ( + SELECT `username` FROM `mailbox`, `alias_domain` + WHERE ( + `alias_domain`.`alias_domain` = :address_d + AND `mailbox`.`username` = CONCAT(:address_l, '@', alias_domain.target_domain)))"); + $stmt->execute(array( + ':address' => $address, + ':address_l' => $local_part, + ':address_d' => $domain + )); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); if ($num_results != 0) { $_SESSION['return'] = array( @@ -501,9 +512,6 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { ); return false; } - $domain = idn_to_ascii(substr(strstr($address, '@'), 1)); - $local_part = strstr($address, '@', true); - $address = $local_part.'@'.$domain; $domaindata = mailbox('get', 'domain_details', $domain); if (is_array($domaindata) && $domaindata['aliases_left'] == "0") { $_SESSION['return'] = array( @@ -722,7 +730,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { } $active = intval($_data['active']); $quota_b = ($quota_m * 1048576); - $maildir = $domain . "/" . $local_part . "/mail-" . time() . "/"; + $maildir = $domain . "/" . $local_part . "/mails/"; if (!is_valid_domain_name($domain)) { $_SESSION['return'] = array( 'type' => 'danger', @@ -2302,7 +2310,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { } else { try { - $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role"); + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND (`domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role)"); $stmt->execute(array( ':username' => $_SESSION['mailcow_cc_username'], ':role' => $_SESSION['mailcow_cc_role'], @@ -3360,7 +3368,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { )); $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :domain"); $stmt->execute(array( - ':domain' => '%@'.$domain, + ':domain' => $domain, )); } catch (PDOException $e) { @@ -3484,7 +3492,7 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { )); $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :alias_domain"); $stmt->execute(array( - ':domain' => '%@'.$alias_domain, + ':domain' => $alias_domain, )); } catch (PDOException $e) { diff --git a/data/web/inc/functions.quarantaine.inc.php b/data/web/inc/functions.quarantaine.inc.php new file mode 100644 index 00000000..4b9e6b00 --- /dev/null +++ b/data/web/inc/functions.quarantaine.inc.php @@ -0,0 +1,282 @@ + 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + foreach ($ids as $id) { + if (!is_numeric($id)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + try { + $stmt = $pdo->prepare('SELECT `rcpt` FROM `quarantaine` WHERE `id` = :id'); + $stmt->execute(array(':id' => $id)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt'])) { + try { + $stmt = $pdo->prepare("DELETE FROM `quarantaine` WHERE `id` = :id"); + $stmt->execute(array( + ':id' => $id + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['items_deleted'], implode(', ', $ids)) + ); + break; + case 'edit': + if (!isset($_SESSION['acl']['quarantaine']) || $_SESSION['acl']['quarantaine'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + // Edit settings + if ($_data['action'] == 'settings') { + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $retention_size = $_data['retention_size']; + $max_size = $_data['max_size']; + $exclude_domains = (array)$_data['exclude_domains']; + try { + $redis->Set('Q_RETENTION_SIZE', intval($retention_size)); + $redis->Set('Q_MAX_SIZE', intval($max_size)); + $redis->Set('Q_EXCLUDE_DOMAINS', json_encode($exclude_domains)); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'Saved settings' + ); + } + // Release item + elseif ($_data['action'] == 'release') { + if (!is_array($_data['id'])) { + $ids = array(); + $ids[] = $_data['id']; + } + else { + $ids = $_data['id']; + } + foreach ($ids as $id) { + if (!is_numeric($id)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + try { + $stmt = $pdo->prepare('SELECT `msg`, `qid`, `sender`, `rcpt` FROM `quarantaine` WHERE `id` = :id'); + $stmt->execute(array(':id' => $id)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt'])) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + $sender = (isset($row['sender'])) ? $row['sender'] : 'sender-unknown@rspamd'; + try { + $mail = new PHPMailer(true); + $mail->isSMTP(); + $mail->SMTPDebug = 0; + $mail->SMTPOptions = array( + 'ssl' => array( + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true + ) + ); + if (!empty(gethostbynamel('postfix-mailcow'))) { + $postfix = 'apostfix-mailcow'; + } + if (!empty(gethostbynamel('postfix'))) { + $postfix = 'postfix'; + } + else { + $_SESSION['return'] = array( + 'type' => 'warning', + 'msg' => sprintf($lang['danger']['release_send_failed'], 'Cannot determine Postfix host') + ); + return false; + } + $mail->Host = $postfix; + $mail->Port = 590; + $mail->setFrom($sender); + $mail->CharSet = 'UTF-8'; + $mail->Subject = sprintf($lang['quarantaine']['release_subject'], $row['qid']); + $mail->addAddress($row['rcpt']); + $mail->IsHTML(false); + $msg_tmpf = tempnam("/tmp", $row['qid']); + file_put_contents($msg_tmpf, $row['msg']); + $mail->addAttachment($msg_tmpf, $row['qid'] . '.eml'); + $mail->Body = sprintf($lang['quarantaine']['release_body']); + $mail->send(); + unlink($msg_tmpf); + } + catch (phpmailerException $e) { + unlink($msg_tmpf); + $_SESSION['return'] = array( + 'type' => 'warning', + 'msg' => sprintf($lang['danger']['release_send_failed'], $e->errorMessage()) + ); + return false; + } + try { + $stmt = $pdo->prepare("DELETE FROM `quarantaine` 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' => $lang['success']['items_released'] + ); + } + return true; + break; + case 'get': + try { + if ($_SESSION['mailcow_cc_role'] == "user") { + $stmt = $pdo->prepare('SELECT `id`, `qid`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantaine` WHERE `rcpt` = :mbox'); + $stmt->execute(array(':mbox' => $_SESSION['mailcow_cc_username'])); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $q_meta[] = $row; + } + } + else { + foreach (mailbox('get', 'mailboxes') as $mbox) { + $stmt = $pdo->prepare('SELECT `id`, `qid`, `rcpt`, `sender`, UNIX_TIMESTAMP(`created`) AS `created` FROM `quarantaine` WHERE `rcpt` = :mbox'); + $stmt->execute(array(':mbox' => $mbox)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $q_meta[] = $row; + } + } + } + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + return $q_meta; + break; + case 'settings': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + try { + $settings['exclude_domains'] = json_decode($redis->Get('Q_EXCLUDE_DOMAINS'), true); + $settings['max_size'] = $redis->Get('Q_MAX_SIZE'); + $settings['retention_size'] = $redis->Get('Q_RETENTION_SIZE'); + } + catch (RedisException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; + } + return $settings; + break; + case 'details': + if (!is_numeric($_data) || empty($_data)) { + return false; + } + try { + $stmt = $pdo->prepare('SELECT `rcpt`, `symbols`, `msg`, `domain` FROM `quarantaine` WHERE `id`= :id'); + $stmt->execute(array(':id' => $_data)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['rcpt'])) { + return $row; + } + return false; + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + } + return false; + break; + } +} \ No newline at end of file diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index cedd07ca..c8dbe4b1 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -15,6 +15,7 @@ + @@ -27,6 +28,8 @@ ' : null; ?> ' : null; ?> ' : null; ?> +' : null; ?> +' : null; ?> @@ -35,7 +38,6 @@
diff --git a/data/web/modals/admin.php b/data/web/modals/admin.php index bf17296c..3a387540 100644 --- a/data/web/modals/admin.php +++ b/data/web/modals/admin.php @@ -13,7 +13,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {