From ade4b9e7ae7e28535ed261535526bc4e98ff48b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= Date: Sun, 19 Nov 2017 15:13:43 +0100 Subject: [PATCH 01/40] [Postfix, Web] Feature: BCC maps --- data/Dockerfiles/postfix/postfix.sh | 22 ++ data/conf/postfix/main.cf | 29 ++- data/web/admin.php | 10 +- data/web/edit.php | 52 +++- data/web/inc/functions.bcc.inc.php | 292 +++++++++++++++++++++++ data/web/inc/functions.customize.inc.php | 1 - data/web/inc/functions.fail2ban.inc.php | 4 +- data/web/inc/functions.mailbox.inc.php | 22 +- data/web/inc/init_db.inc.php | 24 +- data/web/inc/prerequisites.inc.php | 1 + data/web/inc/vars.inc.php | 3 + data/web/js/mailbox.js | 52 +++- data/web/json_api.php | 156 +++++++++++- data/web/mailbox.php | 29 +++ data/web/modals/mailbox.php | 71 ++++++ docker-compose.yml | 2 +- 16 files changed, 748 insertions(+), 22 deletions(-) create mode 100644 data/web/inc/functions.bcc.inc.php diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index 326a6d18..5e60bb18 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -119,6 +119,28 @@ query = SELECT goto FROM alias AND active='1'; EOF +cat < /opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT bcc_dest FROM bcc_maps + WHERE local_dest='%s' + AND type='rcpt' + AND active='1'; +EOF + +cat < /opt/postfix/conf/sql/mysql_sender_bcc_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT bcc_dest FROM bcc_maps + WHERE local_dest='%s' + AND type='sender' + AND active='1'; +EOF + cat < /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf user = ${DBUSER} password = ${DBPASS} diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index edf5c6d5..4e8c577f 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -39,7 +39,27 @@ postscreen_greet_ttl = 2d postscreen_greet_wait = 3s postscreen_non_smtp_command_enable = no postscreen_pipelining_enable = no -proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, $local_recipient_maps $mydestination $virtual_alias_maps $virtual_alias_domains $virtual_mailbox_maps $virtual_mailbox_domains $relay_recipient_maps $relay_domains $canonical_maps $sender_canonical_maps $recipient_canonical_maps $relocated_maps $transport_maps $mynetworks $smtpd_sender_login_maps +proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_sender_dependent_default_transport_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf, + $local_recipient_maps, + $mydestination, + $virtual_alias_maps, + $virtual_alias_domains, + $virtual_mailbox_maps, + $virtual_mailbox_domains, + $relay_recipient_maps, + $relay_domains, + $canonical_maps, + $sender_canonical_maps, + $recipient_canonical_maps, + $relocated_maps, + $transport_maps, + $mynetworks, + $smtpd_sender_login_maps queue_run_delay = 300s relay_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf relay_recipient_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf @@ -79,10 +99,15 @@ smtpd_tls_mandatory_ciphers = high smtpd_tls_security_level = may tls_ssl_options = NO_COMPRESSION tls_high_cipherlist = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA -virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf +virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf, + proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf virtual_gid_maps = static:5000 virtual_mailbox_base = /var/vmail/ virtual_mailbox_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf +recipient_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_recipient_bcc_maps.cf +sender_bcc_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sender_bcc_maps.cf virtual_mailbox_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf virtual_minimum_uid = 104 virtual_transport = lmtp:inet:dovecot:24 diff --git a/data/web/admin.php b/data/web/admin.php index 06af3db9..0d7c6466 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -7,7 +7,6 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $tfa_data = get_tfa(); ?>
- @@ -127,7 +128,9 @@ $tfa_data = get_tfa();
+ + Relayhosts @@ -307,6 +310,7 @@ $tfa_data = get_tfa();
+
@@ -335,6 +339,7 @@ $tfa_data = get_tfa();
+
@@ -506,6 +511,8 @@ $tfa_data = get_tfa();
+ +
Fail2ban @@ -522,6 +529,7 @@ $tfa_data = get_tfa();
+
diff --git a/data/web/edit.php b/data/web/edit.php index e06a0229..11c924b5 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -608,6 +608,52 @@ if (isset($_SESSION['mailcow_cc_role'])) { +

BCC map

+
+
+ +
+ +
+ + BCC destinations can only be valid email addresses. Separated by whitespace, semicolon, new line or comma. +
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ + +
'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $local_dest = strtolower(trim($_data['local_dest'])); + $bcc_dest = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['bcc_dest'])); + $active = intval($_data['active']); + $type = $_data['type']; + if ($type != 'sender' && $type != 'rcpt') { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Invalid BCC map type' + ); + return false; + } + if (empty($bcc_dest)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'BCC destination cannot be empty' + ); + return false; + } + if (is_valid_domain_name($local_dest)) { + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $local_dest)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $domain = idn_to_ascii($local_dest); + $local_dest_sane = '@' . idn_to_ascii($local_dest); + } + elseif (filter_var($local_dest, FILTER_VALIDATE_EMAIL)) { + if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $local_dest)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $domain = mailbox('get', 'mailbox_details', $local_dest)['domain']; + if (empty($domain)) { + return false; + } + $local_dest_sane = $local_dest; + } + else { + return false; + } + foreach ($bcc_dest as &$bcc_dest_e) { + if (!filter_var($bcc_dest_e, FILTER_VALIDATE_EMAIL)) { + $bcc_dest_e = null;; + } + $bcc_dest_e = strtolower($bcc_dest_e); + } + $bcc_dest = array_filter($bcc_dest); + $bcc_dest = implode(",", $bcc_dest); + if (empty($bcc_dest)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'BCC map destination cannot be empty' + ); + return false; + } + try { + $stmt = $pdo->prepare("SELECT `id` FROM `bcc_maps` + WHERE `local_dest` = :local_dest AND `type` = :type"); + $stmt->execute(array(':local_dest' => $local_dest_sane, ':type' => $type)); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + if ($num_results != 0) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'A BCC map entry "' . htmlspecialchars($local_dest_sane) . '" exists for type "' . $type . '"' + ); + return false; + } + try { + $stmt = $pdo->prepare("INSERT INTO `bcc_maps` (`local_dest`, `bcc_dest`, `domain`, `active`, `type`) VALUES + (:local_dest, :bcc_dest, :domain, :active, :type)"); + $stmt->execute(array( + ':local_dest' => $local_dest_sane, + ':bcc_dest' => $bcc_dest, + ':domain' => $domain, + ':active' => $active, + ':type' => $type + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'BCC map entry saved' + ); + break; + case 'edit': + if (!isset($_SESSION['acl']['bcc_maps']) || $_SESSION['acl']['bcc_maps'] != "1" ) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $ids = (array)$_data['id']; + foreach ($ids as $id) { + $is_now = bcc('details', $id); + if (!empty($is_now)) { + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active_int']; + $bcc_dest = (!empty($_data['bcc_dest'])) ? $_data['bcc_dest'] : $is_now['bcc_dest']; + $local_dest = $is_now['local_dest']; + $type = (!empty($_data['type'])) ? $_data['type'] : $is_now['type']; + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $bcc_dest = array_map('trim', preg_split( "/( |,|;|\n)/", $bcc_dest)); + $active = intval($_data['active']); + foreach ($bcc_dest as &$bcc_dest_e) { + if (!filter_var($bcc_dest_e, FILTER_VALIDATE_EMAIL)) { + $bcc_dest_e = null;; + } + $bcc_dest_e = strtolower($bcc_dest_e); + } + $bcc_dest = array_filter($bcc_dest); + $bcc_dest = implode(",", $bcc_dest); + if (empty($bcc_dest)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'BCC map destination cannot be empty' + ); + return false; + } + try { + $stmt = $pdo->prepare("SELECT `id` FROM `bcc_maps` + WHERE `local_dest` = :local_dest AND `type` = :type"); + $stmt->execute(array(':local_dest' => $local_dest, ':type' => $type)); + $id_now = $stmt->fetch(PDO::FETCH_ASSOC)['id']; + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + if (isset($id_now) && $id_now != $id) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'A BCC map entry ' . htmlspecialchars($local_dest) . ' exists for this type' + ); + return false; + } + try { + $stmt = $pdo->prepare("UPDATE `bcc_maps` SET `bcc_dest` = :bcc_dest, `active` = :active, `type` = :type WHERE `id`= :id"); + $stmt->execute(array( + ':bcc_dest' => $bcc_dest, + ':active' => $active, + ':type' => $type, + ':id' => $id + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'BCC map entry edited' + ); + break; + case 'details': + $bccdata = array(); + $id = intval($_data); + try { + $stmt = $pdo->prepare("SELECT `id`, + `local_dest`, + `bcc_dest`, + `active` AS `active_int`, + CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`, + `type`, + `created`, + `domain`, + `modified` FROM `bcc_maps` + WHERE `id` = :id"); + $stmt->execute(array(':id' => $id)); + $bccdata = $stmt->fetch(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $bccdata['domain'])) { + $bccdata = null; + return false; + } + return $bccdata; + break; + case 'get': + $bccdata = array(); + $all_items = array(); + $id = intval($_data); + try { + $stmt = $pdo->query("SELECT `id`, `domain` FROM `bcc_maps`"); + $all_items = $stmt->fetchAll(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + foreach ($all_items as $i) { + if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $i['domain'])) { + $bccdata[] = $i['id']; + } + } + $all_items = null; + return $bccdata; + break; + case 'delete': + $ids = (array)$_data['id']; + foreach ($ids as $id) { + if (!is_numeric($id)) { + return false; + } + try { + $stmt = $pdo->prepare("SELECT `domain` FROM `bcc_maps` WHERE id = :id"); + $stmt->execute(array(':id' => $id)); + $domain = $stmt->fetch(PDO::FETCH_ASSOC)['domain']; + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $stmt = $pdo->prepare("DELETE FROM `bcc_maps` 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 BCC map id/s ' . implode(', ', $ids) + ); + return true; + break; + } +} \ No newline at end of file diff --git a/data/web/inc/functions.customize.inc.php b/data/web/inc/functions.customize.inc.php index 167647fc..7e8f7b03 100644 --- a/data/web/inc/functions.customize.inc.php +++ b/data/web/inc/functions.customize.inc.php @@ -1,5 +1,4 @@ 'danger', @@ -3103,10 +3103,6 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { } foreach ($ids as $id) { if (!is_numeric($id)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => $id - ); return false; } try { @@ -3154,10 +3150,6 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { } foreach ($ids as $id) { if (!is_numeric($id)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => $id - ); return false; } try { @@ -3366,6 +3358,10 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { $stmt->execute(array( ':domain' => '%@'.$domain, )); + $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :domain"); + $stmt->execute(array( + ':domain' => '%@'.$domain, + )); } catch (PDOException $e) { $_SESSION['return'] = array( @@ -3486,6 +3482,10 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { $stmt->execute(array( ':alias_domain' => $alias_domain, )); + $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :alias_domain"); + $stmt->execute(array( + ':domain' => '%@'.$alias_domain, + )); } catch (PDOException $e) { $_SESSION['return'] = array( @@ -3580,6 +3580,10 @@ function mailbox($_action, $_type, $_data = null, $attr = null) { $stmt->execute(array( ':username' => $username )); + $stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :username"); + $stmt->execute(array( + ':username' => $username + )); $stmt = $pdo->prepare("SELECT `address`, `goto` FROM `alias` WHERE `goto` REGEXP :username"); $stmt->execute(array(':username' => '(^|,)'.$username.'($|,)')); diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 15ba214b..24a7e072 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 = "14112017_2103"; + $db_version = "16112017_2259"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -202,6 +202,7 @@ function init_db_schema() { "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'", "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'", "filters" => "TINYINT(1) NOT NULL DEFAULT '1'", + "bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'", ), "keys" => array( "fkey" => array( @@ -325,6 +326,27 @@ function init_db_schema() { ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), + "bcc_maps" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "local_dest" => "VARCHAR(255) NOT NULL", + "bcc_dest" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "type" => "ENUM('sender','rcpt')", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "local_dest" => array("local_dest"), + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "tfa" => array( "cols" => array( "id" => "INT NOT NULL AUTO_INCREMENT", diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 1533e24f..3f13e7dc 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -67,6 +67,7 @@ include $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.'.$_SESSION['mailcow_locale'].'. require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.customize.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.bcc.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php'; diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 2b93af15..12d8b3d9 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -118,3 +118,6 @@ $OTP_LABEL = "mailcow UI"; // Default "to" address in relay test tool $RELAY_TO = "null@hosted.mailcow.de"; + +// Internal constants, can be ignored +define("F2B", 1); \ No newline at end of file diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js index 9e2afa2c..9e3c6868 100644 --- a/data/web/js/mailbox.js +++ b/data/web/js/mailbox.js @@ -314,7 +314,56 @@ jQuery(function($){ } }); } - + function draw_bcc_table() { + ft_bcc_table = FooTable.init('#bcc_table', { + "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, + {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, + {"name":"type","title":"Type"}, + {"name":"local_dest","title":"Local destination"}, + {"name":"bcc_dest","title":"BCC destination/s"}, + {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"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/bcc/all', + jsonp: false, + error: function () { + console.log('Cannot draw bcc table'); + }, + success: function (data) { + $.each(data, function (i, item) { + item.action = ''; + item.chkbox = ''; + if (item.type == 'sender') { + item.type = 'Sender'; + } else { + item.type = 'Recipient'; + } + }); + } + }), + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "filtering": { + "enabled": true, + "position": "left", + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + } + }); + } function draw_alias_table() { ft_alias_table = FooTable.init('#alias_table', { "columns": [ @@ -530,5 +579,6 @@ jQuery(function($){ draw_aliasdomain_table(); draw_sync_job_table(); draw_filter_table(); + draw_bcc_table(); }); diff --git a/data/web/json_api.php b/data/web/json_api.php index b622c312..9ada1044 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -258,6 +258,39 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u )); } break; + case "bcc": + if (isset($_POST['attr'])) { + $attr = (array)json_decode($_POST['attr'], true); + if (bcc('add', $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; case "domain-policy": if (isset($_POST['attr'])) { $attr = (array)json_decode($_POST['attr'], true); @@ -1034,6 +1067,42 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u break; } break; + case "bcc": + switch ($object) { + case "all": + $bcc_items = bcc('get'); + if (!empty($bcc_items)) { + foreach ($bcc_items as $bcc_item) { + if ($details = bcc('details', $bcc_item)) { + $data[] = $details; + } + else { + continue; + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + + default: + $data = bcc('details', $object); + if (!empty($data)) { + $data[] = $details; + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + } + break; case "policy_wl_mailbox": switch ($object) { default: @@ -1464,6 +1533,47 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u )); } break; + case "bcc": + if (isset($_POST['items'])) { + $items = (array)json_decode($_POST['items'], true); + if (is_array($items)) { + if (bcc('delete', 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); @@ -2313,7 +2423,51 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u 'msg' => 'Incomplete post data' )); } - break; + break; + case "bcc": + 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 (bcc('edit', $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/mailbox.php b/data/web/mailbox.php index 193b11c8..b4b8feec 100644 --- a/data/web/mailbox.php +++ b/data/web/mailbox.php @@ -21,6 +21,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
  • +
  • BCC maps
  • @@ -207,6 +208,34 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
    +
    +
    +
    +

    BCC maps

    +
    +

    A recipient map type entry is used, when the local destination acts as recipient of a mail. Sender maps conform to the same principle. + The local destination will not be informed about a failed delivery.

    +
    +
    +
    + +
    +
    diff --git a/data/web/modals/mailbox.php b/data/web/modals/mailbox.php index 192124c2..113b3289 100644 --- a/data/web/modals/mailbox.php +++ b/data/web/modals/mailbox.php @@ -509,6 +509,77 @@ if (!isset($_SESSION['mailcow_cc_role'])) { + + + +
    +
    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 0000000000000000000000000000000000000000..0e97426dcb4e2d20d2f3b13ec381cccb05e4703b GIT binary patch literal 4613 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 literal 0 HcmV?d00001 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'])) {