From aca01c8aa2607777205226a86be29df338eb51d9 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Mon, 27 Jan 2025 15:59:50 +0100 Subject: [PATCH 1/3] [Web] Separate Login pages --- data/web/{debug.php => admin/dashboard.php} | 17 +- data/web/admin/index.php | 29 ++ data/web/{ => admin}/mailbox.php | 16 +- data/web/{ => admin}/queue.php | 15 +- data/web/{admin.php => admin/system.php} | 13 +- data/web/css/site/admin.css | 3 - data/web/domainadmin/index.php | 28 ++ data/web/domainadmin/mailbox.php | 58 ++++ data/web/domainadmin/user.php | 44 +++ data/web/inc/functions.inc.php | 1 + data/web/inc/init_db.inc.php | 2 +- data/web/inc/prerequisites.inc.php | 2 +- data/web/inc/sessions.inc.php | 19 +- data/web/inc/triggers.admin.inc.php | 93 +++++++ data/web/inc/triggers.domainadmin.inc.php | 62 +++++ data/web/inc/triggers.global.inc.php | 48 ++++ data/web/inc/triggers.inc.php | 263 ------------------ data/web/inc/triggers.user.inc.php | 132 +++++++++ data/web/index.php | 19 +- data/web/js/site/{debug.js => dashboard.js} | 0 data/web/js/site/mailbox.js | 2 +- data/web/reset-password.php | 4 +- data/web/templates/admin_index.twig | 91 ++++++ data/web/templates/base.twig | 43 +-- .../templates/{debug.twig => dashboard.twig} | 4 +- data/web/templates/domainadmin_index.twig | 91 ++++++ data/web/templates/user/tab-user-auth.twig | 4 +- .../templates/{index.twig => user_index.twig} | 0 data/web/user.php | 33 +-- 29 files changed, 798 insertions(+), 338 deletions(-) rename data/web/{debug.php => admin/dashboard.php} (83%) create mode 100644 data/web/admin/index.php rename data/web/{ => admin}/mailbox.php (73%) rename data/web/{ => admin}/queue.php (55%) rename data/web/{admin.php => admin/system.php} (90%) create mode 100644 data/web/domainadmin/index.php create mode 100644 data/web/domainadmin/mailbox.php create mode 100644 data/web/domainadmin/user.php create mode 100644 data/web/inc/triggers.admin.inc.php create mode 100644 data/web/inc/triggers.domainadmin.inc.php create mode 100644 data/web/inc/triggers.global.inc.php delete mode 100644 data/web/inc/triggers.inc.php create mode 100644 data/web/inc/triggers.user.inc.php rename data/web/js/site/{debug.js => dashboard.js} (100%) create mode 100644 data/web/templates/admin_index.twig rename data/web/templates/{debug.twig => dashboard.twig} (99%) create mode 100644 data/web/templates/domainadmin_index.twig rename data/web/templates/{index.twig => user_index.twig} (100%) diff --git a/data/web/debug.php b/data/web/admin/dashboard.php similarity index 83% rename from data/web/debug.php rename to data/web/admin/dashboard.php index 4a099cb6..894b1c64 100644 --- a/data/web/debug.php +++ b/data/web/admin/dashboard.php @@ -1,8 +1,17 @@ Get('LICENSE_STATUS_CAC $_SESSION['gal'] = json_decode($license_cache, true); } -$js_minifier->add('/web/js/site/debug.js'); +$js_minifier->add('/web/js/site/dashboard.js'); // vmail df $exec_fields = array('cmd' => 'system', 'task' => 'df', 'dir' => '/var/vmail'); @@ -61,7 +70,7 @@ foreach ($containers_info as $container => $container_info) { $hostname = getenv('MAILCOW_HOSTNAME'); $timezone = getenv('TZ'); -$template = 'debug.twig'; +$template = 'dashboard.twig'; $template_data = [ 'log_lines' => getenv('LOG_LINES'), 'vmail_df' => $vmail_df, diff --git a/data/web/admin/index.php b/data/web/admin/index.php new file mode 100644 index 00000000..05ba7033 --- /dev/null +++ b/data/web/admin/index.php @@ -0,0 +1,29 @@ + @$_SESSION['ldelay'] +]; + +$js_minifier->add('/web/js/site/index.js'); +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/data/web/mailbox.php b/data/web/admin/mailbox.php similarity index 73% rename from data/web/mailbox.php rename to data/web/admin/mailbox.php index a84e32c4..d0073bbd 100644 --- a/data/web/mailbox.php +++ b/data/web/admin/mailbox.php @@ -1,10 +1,20 @@ add('/web/js/site/mailbox.js'); $js_minifier->add('/web/js/presets/sieveMailbox.js'); $js_minifier->add('/web/js/site/pwgen.js'); -$role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin'; +$role = "admin"; $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false'; $allow_admin_email_login = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"])) ? 'true' : 'false'; diff --git a/data/web/queue.php b/data/web/admin/queue.php similarity index 55% rename from data/web/queue.php rename to data/web/admin/queue.php index ffce8d8b..85ec5940 100644 --- a/data/web/queue.php +++ b/data/web/admin/queue.php @@ -1,8 +1,17 @@ add('/web/js/site/queue.js'); $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; -$role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin'; +$role = "admin"; $template = 'queue.twig'; $template_data = [ diff --git a/data/web/admin.php b/data/web/admin/system.php similarity index 90% rename from data/web/admin.php rename to data/web/admin/system.php index 5a8895de..c21d43f0 100644 --- a/data/web/admin.php +++ b/data/web/admin/system.php @@ -1,8 +1,17 @@ thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td { padding: 3px; } -table tbody tr { - cursor: pointer; -} table tbody tr td input[type="checkbox"] { cursor: pointer; } diff --git a/data/web/domainadmin/index.php b/data/web/domainadmin/index.php new file mode 100644 index 00000000..2d909f97 --- /dev/null +++ b/data/web/domainadmin/index.php @@ -0,0 +1,28 @@ + @$_SESSION['ldelay'], +]; + +$js_minifier->add('/web/js/site/index.js'); +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/data/web/domainadmin/mailbox.php b/data/web/domainadmin/mailbox.php new file mode 100644 index 00000000..bb2ef16f --- /dev/null +++ b/data/web/domainadmin/mailbox.php @@ -0,0 +1,58 @@ +add('/web/js/site/mailbox.js'); +$js_minifier->add('/web/js/presets/sieveMailbox.js'); +$js_minifier->add('/web/js/site/pwgen.js'); + +$role = "domainadmin"; +$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false'; +$allow_admin_email_login = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"])) ? 'true' : 'false'; + +// domains +$domains = mailbox('get', 'domains'); + +// mailboxes +$mailboxes = []; +foreach ($domains as $domain) { + foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) { + $mailboxes[] = $mailbox; + } +} + +$template = 'mailbox.twig'; +$template_data = [ + 'acl' => $_SESSION['acl'], + 'acl_json' => json_encode($_SESSION['acl']), + 'role' => $role, + 'is_dual' => $is_dual, + 'allow_admin_email_login' => $allow_admin_email_login, + 'global_filters' => mailbox('get', 'global_filter_details'), + 'domains' => $domains, + 'mailboxes' => $mailboxes, + 'lang_mailbox' => json_encode($lang['mailbox']), + 'lang_rl' => json_encode($lang['ratelimit']), + 'lang_edit' => json_encode($lang['edit']), + 'lang_datatables' => json_encode($lang['datatables']), +]; + +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; \ No newline at end of file diff --git a/data/web/domainadmin/user.php b/data/web/domainadmin/user.php new file mode 100644 index 00000000..7f1b392e --- /dev/null +++ b/data/web/domainadmin/user.php @@ -0,0 +1,44 @@ + "get_friendly_names")); + $username = $_SESSION['mailcow_cc_username']; + + $template = 'domainadmin.twig'; + $template_data = [ + 'acl' => $_SESSION['acl'], + 'acl_json' => json_encode($_SESSION['acl']), + 'user_spam_score' => mailbox('get', 'spam_score', $username), + 'tfa_data' => $tfa_data, + 'fido2_data' => $fido2_data, + 'lang_user' => json_encode($lang['user']), + 'lang_datatables' => json_encode($lang['datatables']), + ]; +} +elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { + header('Location: /admin/dashboard'); + exit(); +} +elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { + header('Location: /user'); + exit(); +} +else { + header('Location: /domainadmin'); + exit(); +} + +$js_minifier->add('/web/js/site/user.js'); +$js_minifier->add('/web/js/site/pwgen.js'); + +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index ed41379f..23362427 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -3101,6 +3101,7 @@ function clear_session(){ session_write_close(); } function set_user_loggedin_session($user) { + session_regenerate_id(true); $_SESSION['mailcow_cc_username'] = $user; $_SESSION['mailcow_cc_role'] = 'user'; $sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass"); diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 7fb619d4..156856c2 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 = "20112024_1105"; + $db_version = "27012025_1555"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 5738e7c0..deb5da8f 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -297,7 +297,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.rspamd.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.tls_policy_maps.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.transports.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/init_db.inc.php'; -require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.global.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/twig.inc.php'; init_db_schema(); if (isset($_SESSION['mailcow_cc_role'])) { diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php index 67bdd35b..bbc08cf1 100644 --- a/data/web/inc/sessions.inc.php +++ b/data/web/inc/sessions.inc.php @@ -99,15 +99,30 @@ if (isset($_POST["logout"])) { unset($_SESSION['sogo-sso-user-allowed']); unset($_SESSION['sogo-sso-pass']); unset($_SESSION["dual-login"]); - header("Location: /mailbox"); + if ($_SESSION["mailcow_cc_role"] == "admin"){ + header("Location: /admin/mailbox"); + } elseif ($_SESSION["mailcow_cc_role"] == "domainadmin") { + header("Location: /domainadmin/mailbox"); + } else { + header("Location: /"); + } exit(); } else { + $role = $_SESSION["mailcow_cc_role"]; session_regenerate_id(true); session_unset(); session_destroy(); session_write_close(); - header("Location: /"); + if ($role == "admin") { + header("Location: /admin"); + } + elseif ($role == "domainadmin") { + header("Location: /domainadmin"); + } + else { + header("Location: /"); + } } } diff --git a/data/web/inc/triggers.admin.inc.php b/data/web/inc/triggers.admin.inc.php new file mode 100644 index 00000000..5b1061f7 --- /dev/null +++ b/data/web/inc/triggers.admin.inc.php @@ -0,0 +1,93 @@ + "admin")); + + if ($as == "admin") { + session_regenerate_id(true); + $_SESSION['mailcow_cc_username'] = $login_user; + $_SESSION['mailcow_cc_role'] = "admin"; + header("Location: /admin/dashboard"); + die(); + } + elseif ($as != "pending") { + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); + unset($_SESSION['mailcow_cc_username']); + unset($_SESSION['mailcow_cc_role']); + } +} + +if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin" && !isset($_SESSION['mailcow_cc_api'])) { + // TODO: Move file upload to API? + if (isset($_POST["submit_main_logo"])) { + if ($_FILES['main_logo']['error'] == 0) { + customize('add', 'main_logo', $_FILES); + } + if ($_FILES['main_logo_dark']['error'] == 0) { + customize('add', 'main_logo_dark', $_FILES); + } + } + if (isset($_POST["reset_main_logo"])) { + customize('delete', 'main_logo'); + customize('delete', 'main_logo_dark'); + } + // Some actions will not be available via API + if (isset($_POST["license_validate_now"])) { + license('verify'); + } + if (isset($_POST["admin_api"])) { + if (isset($_POST["admin_api"]["ro"])) { + admin_api('ro', 'edit', $_POST); + } + elseif (isset($_POST["admin_api"]["rw"])) { + admin_api('rw', 'edit', $_POST); + } + } + if (isset($_POST["admin_api_regen_key"])) { + if (isset($_POST["admin_api_regen_key"]["ro"])) { + admin_api('ro', 'regen_key', $_POST); + } + elseif (isset($_POST["admin_api_regen_key"]["rw"])) { + admin_api('rw', 'regen_key', $_POST); + } + } + if (isset($_POST["rspamd_ui"])) { + rspamd_ui('edit', $_POST); + } + if (isset($_POST["mass_send"])) { + sys_mail($_POST); + } +} +?> diff --git a/data/web/inc/triggers.domainadmin.inc.php b/data/web/inc/triggers.domainadmin.inc.php new file mode 100644 index 00000000..9ee53d67 --- /dev/null +++ b/data/web/inc/triggers.domainadmin.inc.php @@ -0,0 +1,62 @@ + "domain_admin")); + + if ($as == "domainadmin") { + session_regenerate_id(true); + $_SESSION['mailcow_cc_username'] = $login_user; + $_SESSION['mailcow_cc_role'] = "domainadmin"; + header("Location: /domainadmin/mailbox"); + die(); + } + elseif ($as != "pending") { + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); + unset($_SESSION['mailcow_cc_username']); + unset($_SESSION['mailcow_cc_role']); + } +} +?> diff --git a/data/web/inc/triggers.global.inc.php b/data/web/inc/triggers.global.inc.php new file mode 100644 index 00000000..dd88fad5 --- /dev/null +++ b/data/web/inc/triggers.global.inc.php @@ -0,0 +1,48 @@ + "unset_fido2_key", "post_data" => $_POST)); + } +} +?> diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php deleted file mode 100644 index b0c2237d..00000000 --- a/data/web/inc/triggers.inc.php +++ /dev/null @@ -1,263 +0,0 @@ - $_POST['new_password'], - 'new_password2' => $_POST['new_password2'], - 'token' => $_POST['token'], - 'username' => $username, - 'check_tfa' => True - )); - - if ($reset_result){ - header("Location: /"); - exit; - } -} -if (isset($_POST["verify_tfa_login"])) { - if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) { - if ($_SESSION['pending_mailcow_cc_role'] == "admin") { - $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; - $_SESSION['mailcow_cc_role'] = "admin"; - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); - - header("Location: /debug"); - die(); - } - elseif ($_SESSION['pending_mailcow_cc_role'] == "domainadmin") { - $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; - $_SESSION['mailcow_cc_role'] = "domainadmin"; - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); - - header("Location: /mailbox"); - die(); - } - elseif ($_SESSION['pending_mailcow_cc_role'] == "user") { - if (isset($_SESSION['pending_pw_reset_token']) && isset($_SESSION['pending_pw_new_password'])) { - reset_password("reset", array( - 'new_password' => $_SESSION['pending_pw_new_password'], - 'new_password2' => $_SESSION['pending_pw_new_password'], - 'token' => $_SESSION['pending_pw_reset_token'], - 'username' => $_SESSION['pending_mailcow_cc_username'] - )); - unset($_SESSION['pending_pw_reset_token']); - unset($_SESSION['pending_pw_new_password']); - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_tfa_methods']); - - header("Location: /"); - die(); - } else { - set_user_loggedin_session($_SESSION['pending_mailcow_cc_username']); - $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']); - $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; - if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) { - header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}"); - die(); - } else { - header("Location: /user"); - die(); - } - } - } - } - - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); -} - -if (isset($_GET["cancel_tfa_login"])) { - unset($_SESSION['pending_pw_reset_token']); - unset($_SESSION['pending_pw_new_password']); - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); - - header("Location: /"); -} - -if (isset($_POST["quick_release"])) { - quarantine('quick_release', $_POST["quick_release"]); -} - -if (isset($_POST["quick_delete"])) { - quarantine('quick_delete', $_POST["quick_delete"]); -} - -if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { - $login_user = strtolower(trim($_POST["login_user"])); - $as = check_login($login_user, $_POST["pass_user"]); - - if ($as == "admin") { - $_SESSION['mailcow_cc_username'] = $login_user; - $_SESSION['mailcow_cc_role'] = "admin"; - header("Location: /debug"); - die(); - } - elseif ($as == "domainadmin") { - $_SESSION['mailcow_cc_username'] = $login_user; - $_SESSION['mailcow_cc_role'] = "domainadmin"; - header("Location: /mailbox"); - die(); - } - elseif ($as == "user") { - set_user_loggedin_session($login_user); - $http_parameters = explode('&', $_SESSION['index_query_string']); - unset($_SESSION['index_query_string']); - if (in_array('mobileconfig', $http_parameters)) { - if (in_array('only_email', $http_parameters)) { - header("Location: /mobileconfig.php?only_email"); - die(); - } - header("Location: /mobileconfig.php"); - die(); - } - - $user_details = mailbox("get", "mailbox_details", $login_user); - $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; - if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) { - header("Location: /SOGo/so/{$login_user}"); - die(); - } else { - header("Location: /user"); - die(); - } - } - elseif ($as != "pending") { - unset($_SESSION['pending_mailcow_cc_username']); - unset($_SESSION['pending_mailcow_cc_role']); - unset($_SESSION['pending_tfa_methods']); - unset($_SESSION['mailcow_cc_username']); - unset($_SESSION['mailcow_cc_role']); - } -} - -if (isset($_SESSION['mailcow_cc_role']) && (isset($_SESSION['acl']['login_as']) && $_SESSION['acl']['login_as'] == "1")) { - if (isset($_GET["duallogin"])) { - $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; - if (!$is_dual) { - $duallogin = html_entity_decode(rawurldecode($_GET["duallogin"])); - if (filter_var($duallogin, FILTER_VALIDATE_EMAIL)) { - if (!empty(mailbox('get', 'mailbox_details', $duallogin))) { - $_SESSION["dual-login"]["username"] = $_SESSION['mailcow_cc_username']; - $_SESSION["dual-login"]["role"] = $_SESSION['mailcow_cc_role']; - $_SESSION['mailcow_cc_username'] = $duallogin; - $_SESSION['mailcow_cc_role'] = "user"; - header("Location: /user"); - } - } - else { - if (!empty(domain_admin('details', $duallogin))) { - $_SESSION["dual-login"]["username"] = $_SESSION['mailcow_cc_username']; - $_SESSION["dual-login"]["role"] = $_SESSION['mailcow_cc_role']; - $_SESSION['mailcow_cc_username'] = $duallogin; - $_SESSION['mailcow_cc_role'] = "domainadmin"; - header("Location: /user"); - } - } - } - } -} - -if (isset($_SESSION['mailcow_cc_role'])) { - if (isset($_POST["set_tfa"])) { - set_tfa($_POST); - } - if (isset($_POST["unset_tfa_key"])) { - unset_tfa_key($_POST); - } - if (isset($_POST["unset_fido2_key"])) { - fido2(array("action" => "unset_fido2_key", "post_data" => $_POST)); - } -} -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin" && !isset($_SESSION['mailcow_cc_api'])) { - // TODO: Move file upload to API? - if (isset($_POST["submit_main_logo"])) { - if ($_FILES['main_logo']['error'] == 0) { - customize('add', 'main_logo', $_FILES); - } - if ($_FILES['main_logo_dark']['error'] == 0) { - customize('add', 'main_logo_dark', $_FILES); - } - } - if (isset($_POST["reset_main_logo"])) { - customize('delete', 'main_logo'); - customize('delete', 'main_logo_dark'); - } - // Some actions will not be available via API - if (isset($_POST["license_validate_now"])) { - license('verify'); - } - if (isset($_POST["admin_api"])) { - if (isset($_POST["admin_api"]["ro"])) { - admin_api('ro', 'edit', $_POST); - } - elseif (isset($_POST["admin_api"]["rw"])) { - admin_api('rw', 'edit', $_POST); - } - } - if (isset($_POST["admin_api_regen_key"])) { - if (isset($_POST["admin_api_regen_key"]["ro"])) { - admin_api('ro', 'regen_key', $_POST); - } - elseif (isset($_POST["admin_api_regen_key"]["rw"])) { - admin_api('rw', 'regen_key', $_POST); - } - } - if (isset($_POST["rspamd_ui"])) { - rspamd_ui('edit', $_POST); - } - if (isset($_POST["mass_send"])) { - sys_mail($_POST); - } -} -?> diff --git a/data/web/inc/triggers.user.inc.php b/data/web/inc/triggers.user.inc.php new file mode 100644 index 00000000..c16edc10 --- /dev/null +++ b/data/web/inc/triggers.user.inc.php @@ -0,0 +1,132 @@ + $_POST['new_password'], + 'new_password2' => $_POST['new_password2'], + 'token' => $_POST['token'], + 'username' => $username, + 'check_tfa' => True + )); + + if ($reset_result){ + header("Location: /"); + exit; + } +} +if (isset($_POST["verify_tfa_login"])) { + if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) { + if ($_SESSION['pending_mailcow_cc_role'] == "user") { + if (isset($_SESSION['pending_pw_reset_token']) && isset($_SESSION['pending_pw_new_password'])) { + reset_password("reset", array( + 'new_password' => $_SESSION['pending_pw_new_password'], + 'new_password2' => $_SESSION['pending_pw_new_password'], + 'token' => $_SESSION['pending_pw_reset_token'], + 'username' => $_SESSION['pending_mailcow_cc_username'] + )); + unset($_SESSION['pending_pw_reset_token']); + unset($_SESSION['pending_pw_new_password']); + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_tfa_methods']); + + header("Location: /"); + die(); + } else { + set_user_loggedin_session($_SESSION['pending_mailcow_cc_username']); + $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']); + $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; + if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) { + header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}"); + die(); + } else { + header("Location: /user"); + die(); + } + } + } + } + + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); +} + +if (isset($_GET["cancel_tfa_login"])) { + unset($_SESSION['pending_pw_reset_token']); + unset($_SESSION['pending_pw_new_password']); + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); + + header("Location: /"); +} + +if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { + $login_user = strtolower(trim($_POST["login_user"])); + $as = check_login($login_user, $_POST["pass_user"], false, array("role" => "user")); + + if ($as == "user") { + set_user_loggedin_session($login_user); + $http_parameters = explode('&', $_SESSION['index_query_string']); + unset($_SESSION['index_query_string']); + if (in_array('mobileconfig', $http_parameters)) { + if (in_array('only_email', $http_parameters)) { + header("Location: /mobileconfig.php?only_email"); + die(); + } + header("Location: /mobileconfig.php"); + die(); + } + + $user_details = mailbox("get", "mailbox_details", $login_user); + $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; + if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) { + header("Location: /SOGo/so/{$login_user}"); + die(); + } else { + header("Location: /user"); + die(); + } + } + elseif ($as != "pending") { + unset($_SESSION['pending_mailcow_cc_username']); + unset($_SESSION['pending_mailcow_cc_role']); + unset($_SESSION['pending_tfa_methods']); + unset($_SESSION['mailcow_cc_username']); + unset($_SESSION['mailcow_cc_role']); + } +} +?> diff --git a/data/web/index.php b/data/web/index.php index 0282e483..1e91cb78 100644 --- a/data/web/index.php +++ b/data/web/index.php @@ -1,5 +1,6 @@ @$_SESSION['oauth2_request'], 'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'), diff --git a/data/web/js/site/debug.js b/data/web/js/site/dashboard.js similarity index 100% rename from data/web/js/site/debug.js rename to data/web/js/site/dashboard.js diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 0c9ffb3d..3c21c18b 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -939,7 +939,7 @@ jQuery(function($){ ' ' + lang.remove + '' + ' Login'; if (ALLOW_ADMIN_EMAIL_LOGIN) { - item.action += ' SOGo'; + item.action += ' SOGo'; } item.action += ''; } diff --git a/data/web/reset-password.php b/data/web/reset-password.php index a0225dc6..7544d40c 100644 --- a/data/web/reset-password.php +++ b/data/web/reset-password.php @@ -2,11 +2,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /debug'); + header('Location: /admin/dashboard'); exit(); } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /mailbox'); + header('Location: /domainadmin/mailbox'); exit(); } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { diff --git a/data/web/templates/admin_index.twig b/data/web/templates/admin_index.twig new file mode 100644 index 00000000..4127a6a2 --- /dev/null +++ b/data/web/templates/admin_index.twig @@ -0,0 +1,91 @@ +{% extends 'base.twig' %} + +{% block navbar %}{% endblock %} + +{% block content %} +
+
+
+
+ {{ lang.login.login }} +
+ + +
+
+
+ + {% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active %} +
{{ ui_texts.ui_announcement_text|rot13 }}
+ {% endif %} +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ + +
+
+
+
{{ lang.login.other_logins }}
+ + {% if login_delay %} +

{{ lang.login.delayed|format(login_delay) }}

+ {% endif %} +
+ {% if (mailcow_apps or app_links) and not hide_mailcow_apps %} + {{ ui_texts.apps_name|raw }}
+
+ {% for app in mailcow_apps %} + {% if not app.hide %} + {% if not skip_sogo or not is_uri('SOGo', app.link) %} + + {% endif %} + {% endif %} + {% endfor %} + {% for row in app_links %} + {% for key, val in row %} + {% if not val.hide %} +
+ {{ key }} +
+ {% endif %} + {% endfor %} + {% endfor %} +
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 2634574d..4d0a3025 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -66,27 +66,36 @@ + {% endif %} - {% if mailcow_cc_role != 'admin' %} + {% if mailcow_cc_role == 'domainadmin' %} + + {% elseif mailcow_cc_role == 'user' %} {% endif %} - {% if mailcow_cc_role == 'admin' or mailcow_cc_role == 'domainadmin' %} + {% if mailcow_cc_role == 'domainadmin' %} {% endif %} @@ -246,7 +255,7 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".totp-authenticator-selection").click(function(){ $(".totp-authenticator-selection").removeClass("active"); $(this).addClass("active"); - + var id = $(this).children('input').first().val(); $("#totp_selected_id").val(id); @@ -255,7 +264,7 @@ function recursiveBase64StrToArrayBuffer(obj) { if ($('.totp-authenticator-selection').length == 1 && $('#pending_tfa_tab_yubi_otp').length == 0 && $('.webauthn-authenticator-selection').length == 0){ - + // select default if only one authenticator exists $('.totp-authenticator-selection').addClass("active"); @@ -268,7 +277,7 @@ function recursiveBase64StrToArrayBuffer(obj) { $('#pending_tfa_tab_totp').on('shown.bs.tab', function() { // autofocus setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 200); - }); + }); // validate Yubi OTP tfa if ($('.webauthn-authenticator-selection').length == 0){ // autofocus @@ -287,10 +296,10 @@ function recursiveBase64StrToArrayBuffer(obj) { $(".webauthn-authenticator-selection").click(function(){ $(".webauthn-authenticator-selection").removeClass("active"); $(this).addClass("active"); - + var id = $(this).children('input').first().val(); $("#webauthn_selected_id").val(id); - + var webauthn_status_auth = document.getElementById('webauthn_status_auth'); webauthn_status_auth.style.setProperty('display', 'flex', 'important'); var webauthn_return_code = document.getElementById('webauthn_return_code'); @@ -313,7 +322,7 @@ function recursiveBase64StrToArrayBuffer(obj) { console.log(json); if (json.success === false) throw new Error(); if (json.type === "error") throw new Error(json.msg); - + recursiveBase64StrToArrayBuffer(json); return json; }).then(getCredentialArgs => { @@ -340,7 +349,7 @@ function recursiveBase64StrToArrayBuffer(obj) { webauthn_return_code.style.setProperty('display', 'block', 'important'); webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; }); - } + } }); $('#ConfirmTFAModal').on('hidden.bs.modal', function(){ // cancel pending login @@ -551,7 +560,7 @@ function recursiveBase64StrToArrayBuffer(obj) { Version: {{ mailcow_info.version_tag }} - {% endif %} + {% endif %} {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "nightly" and mailcow_info.version_tag|default %} 🛠️🐮 + 🐋 = 💕 diff --git a/data/web/templates/debug.twig b/data/web/templates/dashboard.twig similarity index 99% rename from data/web/templates/debug.twig rename to data/web/templates/dashboard.twig index c148856c..8f8c2d99 100644 --- a/data/web/templates/debug.twig +++ b/data/web/templates/dashboard.twig @@ -42,8 +42,8 @@ mailcow-logo-dark
-
- +
+
diff --git a/data/web/templates/domainadmin_index.twig b/data/web/templates/domainadmin_index.twig new file mode 100644 index 00000000..4127a6a2 --- /dev/null +++ b/data/web/templates/domainadmin_index.twig @@ -0,0 +1,91 @@ +{% extends 'base.twig' %} + +{% block navbar %}{% endblock %} + +{% block content %} +
+
+
+
+ {{ lang.login.login }} +
+ + +
+
+
+ + {% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active %} +
{{ ui_texts.ui_announcement_text|rot13 }}
+ {% endif %} +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ + +
+
+ +
{{ lang.login.other_logins }}
+ + {% if login_delay %} +

{{ lang.login.delayed|format(login_delay) }}

+ {% endif %} +
+ {% if (mailcow_apps or app_links) and not hide_mailcow_apps %} + {{ ui_texts.apps_name|raw }}
+
+ {% for app in mailcow_apps %} + {% if not app.hide %} + {% if not skip_sogo or not is_uri('SOGo', app.link) %} + + {% endif %} + {% endif %} + {% endfor %} + {% for row in app_links %} + {% for key, val in row %} + {% if not val.hide %} +
+ {{ key }} +
+ {% endif %} + {% endfor %} + {% endfor %} +
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/data/web/templates/user/tab-user-auth.twig b/data/web/templates/user/tab-user-auth.twig index 5c90bedf..a7e02543 100644 --- a/data/web/templates/user/tab-user-auth.twig +++ b/data/web/templates/user/tab-user-auth.twig @@ -20,11 +20,11 @@ {{ lang.user.open_webmail_sso }} {% elseif dual_login %} - + {{ lang.user.open_webmail_sso }} {% else %} - + {{ lang.user.open_webmail_sso }} {% endif %} diff --git a/data/web/templates/index.twig b/data/web/templates/user_index.twig similarity index 100% rename from data/web/templates/index.twig rename to data/web/templates/user_index.twig diff --git a/data/web/user.php b/data/web/user.php index 19aafddd..7c34ba95 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -1,29 +1,8 @@ "get_friendly_names")); - $username = $_SESSION['mailcow_cc_username']; - - $template = 'domainadmin.twig'; - $template_data = [ - 'acl' => $_SESSION['acl'], - 'acl_json' => json_encode($_SESSION['acl']), - 'user_spam_score' => mailbox('get', 'spam_score', $username), - 'tfa_data' => $tfa_data, - 'fido2_data' => $fido2_data, - 'lang_user' => json_encode($lang['user']), - 'lang_datatables' => json_encode($lang['datatables']), - ]; -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { +if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { /* / USER @@ -95,6 +74,14 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' 'lang_datatables' => json_encode($lang['datatables']), ]; } +elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { + header('Location: /admin/dashboard'); + exit(); +} +elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { + header('Location: /domainadmin/mailbox'); + exit(); +} else { header('Location: /'); exit(); From f0016eeecdaa74ad28d3e63914895d05f43c8196 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 7 Feb 2025 14:19:20 +0100 Subject: [PATCH 2/3] [Web] Add german translation for idp settings --- data/web/lang/lang.de-de.json | 37 +++++++++++++++++++ data/web/lang/lang.en-gb.json | 3 ++ .../admin/tab-config-identity-provider.twig | 8 ++-- data/web/templates/admin_index.twig | 2 +- data/web/templates/domainadmin_index.twig | 2 +- data/web/templates/user_index.twig | 2 +- 6 files changed, 47 insertions(+), 7 deletions(-) diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 782e4e68..8e164b44 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -206,6 +206,39 @@ "help_text": "Hilfstext unter Login-Maske (HTML ist zulässig)", "host": "Host", "html": "HTML", + "iam": "Identity Provider", + "iam_attribute_field": "Attribut Feld", + "iam_authorize_url": "Authorization Endpunkt", + "iam_auth_flow": "Authentication Flow", + "iam_auth_flow_info": "Zusätzlich zum Authorization Code Flow (dem Standard-Flow in Keycloak), der für Single-Sign-On-Logins verwendet wird, unterstützt mailcow auch den Authentication Flow mit direkten Anmeldeinformationen. Der Mailpassword Flow versucht, die Anmeldedaten des Benutzers über die Keycloak Admin REST API zu validieren. Dabei ruft mailcow das gehashte Passwort aus dem mailcow_password Attribut ab, das in Keycloak zugewiesen ist.", + "iam_basedn": "Base DN", + "iam_client_id": "Client ID", + "iam_client_secret": "Client Secret", + "iam_client_scopes": "Client Scopes", + "iam_description": "Konfiguriere einen externen Identity Provider für die Authentifizierung
Die Mailboxen der Benutzer werden bei ihrer ersten Anmeldung automatisch erstellt, vorausgesetzt, dass ein Attribut Mapping festgelegt wurde.", + "iam_extra_permission": "Damit die folgenden Einstellungen funktionieren, benötigt der mailcow Client in Keycloak ein Service-Konto und die Berechtigung view-users.", + "iam_host": "Host", + "iam_host_info": "Gib einen oder mehrere LDAP-Hosts ein, getrennt durch Kommas.", + "iam_import_users": "Import Users", + "iam_mapping": "Attribut Mapping", + "iam_bindpass": "Bind Passwort", + "iam_periodic_full_sync": "Periodic Full Sync", + "iam_port": "Port", + "iam_realm": "Realm", + "iam_redirect_url": "Redirect Url", + "iam_rest_flow": "Mailpassword Flow", + "iam_server_url": "Server Url", + "iam_sso": "Single Sign-On", + "iam_sync_interval": "Sync / Import interval (min)", + "iam_test_connection": "Verbindung Testen", + "iam_token_url": "Token Endpunkt", + "iam_userinfo_url": "User info Endpunkt", + "iam_username_field": "Username Feld", + "iam_binddn": "Bind DN", + "iam_use_ssl": "Benutze SSL", + "iam_use_tls": "Benutze TLS", + "iam_version": "Version", + "ignore_ssl_error": "Ignoriere SSL Errors", "import": "Importieren", "import_private_key": "Private Key importieren", "in_use_by": "Verwendet von", @@ -403,6 +436,7 @@ "goto_empty": "Eine Alias-Adresse muss auf mindestens eine gültige Ziel-Adresse zeigen", "goto_invalid": "Ziel-Adresse %s ist ungültig", "ham_learn_error": "Ham Lernfehler: %s", + "iam_test_connection": "Verbindung fehlgeschlagen", "imagick_exception": "Fataler Bildverarbeitungsfehler", "img_dimensions_exceeded": "Grafik überschreitet die maximale Bildgröße", "img_invalid": "Grafik konnte nicht validiert werden", @@ -766,6 +800,9 @@ "forgot_password": "> Passwort vergessen?", "invalid_pass_reset_token": "Der Rücksetz-Token für das Passwort ist ungültig oder abgelaufen.
Bitte fordern Sie einen neuen Link zur Passwortwiederherstellung an.", "login": "Anmelden", + "login_user": "Benutzer Anmelden", + "login_dadmin": "Domain-Administrator Anmelden", + "login_admin": "Administrator Anmelden", "mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.", "new_password": "Neues Passwort", "new_password_confirm": "Neues Passwort bestätigen", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 6d3580e1..362262bf 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -804,6 +804,9 @@ "forgot_password": "> Forgot Password?", "invalid_pass_reset_token": "The reset password token is invalid or has expired.
Please request a new password reset link.", "login": "Login", + "login_user": "User Login", + "login_dadmin": "Domain-Administrator Login", + "login_admin": "Administrator Login", "mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.", "new_password": "New Password", "new_password_confirm": "Confirm new password", diff --git a/data/web/templates/admin/tab-config-identity-provider.twig b/data/web/templates/admin/tab-config-identity-provider.twig index e2cea7fa..3381fe4d 100644 --- a/data/web/templates/admin/tab-config-identity-provider.twig +++ b/data/web/templates/admin/tab-config-identity-provider.twig @@ -84,7 +84,7 @@
- Attribute + {{ lang.user.attribute }} {{ lang.mailbox.template }}
@@ -274,8 +274,8 @@
- Attribute - Template + {{ lang.user.attribute }} + {{ lang.mailbox.template }}
@@ -454,7 +454,7 @@
- Attribute + {{ lang.user.attribute }} {{ lang.mailbox.template }}
diff --git a/data/web/templates/admin_index.twig b/data/web/templates/admin_index.twig index 4127a6a2..93a89284 100644 --- a/data/web/templates/admin_index.twig +++ b/data/web/templates/admin_index.twig @@ -7,7 +7,7 @@
- {{ lang.login.login }} + {{ lang.login.login_admin }}
diff --git a/data/web/templates/domainadmin_index.twig b/data/web/templates/domainadmin_index.twig index 4127a6a2..41a9f259 100644 --- a/data/web/templates/domainadmin_index.twig +++ b/data/web/templates/domainadmin_index.twig @@ -7,7 +7,7 @@
- {{ lang.login.login }} + {{ lang.login.login_dadmin }}
diff --git a/data/web/templates/user_index.twig b/data/web/templates/user_index.twig index 07274e6b..950482c9 100644 --- a/data/web/templates/user_index.twig +++ b/data/web/templates/user_index.twig @@ -7,7 +7,7 @@
- {{ lang.login.login }} + {{ lang.login.login_user }}
From 55dcae4a01998b3d9ed861a3f833b6b4df2567e0 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 7 Feb 2025 15:05:43 +0100 Subject: [PATCH 3/3] [Web] Fix Generic-OIDC connection test --- data/web/inc/functions.inc.php | 30 ++++++++++++++++++++++++------ data/web/lang/lang.de-de.json | 1 + 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index a434582c..7969c6bb 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -2337,12 +2337,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { switch ($_data['authsource']) { case 'keycloak': - case 'generic-oidc': - if ($_data['authsource'] == 'keycloak') { - $url = "{$_data['server_url']}/realms/{$_data['realm']}/protocol/openid-connect/token"; - } else { - $url = $_data['token_url']; - } + $url = "{$_data['server_url']}/realms/{$_data['realm']}/protocol/openid-connect/token"; $req = http_build_query(array( 'grant_type' => 'client_credentials', 'client_id' => $_data['client_id'], @@ -2355,6 +2350,29 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { curl_setopt($curl, CURLOPT_POSTFIELDS, $req); curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded')); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + if ($_data['ignore_ssl_error'] == "1"){ + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); + } + $res = curl_exec($curl); + $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close ($curl); + + if ($code != 200) { + return false; + } + break; + case 'generic-oidc': + $url = $_data['token_url']; + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_TIMEOUT, 7); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "OPTIONS"); + if ($_data['ignore_ssl_error'] == "1"){ + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); + } $res = curl_exec($curl); $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close ($curl); diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 8e164b44..9c54d3fc 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -1114,6 +1114,7 @@ "forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt", "global_filter_written": "Filterdatei wurde erfolgreich geschrieben", "hash_deleted": "Hash wurde gelöscht", + "iam_test_connection": "Verbindung erfolgreich", "ip_check_opt_in_modified": "IP Check wurde erfolgreich gespeichert", "item_deleted": "Objekt %s wurde entfernt", "item_released": "Objekt %s freigegeben",
Hostname