From e13bc242a4130c1333f995c1bf02369c06e3a16d Mon Sep 17 00:00:00 2001 From: andryyy Date: Thu, 28 Oct 2021 21:57:19 +0200 Subject: [PATCH] [Web, Dovecot] Allow to define scope of services for app passwords --- data/Dockerfiles/dovecot/docker-entrypoint.sh | 36 ++++++++------- data/web/edit.php | 2 +- data/web/inc/functions.app_passwd.inc.php | 43 ++++++++++++++---- data/web/inc/functions.inc.php | 44 ++++++++++++++++++- data/web/inc/init_db.inc.php | 6 ++- data/web/js/site/user.js | 13 ++++-- data/web/lang/lang.de.json | 3 ++ data/web/lang/lang.en.json | 3 ++ data/web/sogo-auth.php | 23 +++++++--- data/web/templates/base.twig | 2 +- data/web/templates/edit/app-passwd.twig | 12 +++++ data/web/templates/modals/user.twig | 11 +++++ data/web/templates/user/tab-user-auth.twig | 2 +- docker-compose.yml | 2 +- 14 files changed, 161 insertions(+), 41 deletions(-) diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 798078bd..b6b775fb 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -163,24 +163,26 @@ function auth_password_verify(req, pass) row = cur:fetch (row, "a") end - -- check against app passwds - -- removed on 22nd Oct 2021: AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.force_pw_update')), 0) != '1' - local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, app_passwd.password FROM app_passwd - INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox - WHERE mailbox = '%s' - AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.%s_access')), 1) = '1' - AND app_passwd.active = '1' - AND mailbox.active = '1' - AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.user), con:escape(req.service), con:escape(req.domain))) - local row = cur:fetch ({}, "a") - while row do - if req.password_verify(req, row.password, pass) == 1 then - cur:close() - con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip) - VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip))) - return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass + -- check against app passwds for imap and smtp + -- app passwords are only available for imap and smtp in dovecot + if req.service == "smtp" or req.service == "imap" then + local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, app_passwd.imap_access, app_passwd.smtp_access, app_passwd.password FROM app_passwd + INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox + WHERE mailbox = '%s' + AND app_passwd.%s_access = '1' + AND app_passwd.active = '1' + AND mailbox.active = '1' + AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.user), con:escape(req.service), con:escape(req.domain))) + local row = cur:fetch ({}, "a") + while row do + if req.password_verify(req, row.password, pass) == 1 then + cur:close() + con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip) + VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip))) + return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass + end + row = cur:fetch (row, "a") end - row = cur:fetch (row, "a") end return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate" diff --git a/data/web/edit.php b/data/web/edit.php index bc27c6f2..dfba8479 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -11,7 +11,7 @@ $template = 'edit.twig'; $template_data = []; $result = null; if (isset($_SESSION['mailcow_cc_role'])) { - if ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin") { + if ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin") { if (isset($_GET["alias"]) && !empty($_GET["alias"])) { $alias = html_entity_decode(rawurldecode($_GET["alias"])); diff --git a/data/web/inc/functions.app_passwd.inc.php b/data/web/inc/functions.app_passwd.inc.php index 68cc85cd..701bbe68 100644 --- a/data/web/inc/functions.app_passwd.inc.php +++ b/data/web/inc/functions.app_passwd.inc.php @@ -27,6 +27,11 @@ function app_passwd($_action, $_data = null) { $password = $_data['app_passwd']; $password2 = $_data['app_passwd2']; $active = intval($_data['active']); + $protocols = (array)$_data['protocols']; + $imap_access = (in_array('imap_access', $protocols)) ? 1 : 0; + $dav_access = (in_array('dav_access', $protocols)) ? 1 : 0; + $smtp_access = (in_array('smtp_access', $protocols)) ? 1 : 0; + $eas_access = (in_array('eas_access', $protocols)) ? 1 : 0; $domain = mailbox('get', 'mailbox_details', $username)['domain']; if (empty($domain)) { $_SESSION['return'][] = array( @@ -61,13 +66,17 @@ function app_passwd($_action, $_data = null) { ); return false; } - $stmt = $pdo->prepare("INSERT INTO `app_passwd` (`name`, `mailbox`, `domain`, `password`, `active`) - VALUES (:app_name, :mailbox, :domain, :password, :active)"); + $stmt = $pdo->prepare("INSERT INTO `app_passwd` (`name`, `mailbox`, `domain`, `password`, `imap_access`, `smtp_access`, `eas_access`, `dav_access`, `active`) + VALUES (:app_name, :mailbox, :domain, :password, :imap_access, :smtp_access, :eas_access, :dav_access, :active)"); $stmt->execute(array( ':app_name' => $app_name, ':mailbox' => $username, ':domain' => $domain, ':password' => $password_hashed, + ':imap_access' => $imap_access, + ':smtp_access' => $smtp_access, + ':eas_access' => $eas_access, + ':dav_access' => $dav_access, ':active' => $active )); $_SESSION['return'][] = array( @@ -84,6 +93,19 @@ function app_passwd($_action, $_data = null) { $app_name = (!empty($_data['app_name'])) ? $_data['app_name'] : $is_now['name']; $password = (!empty($_data['password'])) ? $_data['password'] : null; $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null; + if (isset($_data['protocols'])) { + $protocols = (array)$_data['protocols']; + $imap_access = (in_array('imap_access', $protocols)) ? 1 : 0; + $dav_access = (in_array('dav_access', $protocols)) ? 1 : 0; + $smtp_access = (in_array('smtp_access', $protocols)) ? 1 : 0; + $eas_access = (in_array('eas_access', $protocols)) ? 1 : 0; + } + else { + $imap_access = $is_now['imap_access']; + $smtp_access = $is_now['smtp_access']; + $dav_access = $is_now['dav_access']; + $eas_access = $is_now['eas_access']; + } $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; } else { @@ -122,14 +144,23 @@ function app_passwd($_action, $_data = null) { ':id' => $id )); } + $stmt = $pdo->prepare("UPDATE `app_passwd` SET `name` = :app_name, `mailbox` = :username, + `imap_access` = :imap_access, + `smtp_access` = :smtp_access, + `eas_access` = :eas_access, + `dav_access` = :dav_access, `active` = :active WHERE `id` = :id"); $stmt->execute(array( ':app_name' => $app_name, ':username' => $username, + ':imap_access' => $imap_access, + ':smtp_access' => $smtp_access, + ':eas_access' => $eas_access, + ':dav_access' => $dav_access, ':active' => $active, ':id' => $id )); @@ -180,13 +211,7 @@ function app_passwd($_action, $_data = null) { break; case 'details': $app_passwd_data = array(); - $stmt = $pdo->prepare("SELECT `id`, - `name`, - `mailbox`, - `domain`, - `created`, - `modified`, - `active` + $stmt = $pdo->prepare("SELECT * FROM `app_passwd` WHERE `id` = :id"); $stmt->execute(array(':id' => $_data)); diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 8245c46e..d05f6255 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -807,10 +807,17 @@ function verify_hash($hash, $password) { } return false; } -function check_login($user, $pass, $allow_app_passwords = false) { +function check_login($user, $pass, $app_passwd_data = false) { global $pdo; global $redis; global $imap_server; + + if ($app_passwd_data === false) { + $app_passwd_data['eas'] = false; + $app_passwd_data['dav'] = false; + $app_passwd_data['proxyauth'] = false; + } + if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -819,6 +826,8 @@ function check_login($user, $pass, $allow_app_passwords = false) { ); return false; } + + // Validate admin $user = strtolower(trim($user)); $stmt = $pdo->prepare("SELECT `password` FROM `admin` WHERE `superadmin` = '1' @@ -854,6 +863,8 @@ function check_login($user, $pass, $allow_app_passwords = false) { } } } + + // Validate domain admin $stmt = $pdo->prepare("SELECT `password` FROM `admin` WHERE `superadmin` = '0' AND `active`='1' @@ -888,6 +899,8 @@ function check_login($user, $pass, $allow_app_passwords = false) { } } } + + // Validate mailbox user $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` INNER JOIN domain on mailbox.domain = domain.domain WHERE `kind` NOT REGEXP 'location|thing|group' @@ -896,7 +909,7 @@ function check_login($user, $pass, $allow_app_passwords = false) { AND `username` = :user"); $stmt->execute(array(':user' => $user)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - if ($allow_app_passwords === true) { + if ($app_passwd_data['eas'] === true) { $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd` INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox` INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain` @@ -904,6 +917,20 @@ function check_login($user, $pass, $allow_app_passwords = false) { AND `mailbox`.`active` = '1' AND `domain`.`active` = '1' AND `app_passwd`.`active` = '1' + AND `app_passwd`.`eas_access` = '1' + AND `app_passwd`.`mailbox` = :user"); + $stmt->execute(array(':user' => $user)); + $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); + } + elseif ($app_passwd_data['dav'] === true) { + $stmt = $pdo->prepare("SELECT `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd` + INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox` + INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain` + WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group' + AND `mailbox`.`active` = '1' + AND `domain`.`active` = '1' + AND `app_passwd`.`active` = '1' + AND `app_passwd`.`dav_access` = '1' AND `app_passwd`.`mailbox` = :user"); $stmt->execute(array(':user' => $user)); $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -916,9 +943,20 @@ function check_login($user, $pass, $allow_app_passwords = false) { 'log' => array(__FUNCTION__, $user, '*'), 'msg' => array('logged_in_as', $user) ); + if ($app_passwd_data['proxyauth'] === true) { + $service = ($app_passwd_data['eas'] === true) ? 'EAS' : (($app_passwd_data['dav'] === true) ? 'DAV' : 'SSO'); + $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)"); + $stmt->execute(array( + ':service' => $service, + ':app_id' => $row['app_passwd_id'], + ':username' => $user, + ':remote_addr' => $_SERVER['REMOTE_ADDR'] + )); + } return "user"; } } + if (!isset($_SESSION['ldelay'])) { $_SESSION['ldelay'] = "0"; $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); @@ -929,11 +967,13 @@ function check_login($user, $pass, $allow_app_passwords = false) { $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); } + $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $user, '*'), 'msg' => 'login_failed' ); + sleep($_SESSION['ldelay']); return false; } diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index c0c984b8..f3aada9a 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 = "23082021_2224"; + $db_version = "28102021_1600"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -364,6 +364,10 @@ function init_db_schema() { "password" => "VARCHAR(255) NOT NULL", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "imap_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "smtp_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "dav_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "eas_access" => "TINYINT(1) NOT NULL DEFAULT '1'", "active" => "TINYINT(1) NOT NULL DEFAULT '1'" ), "keys" => array( diff --git a/data/web/js/site/user.js b/data/web/js/site/user.js index 6bfd8478..62d6e789 100644 --- a/data/web/js/site/user.js +++ b/data/web/js/site/user.js @@ -101,8 +101,8 @@ jQuery(function($){ $.each(data.sasl, function (i, item) { var datetime = new Date(item.datetime.replace(/-/g, "/")); var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"}); - item.app_password ? app_password = ' (App)' : app_password = "", item.location ? ip_location = ' ' : ip_location = ""; - "smtp" == item.service ? service = '
' + item.service.toUpperCase() + '
' : "imap" == item.service ? service = '
' + item.service.toUpperCase() + "
" : service = '
' + item.service.toUpperCase() + "
"; + item.app_password ? app_password = ' App' : app_password = "", item.location ? ip_location = ' ' : ip_location = ""; + service = '
' + item.service.toUpperCase() + '
'; item.real_rip.startsWith("Web") ? real_rip = item.real_rip : real_rip = '' + item.real_rip + ""; ip_data = real_rip + ip_location + app_password; $(".last-login").append('
  • ' + local_datetime + " " + service + " " + lang.from + " " + ip_data + "
  • "); @@ -258,6 +258,7 @@ jQuery(function($){ {"name":"chkbox","title":"","style":{"maxWidth":"60px","width":"60px","text-align":"center"},"filterable": false,"sortable": false,"type":"html"}, {"sorted": true,"name":"id","title":"ID","style":{"maxWidth":"60px","width":"60px","text-align":"center"}}, {"name":"name","title":lang.app_name}, + {"name":"protocols","title":lang.allowed_protocols}, {"name":"active","filterable": false,"style":{"maxWidth":"70px","width":"70px"},"title":lang.active,"formatter": function(value){return 1==value?'':0==value&&'';}}, {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","min-width":"220px","width":"220px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} ], @@ -271,7 +272,13 @@ jQuery(function($){ }, success: function (data) { $.each(data, function (i, item) { - item.name = escapeHtml(item.name); + item.name = escapeHtml(item.name) + item.protocols = [] + if (item.imap_access == 1) { item.protocols.push("IMAP"); } + if (item.smtp_access == 1) { item.protocols.push("SMTP"); } + if (item.eas_access == 1) { item.protocols.push("EAS/ActiveSync"); } + if (item.dav_access == 1) { item.protocols.push("DAV"); } + item.protocols = item.protocols.join(" ") if (acl_data.app_passwds === 1) { item.action = '
    ' + ' ' + lang.edit + '' + diff --git a/data/web/lang/lang.de.json b/data/web/lang/lang.de.json index 15e981e2..33fadb41 100644 --- a/data/web/lang/lang.de.json +++ b/data/web/lang/lang.de.json @@ -42,6 +42,7 @@ "alias_domain_info": "Nur gültige Domains. Getrennt durch Komma.", "app_name": "App-Name", "app_password": "App-Passwort hinzufügen", + "app_passwd_protocols": "Zugelassene Protokolle für App-Passwort", "automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Relay-Optionen", "bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.", @@ -508,6 +509,7 @@ "allowed_protocols": "Erlaubte Protokolle", "app_name": "App-Name", "app_passwd": "App-Passwörter", + "app_passwd_protocols": "Zugelassene Protokolle für App-Passwort", "automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Relay-Optionen", "bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.", @@ -992,6 +994,7 @@ "aliases_also_send_as": "Darf außerdem versenden als Benutzer", "aliases_send_as_all": "Absender für folgende Domains und zugehörige Alias-Domains nicht prüfen", "app_hint": "App-Passwörter sind alternative Passwörter für den IMAP-, SMTP-, CalDAV-, CardDAV- und EAS-Login am Mailserver. Der Benutzername bleibt unverändert.
    SOGo Webmail ist mit diesem Kennwort nicht verwendbar.", + "allowed_protocols": "Erlaubte Protokolle", "app_name": "App-Name", "app_passwds": "App-Passwörter", "apple_connection_profile": "Apple-Verbindungsprofil", diff --git a/data/web/lang/lang.en.json b/data/web/lang/lang.en.json index 3f4a6220..d060b6ec 100644 --- a/data/web/lang/lang.en.json +++ b/data/web/lang/lang.en.json @@ -42,6 +42,7 @@ "alias_domain_info": "Valid domain names only (comma-separated).", "app_name": "App name", "app_password": "Add app password", + "app_passwd_protocols": "Allowed protocols for app password", "automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Relay options", "bcc_dest_format": "BCC destination must be a single valid email address.
    If you need to send a copy to multiple addresses, create an alias and use it here.", @@ -514,6 +515,7 @@ "allowed_protocols": "Allowed protocols", "app_name": "App name", "app_passwd": "App password", + "app_passwd_protocols": "Allowed protocols for app password", "automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "backup_mx_options": "Relay options", "bcc_dest_format": "BCC destination must be a single valid email address.
    If you need to send a copy to multiple addresses, create an alias and use it here.", @@ -1034,6 +1036,7 @@ "aliases_also_send_as": "Also allowed to send as user", "aliases_send_as_all": "Do not check sender access for the following domain(s) and its alias domains", "app_hint": "App passwords are alternative passwords for your IMAP, SMTP, CalDAV, CardDAV and EAS login. The username remains unchanged. SOGo webmail is not available through app passwords.", + "allowed_protocols": "Allowed protocols", "app_name": "App name", "app_passwds": "App passwords", "apple_connection_profile": "Apple connection profile", diff --git a/data/web/sogo-auth.php b/data/web/sogo-auth.php index a4b8ffcf..3c0130da 100644 --- a/data/web/sogo-auth.php +++ b/data/web/sogo-auth.php @@ -14,8 +14,16 @@ if (isset($_SERVER['PHP_AUTH_USER'])) { require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; $username = $_SERVER['PHP_AUTH_USER']; $password = $_SERVER['PHP_AUTH_PW']; - $is_eas = preg_match('/^(\/SOGo|)\/(dav|Microsoft-Server-ActiveSync).*/', $_SERVER['HTTP_X_ORIGINAL_URI']); - $login_check = check_login($username, $password, $is_eas); + $is_eas = false; + $is_dav = false; + $original_uri = isset($_SERVER['HTTP_X_ORIGINAL_URI']) ? $_SERVER['HTTP_X_ORIGINAL_URI'] : ''; + if (preg_match('/^(\/SOGo|)\/dav.*/', $original_uri) === 1) { + $is_dav = true; + } + elseif (preg_match('/^(\/SOGo|)\/Microsoft-Server-ActiveSync.*/', $original_uri) === 1) { + $is_eas = true; + } + $login_check = check_login($username, $password, array('dav' => $is_dav, 'eas' => $is_eas, 'proxyauth' => true)); if ($login_check === 'user') { header("X-User: $username"); header("X-Auth: Basic ".base64_encode("$username:$password")); @@ -44,6 +52,13 @@ elseif (isset($_GET['login'])) { // register username and password in session $_SESSION[$session_var_user_allowed][] = $login; $_SESSION[$session_var_pass] = $sogo_sso_pass; + // update sasl logs + $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; + $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES ('SSO', 0, :username, :remote_addr)"); + $stmt->execute(array( + ':username' => $login, + ':remote_addr' => $_SERVER['REMOTE_ADDR'] + )); // redirect to sogo (sogo will get the correct credentials via nginx auth_request header("Location: /SOGo/so/${login}"); exit; @@ -55,9 +70,7 @@ elseif (isset($_GET['login'])) { exit; } // only check for admin-login on sogo GUI requests -elseif ( - strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0 -) { +elseif (isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0) { // this is an nginx auth_request call, we check for existing sogo-sso session variables session_start(); // extract email address from "/SOGo/so/user@domain/xy" diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 4243f6ac..e587500a 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -170,7 +170,7 @@ function recursiveBase64StrToArrayBuffer(obj) { // TFA, CSRF, Alerts in footer.inc.php // Other general functions in mailcow.js {% for alert_type, alert_msg in alerts %} - mailcow_alert_box('{{ alert_msg|raw }}', '{{ alert_type }}'); + mailcow_alert_box('{{ alert_msg|raw|replace({"\n": "", "\r": "", "\t": "
    "}) }}', '{{ alert_type }}'); {% endfor %} // Confirm TFA modal diff --git a/data/web/templates/edit/app-passwd.twig b/data/web/templates/edit/app-passwd.twig index d220c47e..d7bb617a 100644 --- a/data/web/templates/edit/app-passwd.twig +++ b/data/web/templates/edit/app-passwd.twig @@ -5,6 +5,7 @@

    {{ lang.edit.app_passwd }}

    +
    @@ -30,6 +31,17 @@
    +
    + +
    + +
    +
    diff --git a/data/web/templates/modals/user.twig b/data/web/templates/modals/user.twig index c3b4086c..13065839 100644 --- a/data/web/templates/modals/user.twig +++ b/data/web/templates/modals/user.twig @@ -213,6 +213,17 @@

    {{ lang.user.new_password_description }}

    +
    + +
    + +
    +
    diff --git a/data/web/templates/user/tab-user-auth.twig b/data/web/templates/user/tab-user-auth.twig index 0dbfbb00..5687f5a0 100644 --- a/data/web/templates/user/tab-user-auth.twig +++ b/data/web/templates/user/tab-user-auth.twig @@ -11,7 +11,7 @@ {{ lang.user.open_webmail_sso }} {% else %} - + {{ lang.user.open_webmail_sso }} {% endif %} diff --git a/docker-compose.yml b/docker-compose.yml index 9269421b..753271ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -211,7 +211,7 @@ services: - sogo dovecot-mailcow: - image: mailcow/dovecot:1.157 + image: mailcow/dovecot:1.158 depends_on: - mysql-mailcow dns: