public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
Merge remote-tracking branch 'origin/staging' into nightly
962ac39e
.github/PULL_REQUEST_TEMPLATE.md | 38 ++
CONTRIBUTING.md | 33 +-
data/assets/templates/pw_reset_html.tpl | 29 ++
data/assets/templates/pw_reset_text.tpl | 11 +
data/web/admin.php | 1 +
data/web/inc/functions.inc.php | 438 ++++++++++++++++++++-
data/web/inc/functions.mailbox.inc.php | 115 +++---
data/web/inc/init_db.inc.php | 16 +-
data/web/inc/triggers.inc.php | 60 ++-
data/web/inc/vars.inc.php | 6 +
data/web/js/site/mailbox.js | 3 +
data/web/json_api.php | 4 +-
data/web/lang/lang.de-de.json | 26 ++
data/web/lang/lang.en-gb.json | 26 ++
data/web/reset-password.php | 31 ++
data/web/templates/admin.twig | 4 +-
.../admin/tab-config-password-policy.twig | 40 --
.../admin/tab-config-password-settings.twig | 102 +++++
data/web/templates/edit/mailbox-templates.twig | 1 +
data/web/templates/edit/mailbox.twig | 7 +
data/web/templates/index.twig | 3 +
data/web/templates/modals/mailbox.twig | 2 +
data/web/templates/modals/user.twig | 27 ++
data/web/templates/reset-password.twig | 57 +++
data/web/templates/user/tab-user-auth.twig | 5 +-
25 files changed, 958 insertions(+), 127 deletions(-)
create mode 100644 .github/PULL_REQUEST_TEMPLATE.md
create mode 100644 data/assets/templates/pw_reset_html.tpl
create mode 100644 data/assets/templates/pw_reset_text.tpl
create mode 100644 data/web/reset-password.php
delete mode 100644 data/web/templates/admin/tab-config-password-policy.twig
create mode 100644 data/web/templates/admin/tab-config-password-settings.twig
create mode 100644 data/web/templates/reset-password.twig
Diff
diff --cc data/web/inc/functions.inc.php
index 4d63e04c,25d08b9f..97f97a42
--- a/data/web/inc/functions.inc.php
+++ b/data/web/inc/functions.inc.php
@@@ -918,20 -1092,24 +922,24 @@@ function edit_user_account($_data)
);
return false;
}
- $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
- WHERE `kind` NOT REGEXP 'location|thing|group'
- AND `username` = :user AND authsource = 'mailcow'");
- $stmt->execute(array(':user' => $username));
- $row = $stmt->fetch(PDO::FETCH_ASSOC);
- if (!verify_hash($row['password'], $password_old)) {
- $_SESSION['return'][] = array(
- 'type' => 'danger',
- 'log' => array(__FUNCTION__, $_data_log),
- 'msg' => 'access_denied'
- );
- return false;
- }
- if (!empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
+
+ // edit password
+ if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
+ $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
+ WHERE `kind` NOT REGEXP 'location|thing|group'
- AND `username` = :user");
++ AND `username` = :user AND authsource = 'mailcow'");
+ $stmt->execute(array(':user' => $username));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!verify_hash($row['password'], $password_old)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+
$password_new = $_data['user_new_pass'];
$password_new2 = $_data['user_new_pass2'];
if (password_check($password_new, $password_new2) !== true) {
@@@ -946,8 -1124,29 +954,29 @@@
':password_hashed' => $password_hashed,
':username' => $username
));
+
+ update_sogo_static_view();
+ }
+ // edit password recovery email
+ elseif (isset($pw_recovery_email)) {
+ if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+
+ $pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email;
+ $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
- WHERE `username` = :username");
++ WHERE `username` = :username AND authsource = 'mailcow'");
+ $stmt->execute(array(
+ ':recovery_email' => $pw_recovery_email,
+ ':username' => $username
+ ));
}
- update_sogo_static_view();
+
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_data_log),
@@@ -2097,573 -2290,387 +2126,952 @@@ function uuid4()
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
+function identity_provider($_action = null, $_data = null, $_extra = null) {
+ global $pdo;
+ global $iam_provider;
+
+ $data_log = $_data;
+ if (isset($data_log['client_secret'])) $data_log['client_secret'] = '*';
+ if (isset($data_log['access_token'])) $data_log['access_token'] = '*';
+
+ switch ($_action) {
+ case NULL:
+ if ($iam_provider) {
+ return $iam_provider;
+ } else {
+ $iam_provider = identity_provider("init");
+ return $iam_provider;
+ }
+ break;
+ case 'get':
+ $settings = array();
+ $stmt = $pdo->prepare("SELECT * FROM `identity_provider`;");
+ $stmt->execute();
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ foreach($rows as $row){
+ switch ($row["key"]) {
+ case "mappers":
+ case "templates":
+ $settings[$row["key"]] = json_decode($row["value"]);
+ break;
+ case "use_ssl":
+ case "use_tls":
+ case "ignore_ssl_errors":
+ $settings[$row["key"]] = boolval($row["value"]);
+ break;
+ default:
+ $settings[$row["key"]] = $row["value"];
+ break;
+ }
+ }
+ // return default client_scopes for generic-oidc if none is set
+ if ($settings["authsource"] == "generic-oidc" && empty($settings["client_scopes"])){
+ $settings["client_scopes"] = "openid profile email";
+ }
+ if ($_extra['hide_sensitive']){
+ $settings['client_secret'] = '';
+ $settings['access_token'] = '';
+ }
+ // return default ldap options
+ if ($settings["authsource"] == "ldap"){
+ $settings['use_ssl'] = !isset($settings['use_ssl']) ? false : $settings['use_ssl'];
+ $settings['use_tls'] = !isset($settings['use_tls']) ? false : $settings['use_tls'];
+ $settings['ignore_ssl_errors'] = !isset($settings['ignore_ssl_errors']) ? false : $settings['ignore_ssl_errors'];
+ }
+ return $settings;
+ break;
+ case 'edit':
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+ if (!isset($_data['authsource'])){
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $data_log),
+ 'msg' => array('required_data_missing', '')
+ );
+ return false;
+ }
+
+ $available_authsources = array(
+ "keycloak",
+ "generic-oidc",
+ "ldap"
+ );
+ $_data['authsource'] = strtolower($_data['authsource']);
+ if (!in_array($_data['authsource'], $available_authsources)){
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $data_log),
+ 'msg' => array('invalid_authsource', $setting)
+ );
+ return false;
+ }
+
+ $stmt = $pdo->prepare("SELECT * FROM `mailbox`
+ WHERE `authsource` != 'mailcow'
+ AND `authsource` IS NOT NULL
+ AND `authsource` != :authsource");
+ $stmt->execute(array(':authsource' => $_data['authsource']));
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ if ($rows) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $data_log),
+ 'msg' => array('authsource_in_use', $setting)
+ );
+ return false;
+ }
+
+ switch ($_data['authsource']) {
+ case "keycloak":
+ $_data['server_url'] = (!empty($_data['server_url'])) ? rtrim($_data['server_url'], '/') : null;
+ $_data['mailpassword_flow'] = isset($_data['mailpassword_flow']) ? intval($_data['mailpassword_flow']) : 0;
+ $_data['periodic_sync'] = isset($_data['periodic_sync']) ? intval($_data['periodic_sync']) : 0;
+ $_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
+ $_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
+ $_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
+ $required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval');
+ break;
+ case "generic-oidc":
+ $_data['authorize_url'] = (!empty($_data['authorize_url'])) ? $_data['authorize_url'] : null;
+ $_data['token_url'] = (!empty($_data['token_url'])) ? $_data['token_url'] : null;
+ $_data['userinfo_url'] = (!empty($_data['userinfo_url'])) ? $_data['userinfo_url'] : null;
+ $_data['client_scopes'] = (!empty($_data['client_scopes'])) ? $_data['client_scopes'] : "openid profile email";
+ $required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes');
+ break;
+ case "ldap":
+ $_data['port'] = (!empty($_data['port'])) ? intval($_data['port']) : 389;
+ $_data['username_field'] = (!empty($_data['username_field'])) ? $_data['username_field'] : "mail";
+ $_data['filter'] = (!empty($_data['filter'])) ? $_data['filter'] : "";
+ $_data['periodic_sync'] = isset($_data['periodic_sync']) ? intval($_data['periodic_sync']) : 0;
+ $_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
+ $_data['use_ssl'] = isset($_data['use_ssl']) ? boolval($_data['use_ssl']) : false;
+ $_data['use_tls'] = isset($_data['use_tls']) && !$_data['use_ssl'] ? boolval($_data['use_tls']) : false;
+ $_data['ignore_ssl_error'] = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false;
+ $_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
+ $_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
+ $required_settings = array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error');
+ break;
+ }
+
+ $pdo->beginTransaction();
+ $stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES (:key, :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
+ // add connection settings
+ foreach($required_settings as $setting){
+ if (!isset($_data[$setting])){
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $data_log),
+ 'msg' => array('required_data_missing', $setting)
+ );
+ $pdo->rollback();
+ return false;
+ }
+
+ $stmt->bindParam(':key', $setting);
+ $stmt->bindParam(':value', $_data[$setting]);
+ $stmt->execute();
+ }
+ $pdo->commit();
+
+ // add mappers
+ if ($_data['mappers'] && $_data['templates']){
+ $_data['mappers'] = (!is_array($_data['mappers'])) ? array($_data['mappers']) : $_data['mappers'];
+ $_data['templates'] = (!is_array($_data['templates'])) ? array($_data['templates']) : $_data['templates'];
+
+ $mappers = array_filter($_data['mappers']);
+ $templates = array_filter($_data['templates']);
+ if (count($mappers) == count($templates)){
+ $mappers = json_encode($mappers);
+ $templates = json_encode($templates);
+
+ $stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('mappers', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
+ $stmt->bindParam(':value', $mappers);
+ $stmt->execute();
+ $stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('templates', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
+ $stmt->bindParam(':value', $templates);
+ $stmt->execute();
+ }
+ }
+
+ // delete old access_token
+ $stmt = $pdo->query("INSERT INTO identity_provider (`key`, `value`) VALUES ('access_token', '') ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
+
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $data_log),
+ 'msg' => array('object_modified', '')
+ );
+ return true;
+ break;
+ case 'test':
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+
+ 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'];
+ }
+ $req = http_build_query(array(
+ 'grant_type' => 'client_credentials',
+ 'client_id' => $_data['client_id'],
+ 'client_secret' => $_data['client_secret']
+ ));
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, $url);
+ curl_setopt($curl, CURLOPT_TIMEOUT, 7);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ 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);
+ $res = curl_exec($curl);
+ $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close ($curl);
+
+ if ($code != 200) {
+ return false;
+ }
+ break;
+ case 'ldap':
+ if (!$_data['host'] || !$_data['port'] || !$_data['basedn'] ||
+ !$_data['binddn'] || !$_data['bindpass']){
+ return false;
+ }
+ $_data['use_ssl'] = isset($_data['use_ssl']) ? boolval($_data['use_ssl']) : false;
+ $_data['use_tls'] = isset($_data['use_tls']) && !$_data['use_ssl'] ? boolval($_data['use_tls']) : false;
+ $_data['ignore_ssl_error'] = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false;
+ $options = array();
+ if ($_data['ignore_ssl_error']) {
+ $options['LDAP_OPT_X_TLS_REQUIRE_CERT'] = "LDAP_OPT_X_TLS_NEVER";
+ }
+ $provider = new \LdapRecord\Connection([
+ 'hosts' => [$_data['host']],
+ 'port' => $_data['port'],
+ 'base_dn' => $_data['basedn'],
+ 'username' => $_data['binddn'],
+ 'password' => $_data['bindpass'],
+ 'use_ssl' => $_data['use_ssl'],
+ 'use_tls' => $_data['use_tls'],
+ 'options' => $options
+ ]);
+ try {
+ $provider->connect();
+ } catch (TypeError $e) {
+ return false;
+ } catch (\LdapRecord\Auth\BindException $e) {
+ return false;
+ }
+ break;
+ }
+
+ return true;
+ break;
+ case "delete":
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+
+ $stmt = $pdo->query("SELECT * FROM `mailbox`
+ WHERE `authsource` != 'mailcow'
+ AND `authsource` IS NOT NULL");
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ if ($rows) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $data_log),
+ 'msg' => array('authsource_in_use', $setting)
+ );
+ return false;
+ }
+
+ $stmt = $pdo->query("DELETE FROM identity_provider;");
+
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $data_log),
+ 'msg' => array('item_deleted', '')
+ );
+ return true;
+ break;
+ case "init":
+ $iam_settings = identity_provider('get');
+ $provider = null;
+
+ switch ($iam_settings['authsource']) {
+ case "keycloak":
+ if ($iam_settings['server_url'] && $iam_settings['realm'] && $iam_settings['client_id'] &&
+ $iam_settings['client_secret'] && $iam_settings['redirect_url'] && $iam_settings['version']){
+ $provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
+ 'authServerUrl' => $iam_settings['server_url'],
+ 'realm' => $iam_settings['realm'],
+ 'clientId' => $iam_settings['client_id'],
+ 'clientSecret' => $iam_settings['client_secret'],
+ 'redirectUri' => $iam_settings['redirect_url'],
+ 'version' => $iam_settings['version'],
+ // 'encryptionAlgorithm' => 'RS256', // optional
+ // 'encryptionKeyPath' => '../key.pem' // optional
+ // 'encryptionKey' => 'contents_of_key_or_certificate' // optional
+ ]);
+ }
+ break;
+ case "generic-oidc":
+ if ($iam_settings['client_id'] && $iam_settings['client_secret'] && $iam_settings['redirect_url'] &&
+ $iam_settings['authorize_url'] && $iam_settings['token_url'] && $iam_settings['userinfo_url']){
+ $provider = new \League\OAuth2\Client\Provider\GenericProvider([
+ 'clientId' => $iam_settings['client_id'],
+ 'clientSecret' => $iam_settings['client_secret'],
+ 'redirectUri' => $iam_settings['redirect_url'],
+ 'urlAuthorize' => $iam_settings['authorize_url'],
+ 'urlAccessToken' => $iam_settings['token_url'],
+ 'urlResourceOwnerDetails' => $iam_settings['userinfo_url'],
+ 'scopes' => $iam_settings['client_scopes']
+ ]);
+ }
+ break;
+ case "ldap":
+ if ($iam_settings['host'] && $iam_settings['port'] && $iam_settings['basedn'] &&
+ $iam_settings['binddn'] && $iam_settings['bindpass']){
+ $options = array();
+ if ($iam_settings['ignore_ssl_error']) {
+ $options['LDAP_OPT_X_TLS_REQUIRE_CERT'] = "LDAP_OPT_X_TLS_NEVER";
+ }
+ $provider = new \LdapRecord\Connection([
+ 'hosts' => [$iam_settings['host']],
+ 'port' => $iam_settings['port'],
+ 'base_dn' => $iam_settings['basedn'],
+ 'username' => $iam_settings['binddn'],
+ 'password' => $iam_settings['bindpass'],
+ 'use_ssl' => $iam_settings['use_ssl'],
+ 'use_tls' => $iam_settings['use_tls'],
+ 'options' => $options
+ ]);
+ try {
+ $provider->connect();
+ } catch (Throwable $e) {
+ $provider = null;
+ }
+ }
+ break;
+ }
+
+ return $provider;
+ break;
+ case "verify-sso":
+ $provider = $_data['iam_provider'];
+ $iam_settings = identity_provider('get');
+ if ($iam_settings['authsource'] != 'keycloak' && $iam_settings['authsource'] != 'generic-oidc'){
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => array('login_failed', "no OIDC provider configured")
+ );
+ return false;
+ }
+
+ try {
+ $token = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']]);
+ $_SESSION['iam_token'] = $token->getToken();
+ $_SESSION['iam_refresh_token'] = $token->getRefreshToken();
+ $info = $provider->getResourceOwner($token)->toArray();
+ } catch (Throwable $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => array('login_failed', $e->getMessage())
+ );
+ return false;
+ }
+ // check if email address is given
+ if (empty($info['email'])) return false;
+
+ // token valid, get mailbox
+ $stmt = $pdo->prepare("SELECT * FROM `mailbox`
+ INNER JOIN domain on mailbox.domain = domain.domain
+ WHERE `kind` NOT REGEXP 'location|thing|group'
+ AND `mailbox`.`active`='1'
+ AND `domain`.`active`='1'
+ AND `username` = :user
+ AND (`authsource`='keycloak' OR `authsource`='generic-oidc')");
+ $stmt->execute(array(':user' => $info['email']));
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($row){
+ // success
+ set_user_loggedin_session($info['email']);
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role']),
+ 'msg' => array('logged_in_as', $_SESSION['mailcow_cc_username'])
+ );
+ return true;
+ }
+
+ // get mapped template, if not set return false
+ // also return false if no mappers were defined
+ $user_template = $info['mailcow_template'];
+ if (empty($iam_settings['mappers']) || empty($user_template)){
+ clear_session();
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $info['email']),
+ 'msg' => array('login_failed', 'empty attribute mapping or missing template attribute')
+ );
+ return false;
+ }
+
+ // check if matching attribute exist
+ $mapper_key = array_search($user_template, $iam_settings['mappers']);
+ if ($mapper_key === false) {
+ clear_session();
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $info['email']),
+ 'msg' => array('login_failed', 'specified template not found')
+ );
+ return false;
+ }
+
+ // create mailbox
+ $create_res = mailbox('add', 'mailbox_from_template', array(
+ 'domain' => explode('@', $info['email'])[1],
+ 'local_part' => explode('@', $info['email'])[0],
+ 'name' => $info['firstName'] . " " . $info['lastName'],
+ 'authsource' => $iam_settings['authsource'],
+ 'template' => $iam_settings['templates'][$mapper_key]
+ ));
+ if (!$create_res){
+ clear_session();
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $info['email']),
+ 'msg' => array('login_failed', 'mailbox creation failed')
+ );
+ return false;
+ }
+
+ set_user_loggedin_session($info['email']);
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role']),
+ 'msg' => array('logged_in_as', $_SESSION['mailcow_cc_username'])
+ );
+ return true;
+ break;
+ case "refresh-token":
+ $provider = $_data['iam_provider'];
+
+ try {
+ $token = $provider->getAccessToken('refresh_token', ['refresh_token' => $_SESSION['iam_refresh_token']]);
+ $_SESSION['iam_token'] = $token->getToken();
+ $_SESSION['iam_refresh_token'] = $token->getRefreshToken();
+ $info = $provider->getResourceOwner($token)->toArray();
+ } catch (Throwable $e) {
+ clear_session();
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__),
+ 'msg' => array('refresh_login_failed', $e->getMessage())
+ );
+ return false;
+ }
+
+ if (empty($info['email'])){
+ clear_session();
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role']),
+ 'msg' => 'refresh_login_failed'
+ );
+ return false;
+ }
+
+ $_SESSION['mailcow_cc_username'] = $info['email'];
+ $_SESSION['mailcow_cc_role'] = "user";
+ return true;
+ break;
+ case "get-redirect":
+ $iam_settings = identity_provider('get');
+ if ($iam_settings['authsource'] != 'keycloak' && $iam_settings['authsource'] != 'generic-oidc')
+ return false;
+ $provider = $_data['iam_provider'];
+ $authUrl = $provider->getAuthorizationUrl();
+ $_SESSION['oauth2state'] = $provider->getState();
+ return $authUrl;
+ break;
+ case "get-keycloak-admin-token":
+ // get access_token for service account of mailcow client
+ $iam_settings = identity_provider('get');
+ if ($iam_settings['authsource'] !== 'keycloak') return false;
+ if (isset($iam_settings['access_token'])) {
+ // check if access_token is valid
+ $url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token/introspect";
+ $req = http_build_query(array(
+ 'token' => $iam_settings['access_token'],
+ 'client_id' => $iam_settings['client_id'],
+ 'client_secret' => $iam_settings['client_secret']
+ ));
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, $url);
+ curl_setopt($curl, CURLOPT_TIMEOUT, 7);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ 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);
+ curl_setopt($curl, CURLOPT_TIMEOUT, 5);
+ $res = json_decode(curl_exec($curl), true);
+ $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close ($curl);
+ if ($code == 200 && $res['active'] == true) {
+ // token is valid
+ return $iam_settings['access_token'];
+ }
+ }
+
+ $url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token";
+ $req = http_build_query(array(
+ 'grant_type' => 'client_credentials',
+ 'client_id' => $iam_settings['client_id'],
+ 'client_secret' => $iam_settings['client_secret']
+ ));
+ $curl = curl_init();
+ curl_setopt($curl, CURLOPT_URL, $url);
+ curl_setopt($curl, CURLOPT_TIMEOUT, 7);
+ curl_setopt($curl, CURLOPT_POST, 1);
+ 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);
+ curl_setopt($curl, CURLOPT_TIMEOUT, 5);
+ $res = json_decode(curl_exec($curl), true);
+ $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+ curl_close ($curl);
+ if ($code != 200) {
+ return false;
+ }
+
+ $stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES (:key, :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
+ $stmt->execute(array(
+ ':key' => 'access_token',
+ ':value' => $res['access_token']
+ ));
+ return $res['access_token'];
+ break;
+ }
+}
+ function reset_password($action, $data = null) {
+ global $pdo;
+ global $redis;
+ global $mailcow_hostname;
+ global $PW_RESET_TOKEN_LIMIT;
+ global $PW_RESET_TOKEN_LIFETIME;
+
+ $_data_log = $data;
+ if (isset($_data_log['new_password'])) $_data_log['new_password'] = '*';
+ if (isset($_data_log['new_password2'])) $_data_log['new_password2'] = '*';
+
+ switch ($action) {
+ case 'check':
+ $token = $data;
+
- $stmt = $pdo->prepare("SELECT `t1`.`username` FROM `reset_password` AS `t1` JOIN `mailbox` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL :lifetime MINUTE) AND `t2`.`active` = 1;");
++ $stmt = $pdo->prepare("SELECT `t1`.`username` FROM `reset_password` AS `t1` JOIN `mailbox` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL :lifetime MINUTE) AND `t2`.`active` = 1 AND `t2`.`authsource` = 'mailcow';");
+ $stmt->execute(array(
+ ':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token),
+ ':lifetime' => $PW_RESET_TOKEN_LIFETIME
+ ));
+ $return = $stmt->fetch(PDO::FETCH_ASSOC);
+ return empty($return['username']) ? false : $return['username'];
+ break;
+ case 'issue':
+ $username = $data;
+
+ // perform cleanup
+ $stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE created < DATE_SUB(NOW(), INTERVAL :lifetime MINUTE);");
+ $stmt->execute(array(':lifetime' => $PW_RESET_TOKEN_LIFETIME));
+
+ if (filter_var($username, FILTER_VALIDATE_EMAIL) === false) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+
+ $pw_reset_notification = reset_password('get_notification', 'raw');
+ if (!$pw_reset_notification) return false;
+ if (empty($pw_reset_notification['from']) || empty($pw_reset_notification['subject'])) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => 'password_reset_na'
+ );
+ return false;
+ }
+
+ $stmt = $pdo->prepare("SELECT * FROM `mailbox`
- WHERE `username` = :username");
++ WHERE `username` = :username AND authsource = 'mailcow'");
+ $stmt->execute(array(':username' => $username));
+ $mailbox_data = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (empty($mailbox_data)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => 'password_reset_invalid_user'
+ );
+ return false;
+ }
+
+ $mailbox_attr = json_decode($mailbox_data['attributes'], true);
+ if (empty($mailbox_attr['recovery_email']) || filter_var($mailbox_attr['recovery_email'], FILTER_VALIDATE_EMAIL) === false) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => "password_reset_invalid_user"
+ );
+ return false;
+ }
+
+ $stmt = $pdo->prepare("SELECT * FROM `reset_password`
+ WHERE `username` = :username");
+ $stmt->execute(array(':username' => $username));
+ $generated_token_count = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+ if ($generated_token_count >= $PW_RESET_TOKEN_LIMIT) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => "reset_token_limit_exceeded"
+ );
+ return false;
+ }
+
+ $token = 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 `reset_password` (`username`, `token`)
+ VALUES (:username, :token)");
+ $stmt->execute(array(
+ ':username' => $username,
+ ':token' => $token
+ ));
+
+ $reset_link = getBaseURL() . "/reset-password?token=" . $token;
+
+ $request_date = new DateTime();
+ $locale_date = locale_get_default();
+ $date_formatter = new IntlDateFormatter(
+ $locale_date,
+ IntlDateFormatter::FULL,
+ IntlDateFormatter::FULL
+ );
+ $formatted_request_date = $date_formatter->format($request_date);
+
+ // set template vars
+ // subject
+ $pw_reset_notification['subject'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['subject']);
+ $pw_reset_notification['subject'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['subject']);
+ $pw_reset_notification['subject'] = str_replace('{{username}}', $username, $pw_reset_notification['subject']);
+ $pw_reset_notification['subject'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['subject']);
+ $pw_reset_notification['subject'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['subject']);
+ $pw_reset_notification['subject'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['subject']);
+ // text
+ $pw_reset_notification['text_tmpl'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['text_tmpl']);
+ $pw_reset_notification['text_tmpl'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['text_tmpl']);
+ $pw_reset_notification['text_tmpl'] = str_replace('{{username}}', $username, $pw_reset_notification['text_tmpl']);
+ $pw_reset_notification['text_tmpl'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['text_tmpl']);
+ $pw_reset_notification['text_tmpl'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['text_tmpl']);
+ $pw_reset_notification['text_tmpl'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['text_tmpl']);
+ // html
+ $pw_reset_notification['html_tmpl'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['html_tmpl']);
+ $pw_reset_notification['html_tmpl'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['html_tmpl']);
+ $pw_reset_notification['html_tmpl'] = str_replace('{{username}}', $username, $pw_reset_notification['html_tmpl']);
+ $pw_reset_notification['html_tmpl'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['html_tmpl']);
+ $pw_reset_notification['html_tmpl'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['html_tmpl']);
+ $pw_reset_notification['html_tmpl'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['html_tmpl']);
+
+
+ $email_sent = reset_password('send_mail', array(
+ "from" => $pw_reset_notification['from'],
+ "to" => $mailbox_attr['recovery_email'],
+ "subject" => $pw_reset_notification['subject'],
+ "text" => $pw_reset_notification['text_tmpl'],
+ "html" => $pw_reset_notification['html_tmpl']
+ ));
+
+ if (!$email_sent){
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => "recovery_email_failed"
+ );
+ return false;
+ }
+
+ list($localPart, $domainPart) = explode('@', $mailbox_attr['recovery_email']);
+ if (strlen($localPart) > 1) {
+ $maskedLocalPart = $localPart[0] . str_repeat('*', strlen($localPart) - 1);
+ } else {
+ $maskedLocalPart = "*";
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => array("recovery_email_sent", $maskedLocalPart . '@' . $domainPart)
+ );
+ return array(
+ "username" => $username,
+ "issue" => "success"
+ );
+ break;
+ case 'reset':
+ $token = $data['token'];
+ $new_password = $data['new_password'];
+ $new_password2 = $data['new_password2'];
+ $username = $data['username'];
+ $check_tfa = $data['check_tfa'];
+
+ if (!$username || !$token) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => 'invalid_reset_token'
+ );
+ return false;
+ }
+
+ # check new password
+ if (!password_check($new_password, $new_password2)) {
+ return false;
+ }
+
+ if ($check_tfa){
+ // check for tfa authenticators
+ $authenticators = get_tfa($username);
+ if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
+ $_SESSION['pending_mailcow_cc_username'] = $username;
+ $_SESSION['pending_pw_reset_token'] = $token;
+ $_SESSION['pending_pw_new_password'] = $new_password;
+ $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+ $_SESSION['return'][] = array(
+ 'type' => 'info',
+ 'log' => array(__FUNCTION__, $user, '*'),
+ 'msg' => 'awaiting_tfa_confirmation'
+ );
+ return false;
+ }
+ }
+
+ # set new password
+ $password_hashed = hash_password($new_password);
+ $stmt = $pdo->prepare("UPDATE `mailbox` SET
+ `password` = :password_hashed,
+ `attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW())
- WHERE `username` = :username");
++ WHERE `username` = :username AND authsource = 'mailcow'");
+ $stmt->execute(array(
+ ':password_hashed' => $password_hashed,
+ ':username' => $username
+ ));
+
+ // perform cleanup
+ $stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE `username` = :username;");
+ $stmt->execute(array(
+ ':username' => $username
+ ));
+
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => 'password_changed_success'
+ );
+ return true;
+ break;
+ case 'get_notification':
+ $type = $data;
+
+ try {
+ $settings['from'] = $redis->Get('PW_RESET_FROM');
+ $settings['subject'] = $redis->Get('PW_RESET_SUBJ');
+ $settings['html_tmpl'] = $redis->Get('PW_RESET_HTML');
+ $settings['text_tmpl'] = $redis->Get('PW_RESET_TEXT');
+ if (empty($settings['html_tmpl']) && empty($settings['text_tmpl'])) {
+ $settings['html_tmpl'] = file_get_contents("/tpls/pw_reset_html.tpl");
+ $settings['text_tmpl'] = file_get_contents("/tpls/pw_reset_text.tpl");
+ }
+
+ if ($type != "raw") {
+ $settings['html_tmpl'] = htmlspecialchars($settings['html_tmpl']);
+ $settings['text_tmpl'] = htmlspecialchars($settings['text_tmpl']);
+ }
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+
+ return $settings;
+ break;
+ case 'send_mail':
+ $from = $data['from'];
+ $to = $data['to'];
+ $text = $data['text'];
+ $html = $data['html'];
+ $subject = $data['subject'];
+
+ if (!filter_var($from, FILTER_VALIDATE_EMAIL)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => 'from_invalid'
+ );
+ return false;
+ }
+ if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => 'to_invalid'
+ );
+ return false;
+ }
+ if (empty($subject)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => 'subject_empty'
+ );
+ return false;
+ }
+ if (empty($text)) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => 'text_empty'
+ );
+ return false;
+ }
+
+ ini_set('max_execution_time', 0);
+ ini_set('max_input_time', 0);
+ $mail = new PHPMailer;
+ $mail->Timeout = 10;
+ $mail->SMTPOptions = array(
+ 'ssl' => array(
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'allow_self_signed' => true
+ )
+ );
+ $mail->isSMTP();
+ $mail->Host = 'postfix-mailcow';
+ $mail->SMTPAuth = false;
+ $mail->Port = 25;
+ $mail->setFrom($from);
+ $mail->Subject = $subject;
+ $mail->CharSet ="UTF-8";
+ if (!empty($html)) {
+ $mail->Body = $html;
+ $mail->AltBody = $text;
+ }
+ else {
+ $mail->Body = $text;
+ }
+ $mail->XMailer = 'MooMail';
+ $mail->AddAddress($to);
+ if (!$mail->send()) {
+ return false;
+ }
+ $mail->ClearAllRecipients();
+
+ return true;
+ break;
+ }
+
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => 'access_denied'
+ );
+ return false;
+ }
+
+ switch ($action) {
+ case 'edit_notification':
+ $subject = $data['subject'];
+ $from = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $data['from']);
+
+ $from = (!filter_var($from, FILTER_VALIDATE_EMAIL)) ? "" : $from;
+ $subject = (empty($subject)) ? "" : $subject;
+ $text = (empty($data['text_tmpl'])) ? "" : $data['text_tmpl'];
+ $html = (empty($data['html_tmpl'])) ? "" : $data['html_tmpl'];
+
+ try {
+ $redis->Set('PW_RESET_FROM', $from);
+ $redis->Set('PW_RESET_SUBJ', $subject);
+ $redis->Set('PW_RESET_HTML', $html);
+ $redis->Set('PW_RESET_TEXT', $text);
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $action, $_data_log),
+ 'msg' => 'saved_settings'
+ );
+ break;
+ }
+ }
-
-
+function clear_session(){
+ session_regenerate_id(true);
+ session_unset();
+ session_destroy();
+ session_write_close();
+}
+function set_user_loggedin_session($user) {
+ $_SESSION['mailcow_cc_username'] = $user;
+ $_SESSION['mailcow_cc_role'] = 'user';
+ $sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
+ $_SESSION['sogo-sso-user-allowed'][] = $user;
+ $_SESSION['sogo-sso-pass'] = $sogo_sso_pass;
+ unset($_SESSION['pending_mailcow_cc_username']);
+ unset($_SESSION['pending_mailcow_cc_role']);
+ unset($_SESSION['pending_tfa_methods']);
+}
function get_logs($application, $lines = false) {
if ($lines === false) {
$lines = $GLOBALS['LOG_LINES'] - 1;
diff --cc data/web/inc/functions.mailbox.inc.php
index 09951bed,c927ce49..c0517d84
--- a/data/web/inc/functions.mailbox.inc.php
+++ b/data/web/inc/functions.mailbox.inc.php
@@@ -2927,26 -2870,22 +2932,27 @@@ function mailbox($_action, $_type, $_da
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
}
if (!empty($is_now)) {
- $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
+ $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
- (int)$sogo_access = (isset($_data['sogo_access']) && isset($_SESSION['acl']['sogo_access']) && $_SESSION['acl']['sogo_access'] == "1") ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
- (int)$imap_access = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
- (int)$pop3_access = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
- (int)$smtp_access = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
- (int)$sieve_access = (isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
- (int)$relayhost = (isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
- (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
- $name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
- $domain = $is_now['domain'];
- $quota_b = $quota_m * 1048576;
- $password = (!empty($_data['password'])) ? $_data['password'] : null;
- $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
- $tags = (is_array($_data['tags']) ? $_data['tags'] : array());
- $attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
- $authsource = $is_now['authsource'];
+ (int)$sogo_access = (isset($_data['sogo_access']) && isset($_SESSION['acl']['sogo_access']) && $_SESSION['acl']['sogo_access'] == "1") ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
+ (int)$imap_access = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
+ (int)$pop3_access = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
+ (int)$smtp_access = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
+ (int)$sieve_access = (isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
+ (int)$relayhost = (isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
+ (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
+ $name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
+ $domain = $is_now['domain'];
+ $quota_b = $quota_m * 1048576;
+ $password = (!empty($_data['password'])) ? $_data['password'] : null;
+ $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
- $pw_recovery_email = (isset($_data['pw_recovery_email'])) ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
+ $tags = (is_array($_data['tags']) ? $_data['tags'] : array());
++ $attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
++ $authsource = $is_now['authsource'];
+ if (in_array($_data['authsource'], array('mailcow', 'keycloak', 'generic-oidc', 'ldap'))){
+ $authsource = $_data['authsource'];
+ }
++ $pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
}
else {
$_SESSION['return'][] = array(
@@@ -3199,35 -3138,43 +3205,47 @@@
':address' => $username,
':active' => $active
));
- $stmt = $pdo->prepare("UPDATE `mailbox` SET
- `active` = :active,
- `name`= :name,
- `quota` = :quota_b,
- `authsource` = :authsource,
- `attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
- `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
- `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
- `attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
- `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
- `attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
- `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
- `attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
- WHERE `username` = :username");
- $stmt->execute(array(
- ':active' => $active,
- ':name' => $name,
- ':quota_b' => $quota_b,
- ':attribute_hash' => $attribute_hash,
- ':force_pw_update' => $force_pw_update,
- ':sogo_access' => $sogo_access,
- ':imap_access' => $imap_access,
- ':pop3_access' => $pop3_access,
- ':sieve_access' => $sieve_access,
- ':smtp_access' => $smtp_access,
- ':relayhost' => $relayhost,
- ':username' => $username,
- ':authsource' => $authsource
- ));
+ try {
+ $stmt = $pdo->prepare("UPDATE `mailbox` SET
+ `active` = :active,
+ `name`= :name,
+ `quota` = :quota_b,
++ `authsource` = :authsource,
+ `attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
+ `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
+ `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
+ `attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
+ `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
+ `attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
+ `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
- `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
++ `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email),
++ `attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
+ WHERE `username` = :username");
- $stmt->execute(array(
- ':active' => $active,
- ':name' => $name,
- ':quota_b' => $quota_b,
- ':force_pw_update' => $force_pw_update,
- ':sogo_access' => $sogo_access,
- ':imap_access' => $imap_access,
- ':pop3_access' => $pop3_access,
- ':sieve_access' => $sieve_access,
- ':smtp_access' => $smtp_access,
- ':recovery_email' => $pw_recovery_email,
- ':relayhost' => $relayhost,
- ':username' => $username
- ));
++ $stmt->execute(array(
++ ':active' => $active,
++ ':name' => $name,
++ ':quota_b' => $quota_b,
++ ':attribute_hash' => $attribute_hash,
++ ':force_pw_update' => $force_pw_update,
++ ':sogo_access' => $sogo_access,
++ ':imap_access' => $imap_access,
++ ':pop3_access' => $pop3_access,
++ ':sieve_access' => $sieve_access,
++ ':smtp_access' => $smtp_access,
++ ':recovery_email' => $pw_recovery_email,
++ ':relayhost' => $relayhost,
++ ':username' => $username,
++ ':authsource' => $authsource
++ ));
+ }
+ catch (PDOException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+ 'msg' => $e->getMessage()
+ );
+ return false;
+ }
// save tags
foreach($tags as $index => $tag){
if (empty($tag)) continue;
diff --cc data/web/inc/init_db.inc.php
index 1dd47fec,8c4951d5..fd196f9e
--- a/data/web/inc/init_db.inc.php
+++ b/data/web/inc/init_db.inc.php
@@@ -3,7 -3,7 +3,7 @@@ function init_db_schema()
try {
global $pdo;
- $db_version = "31072024_1012";
- $db_version = "29072024_1000";
++ $db_version = "15082024_1212";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
diff --cc data/web/inc/triggers.inc.php
index 30198778,5c625e41..8fa5dcd9
--- a/data/web/inc/triggers.inc.php
+++ b/data/web/inc/triggers.inc.php
@@@ -38,45 -10,58 +38,81 @@@ if (!empty($_GET['sso_token']))
}
}
+ if (isset($_POST["pw_reset_request"]) && !empty($_POST['username'])) {
+ reset_password("issue", $_POST['username']);
+ header("Location: /");
+ exit;
+ }
+ if (isset($_POST["pw_reset"])) {
+ $username = reset_password("check", $_POST['token']);
+ $reset_result = reset_password("reset", array(
+ 'new_password' => $_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 (isset($_SESSION['pending_mailcow_cc_username']) && 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']);
+ 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: /");
- exit;
- } else {
+
+ header("Location: /debug");
+ die();
+ }
+ elseif ($_SESSION['pending_mailcow_cc_role'] == "domainadmin") {
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
- $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
+ $_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") {
- 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']}");
++ 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: /user");
++ header("Location: /");
+ die();
+ } else {
- header("Location: /user");
- die();
++ 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();
++ }
+ }
}
- } else {
- 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']);
}
+
+ unset($_SESSION['pending_mailcow_cc_username']);
+ unset($_SESSION['pending_mailcow_cc_role']);
+ unset($_SESSION['pending_tfa_methods']);
}
if (isset($_GET["cancel_tfa_login"])) {
diff --cc data/web/lang/lang.de-de.json
index 821c1551,189774ee..ddbbc342
--- a/data/web/lang/lang.de-de.json
+++ b/data/web/lang/lang.de-de.json
@@@ -1206,7 -1226,7 +1230,8 @@@
"password": "Passwort",
"password_now": "Aktuelles Passwort (Änderungen bestätigen)",
"password_repeat": "Passwort (Wiederholung)",
+ "password_reset_info": "Wenn keine E-Mail zur Passwortwiederherstellung hinterlegt ist, kann diese Funktion nicht genutzt werden.",
+ "protocols": "Protokolle",
"pushover_evaluate_x_prio": "Hohe Priorität eskalieren [<code>X-Priority: 1</code>]",
"pushover_info": "Push-Benachrichtungen werden angewendet auf alle nicht-Spam Nachrichten zugestellt an <b>%s</b>, einschließlich Alias-Adressen (shared, non-shared, tagged).",
"pushover_only_x_prio": "Nur Mail mit hoher Priorität berücksichtigen [<code>X-Priority: 1</code>]",
diff --cc data/web/lang/lang.en-gb.json
index aac2abc8,60044180..36d886f7
--- a/data/web/lang/lang.en-gb.json
+++ b/data/web/lang/lang.en-gb.json
@@@ -485,8 -459,8 +497,9 @@@
"redis_error": "Redis error: %s",
"relayhost_invalid": "Map entry %s is invalid",
"release_send_failed": "Message could not be released: %s",
+ "required_data_missing": "Required data %s is missing",
"reset_f2b_regex": "Regex filter could not be reset in time, please try again or wait a few more seconds and reload the website.",
+ "reset_token_limit_exceeded": "Reset token limit has been exceeded. Please try again later.",
"resource_invalid": "Resource name %s is invalid",
"rl_timeframe": "Rate limit time frame is incorrect",
"rspamd_ui_pw_length": "Rspamd UI password should be at least 6 chars long",
@@@ -1253,7 -1234,7 +1277,8 @@@
"password": "Password",
"password_now": "Current password (confirm changes)",
"password_repeat": "Password (repeat)",
+ "password_reset_info": "If no email for password recovery is provided, this function cannot be used.",
+ "protocols": "Protocols",
"pushover_evaluate_x_prio": "Escalate high priority mail [<code>X-Priority: 1</code>]",
"pushover_info": "Push notification settings will apply to all clean (non-spam) mail delivered to <b>%s</b> including aliases (shared, non-shared, tagged).",
"pushover_only_x_prio": "Only consider high priority mail [<code>X-Priority: 1</code>]",
diff --cc data/web/templates/user/tab-user-auth.twig
index 424d5e94,24c9eb48..0f30d333
--- a/data/web/templates/user/tab-user-auth.twig
+++ b/data/web/templates/user/tab-user-auth.twig
@@@ -43,168 -47,119 +43,171 @@@
{{ mailboxdata.percent_in_use }}%
</div>
</div>
- <p>{{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}∞{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}<br>{{ mailboxdata.messages }} {{ lang.user.messages }}</p>
+
+ <div class="row">
+ <div class="col-12 col-md-3 d-flex">
+ <span class="mt-2 w-100 text-md-end">{{ lang.user.protocols }}:</span>
+ </div>
+ <div class="col-12 col-md-9 d-flex">
+ <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill m-2 ms-0" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.direct_protocol_access|raw }}"></i>
+ <div class="d-flex flex-wrap">
+ {% if mailboxdata.attributes.imap_access == 1 %}<div class="badge fs-6 bg-success m-2">IMAP <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">IMAP <i class="bi bi-x-lg"></i></div>{% endif %}
+ {% if mailboxdata.attributes.smtp_access == 1 %}<div class="badge fs-6 bg-success m-2">SMTP <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">SMTP <i class="bi bi-x-lg"></i></div>{% endif %}
+ {% if mailboxdata.attributes.sieve_access == 1 %}<div class="badge fs-6 bg-success m-2">Sieve <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">Sieve <i class="bi bi-x-lg"></i></div>{% endif %}
+ {% if mailboxdata.attributes.pop3_access == 1 %}<div class="badge fs-6 bg-success m-2">POP3 <i class="bi bi-check-lg"></i></div>{% else %}<div class="badge fs-6 bg-danger m-2">POP3 <i class="bi bi-x-lg"></i></div>{% endif %}
+ </div>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-12 col-md-3 d-flex">
+ <span class="mt-2 w-100 text-md-end">{{ lang.user.apple_connection_profile }}:</span>
+ </div>
+ <div class="col-12 col-md-8">
+ <div class="d-flex">
+ <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_mailonly }}"></i>
+ <a href="/mobileconfig.php?only_email">{{ lang.user.email }} <small>[IMAP, SMTP]</small></a>
+ </div>
+ {% if not skip_sogo %}
+ <div class="d-flex">
+ <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_complete }}"></i>
+ <a href="/mobileconfig.php">{{ lang.user.email_and_dav }} <small>[IMAP, SMTP, Cal/CardDAV]</small></a>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-12 col-md-3 d-flex">
+ <span class="mt-2 w-100 text-md-end">{{ lang.user.apple_connection_profile }}<br>{{ lang.user.with_app_password }}:</span>
+ </div>
+ <div class="col-12 col-md-9">
+ <div class="d-flex">
+ <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_mailonly }} {{ lang.user.apple_connection_profile_with_app_password }}"></i>
+ <a href="/mobileconfig.php?only_email&app_password">{{ lang.user.email }} <small>[IMAP, SMTP]</small></a>
+ </div>
+ {% if not skip_sogo %}
+ <div class="d-flex">
+ <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill me-2" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.apple_connection_profile_complete }} {{ lang.user.apple_connection_profile_with_app_password }}"></i>
+ <a href="/mobileconfig.php?app_password">{{ lang.user.email_and_dav }} <small>[IMAP, SMTP, Cal/CardDAV]</small></a>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-12 d-flex flex-column text-center align-items-center mt-4">
+ <a target="_blank" href="https://mailcow.github.io/mailcow-dockerized-docs/client/client/#{{ clientconfigstr }}">{{ lang.user.client_configuration }}</a>
+ <a class="mt-2" href="#userFilterModal" data-bs-toggle="modal">{{ lang.user.show_sieve_filters }}</a>
+ </div>
+ </div>
+
+ <legend class="mt-4">{{ lang.user.authentication }}</legend>
<hr>
- <p><a href="#pwChangeModal" data-bs-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.change_password }}</a></p>
- {% if acl.pw_reset == 1 %}<p><a href="#pwRecoveryEmailModal" data-bs-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.pw_recovery_email }}</a></p>{% endif %}
- </div>
- </div>
- <hr>
- {# TFA #}
- <div class="row">
- <div class="col-sm-3 col-xs-5 text-right">{{ lang.tfa.tfa }}:</div>
- <div class="col-sm-9 col-xs-7">
- <p id="tfa_pretty">{{ tfa_data.pretty }}</p>
- {% include 'tfa_keys.twig' %}
- <br>
- </div>
- </div>
- <div class="row">
- <div class="col-sm-3 col-xs-5 text-right">{{ lang.tfa.set_tfa }}:</div>
- <div class="col-sm-9 col-xs-7">
- <select data-style="btn btn-sm dropdown-toggle bs-placeholder btn-secondary" data-width="fit" id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}">
- <option value="yubi_otp">{{ lang.tfa.yubi_otp }}</option>
- <option value="webauthn">{{ lang.tfa.webauthn }}</option>
- <option value="totp">{{ lang.tfa.totp }}</option>
- <option value="none">{{ lang.tfa.none }}</option>
- </select>
- </div>
- </div>
- <hr>
- {# FIDO2 #}
- <div class="row">
- <div class="col-sm-3 col-12 text-sm-end text-start">
- <p><i class="bi bi-shield-fill-check"></i> {{ lang.fido2.fido2_auth }}</p>
- </div>
- </div>
- <div class="row">
- <div class="col-sm-3 col-12 text-sm-end text-start mb-4">
- {{ lang.fido2.known_ids }}:
- </div>
- <div class="col-sm-9 col-12">
- <div class="table-responsive">
- <table class="table table-striped table-hover table-condensed" id="fido2_keys">
- <tr>
- <th>ID</th>
- <th style="min-width:240px;text-align: right">{{ lang.admin.action }}</th>
- </tr>
- {% include 'fido2.twig' %}
- </table>
+ {# Password Change #}
+ {% if mailboxdata.authsource == "mailcow" %}
+ <div class="row">
+ <div class="col-12 col-md-3 d-flex"></div>
- <div class="col-12 col-md-9 d-flex flex-wrap justify-content-center justify-content-sm-start">
++ <div class="col-12 col-md-9 d-flex flex-wrap">
+ <a class="btn btn-secondary" href="#pwChangeModal" data-bs-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.change_password }}</a>
++ {% if acl.pw_reset == 1 %}
++ <a class="btn btn-secondary ms-4" href="#pwRecoveryEmailModal" data-bs-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.pw_recovery_email }}</a></p>
++ {% endif %}
+ </div>
</div>
- <br>
- </div>
- </div>
- <div class="row">
- <div class="offset-sm-3 col-sm-9">
- <div class="btn-group nowrap">
- <button class="btn btn-sm btn-primary d-block d-sm-inline" id="register-fido2">{{ lang.fido2.set_fido2 }}</button>
- <button type="button" class="btn btn-sm btn-xs-lg btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
- <ul class="dropdown-menu">
- <li><a class="dropdown-item" href="#" id="register-fido2-touchid"><i class="bi bi-apple"></i> {{ lang.fido2.set_fido2_touchid }}</a></li>
- </ul>
+ {% endif %}
+ {# TFA #}
+ {% if mailboxdata.authsource == "mailcow" or mailboxdata.authsource == "ldap" %}
+ <div class="row mt-4">
+ <div class="col-12 col-md-3 d-flex">
+ <span class="mt-2 w-100 text-md-end">{{ lang.tfa.tfa }}:</span>
+ </div>
+ <div class="col-12 col-md-9 d-flex flex-wrap justify-content-center justify-content-sm-start">
+ <span id="tfa_pretty">{{ tfa_data.pretty }}</span>
+ {% include 'tfa_keys.twig' %}
+ </div>
+ </div>
+ <div class="row mt-2 mb-4">
+ <div class="col-12 col-md-3 d-flex">
+ <span class="mt-2 w-100 text-md-end">{{ lang.tfa.set_tfa }}:</span>
+ </div>
+ <div class="col-12 col-md-9 d-flex flex-wrap align-items-center justify-content-center justify-content-sm-start">
+ <i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill m-2 ms-0" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.user.tfa_info|raw }}"></i>
+ <select data-style="ms-2 btn btn-sm dropdown-toggle bs-placeholder btn-secondary" data-width="fit" id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}">
+ <option value="yubi_otp">{{ lang.tfa.yubi_otp }}</option>
+ <option value="webauthn">{{ lang.tfa.webauthn }}</option>
+ <option value="totp">{{ lang.tfa.totp }}</option>
+ <option value="none">{{ lang.tfa.none }}</option>
+ </select>
+ </div>
</div>
- </div>
- </div>
- <br>
- <div class="row" id="status-fido2">
- <div class="col-sm-3 col-5 text-end">{{ lang.fido2.register_status }}:</div>
- <div class="col-sm-9 col-7">
- <div id="fido2-alerts">-</div>
- </div>
- <br>
- </div>
- <hr>
- <div class="row">
- <div class="col-md-3 col-12 text-sm-end text-start mb-4"><i class="bi bi-file-earmark-text"></i> {{ lang.user.apple_connection_profile }}:</div>
- <div class="col-md-9 col-12">
- <p><i class="bi bi-file-earmark-post"></i> <a href="/mobileconfig.php?only_email">{{ lang.user.email }}</a> <small>IMAP, SMTP</small></p>
- <p class="text-muted">{{ lang.user.apple_connection_profile_mailonly }}</p>
- {% if not skip_sogo %}
- <p><i class="bi bi-file-earmark-post"></i> <a href="/mobileconfig.php">{{ lang.user.email_and_dav }}</a> <small>IMAP, SMTP, Cal/CardDAV</small></p>
- <p class="text-muted">{{ lang.user.apple_connection_profile_complete }}</p>
{% endif %}
- </div>
- </div>
- <div class="row">
- <div class="col-md-3 col-12 text-sm-end text-start mb-4"><i class="bi bi-file-earmark-text"></i> {{ lang.user.apple_connection_profile }}<br class="d-none d-lg-block" />{{ lang.user.with_app_password }}:</div>
- <div class="col-md-9 col-12">
- <p><i class="bi bi-file-earmark-post"></i> <a href="/mobileconfig.php?only_email&app_password">{{ lang.user.email }}</a> <small>IMAP, SMTP</small></p>
- <p class="text-muted">{{ lang.user.apple_connection_profile_mailonly }}<br /> {{ lang.user.apple_connection_profile_with_app_password }}</p>
- {% if not skip_sogo %}
- <p><i class="bi bi-file-earmark-post"></i> <a href="/mobileconfig.php?app_password">{{ lang.user.email_and_dav }}</a> <small>IMAP, SMTP, Cal/CardDAV</small></p>
- <p class="text-muted">{{ lang.user.apple_connection_profile_complete }}<br /> {{ lang.user.apple_connection_profile_with_app_password }}</p>
+ {# FIDO2 #}
+ {% if mailboxdata.authsource == "mailcow" %}
+ <div class="row mt-4">
+ <div class="col-sm-3 col-12 text-sm-end text-start">
+ <p><i class="bi bi-shield-fill-check"></i> {{ lang.fido2.fido2_auth }}</p>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-sm-3 col-12 text-sm-end text-start mb-4">
+ {{ lang.fido2.known_ids }}:
+ </div>
+ <div class="col-sm-9 col-12">
+ <div class="table-responsive">
+ <table class="table table-striped table-hover table-condensed" id="fido2_keys">
+ <tr>
+ <th>ID</th>
+ <th style="min-width:240px;text-align: right">{{ lang.admin.action }}</th>
+ </tr>
+ {% include 'fido2.twig' %}
+ </table>
+ </div>
+ <br>
+ </div>
+ </div>
+ <div class="row">
+ <div class="offset-sm-3 col-sm-9">
+ <div class="btn-group nowrap">
+ <button class="btn btn-sm btn-primary d-block d-sm-inline" id="register-fido2">{{ lang.fido2.set_fido2 }}</button>
+ <button type="button" class="btn btn-sm btn-xs-lg btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
+ <ul class="dropdown-menu">
+ <li><a class="dropdown-item" href="#" id="register-fido2-touchid"><i class="bi bi-apple"></i> {{ lang.fido2.set_fido2_touchid }}</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <br>
+ <div class="row" id="status-fido2">
+ <div class="col-sm-3 col-5 text-end">{{ lang.fido2.register_status }}:</div>
+ <div class="col-sm-9 col-7">
+ <div id="fido2-alerts">-</div>
+ </div>
+ <br>
+ </div>
{% endif %}
</div>
- </div>
- <hr>
- <div class="row">
- <div class="offset-sm-3 col-sm-9">
- <p><a target="_blank" href="https://docs.mailcow.email/client/client/#{{ clientconfigstr }}">[{{ lang.user.client_configuration }}]</a></p>
- <p><a href="#userFilterModal" data-bs-toggle="modal">[{{ lang.user.show_sieve_filters }}]</a></p>
+ <div class="ms-auto col-xl-3 col-lg-5 col-md-12 col-12 d-flex flex-column well flex-grow-1">
+ <legend class="d-flex">
+ <span>{{ lang.user.recent_successful_connections }}</span>
+ <div id="spinner-last-login" class="ms-auto my-auto spinner-border spinner-border-sm d-none" role="status">
+ <span class="visually-hidden">Loading...</span>
+ </div>
+ </legend>
<hr>
- <h4 class="recent-login-success">{{ lang.user.recent_successful_connections }}</h4>
- <div class="dropdown mt-2">
- <button class="btn btn-secondary btn-xs btn-xs-lg dropdown-toggle" type="button" id="history_sasl_days" data-bs-toggle="dropdown">{{ lang.user.login_history }}</button>
- <ul class="dropdown-menu">
- <li class="login-history" data-days="1"><a class="dropdown-item" href="#">1 {{ lang.user.day }}</a></li>
- <li class="login-history" data-days="7"><a class="dropdown-item active" href="#">1 {{ lang.user.week }}</a></li>
- <li class="login-history" data-days="14"><a class="dropdown-item" href="#">2 {{ lang.user.weeks }}</a></li>
- <li class="login-history" data-days="31"><a class="dropdown-item" href="#">1 {{ lang.user.month }}</a></li>
- </ul>
+ <h6 class="last-ui-login"></h6>
+ <div class="d-flex">
+ <span class="clear-last-logins mt-auto mb-2">
+ {{ lang.user.clear_recent_successful_connections }}
+ </span>
+ <div class="dropdown mt-4 mb-2 ms-auto">
+ <button class="btn btn-secondary btn-xs btn-xs-lg dropdown-toggle" type="button" id="history_sasl_days" data-bs-toggle="dropdown">{{ lang.user.login_history }}</button>
+ <ul class="dropdown-menu">
+ <li class="login-history" data-days="1"><a class="dropdown-item" href="#">1 {{ lang.user.day }}</a></li>
+ <li class="login-history" data-days="7"><a class="dropdown-item active" href="#">1 {{ lang.user.week }}</a></li>
+ <li class="login-history" data-days="14"><a class="dropdown-item" href="#">2 {{ lang.user.weeks }}</a></li>
+ <li class="login-history" data-days="31"><a class="dropdown-item" href="#">1 {{ lang.user.month }}</a></li>
+ </ul>
+ </div>
</div>
- <div class="last-login mt-4" id="recent-logins"></div>
- <span class="clear-last-logins mt-2">
- {{ lang.user.clear_recent_successful_connections }}
- </span>
+ <ul class="list-group last-sasl-login overflow-auto" style="flex: 1 1 0; min-height: 400px;"></ul>
</div>
</div>
</div>